インターフェース入門:操作と中身を分ける
車のステアリングとペダル、テレビのリモコンから考える「外から見える約束」— カプセル化、差し替え可能な実装、そしてポリモーフィズムまで
身の回りのインターフェース
車を運転するとき、運転者はステアリングを回し、アクセルやブレーキを踏みます。タイヤの角度を変える機構、燃料噴射、モーター制御、油圧などの内部構造を直接操作する必要はありません。車種や動力源が変わっても、運転者に見える基本的な操作はよく似ています。
テレビのリモコンも同じです。視聴者は電源、音量、チャンネルというボタンを使います。ボタンを押した後に赤外線や無線の信号がどう送られ、テレビ内部の回路がどう動くかを知らなくても、目的の操作ができます。
車の操作系
運転者は共通の操作方法を使い、車両内部の異なる機構はその操作をそれぞれの方法で実現します。
曲がる
止まる
進む
テレビのリモコン
ボタンの名前と信号の決まりが、視聴者とテレビをつなぐ操作上の約束になります。
インターフェースとは、利用者と中身の境界に置かれた「操作方法の約束」です。利用者はその約束だけを知れば、中の仕組みを知らなくても目的を達成できます。ここでいうインターフェースは画面だけでなく、人と機械、部品と部品の接点全般を指します。
プログラムでは「何ができるか」を型にする
プログラムでも、部品を使う側に必要なのは「呼べる操作」「渡す値」「返る値」の約束です。TypeScript の interface を使うと、その約束を型として表現できます。
// 利用者に見せる「操作の約束」だけを宣言する interface CarControls { steer(direction: number): void accelerate(amount: number): void brake(amount: number): void } interface RemoteControllable { power(): void volumeUp(): void selectChannel(channel: number): void }
CarControls は steer、accelerate、brake を使えることだけを宣言しています。RemoteControllable も、電源・音量・チャンネルの操作だけを宣言します。利用者側のコードは、この約束を満たす相手なら同じように扱えます。
重要なのは、この宣言がエンジンやモーター、赤外線送信、画面表示の実装方法を決めていないことです。また、型の適合は操作の形を確認するもので、実装が正しい結果を返すことまで証明するものではありません。
カプセル化:中身を隠し、正しい状態を守る
インターフェースが外側へ見せる約束なら、カプセル化(encapsulation)は内側の実装や状態を境界の中へ閉じ込める考え方です。利用者が内部状態を勝手に書き換えず、公開された操作を通してだけ扱えるようにします。
class ElectricCar implements CarControls { #speed = 0 #batteryPercent = 80 steer(direction: number) { if (direction < -1 || direction > 1) { throw new RangeError('direction must be between -1 and 1') } // 内部の操舵装置を動かす } accelerate(amount: number) { if (amount < 0 || amount > 1) { throw new RangeError('amount must be between 0 and 1') } if (this.#batteryPercent === 0) return this.#speed += amount * 5 this.#batteryPercent -= 1 } brake(amount: number) { if (amount < 0 || amount > 1) { throw new RangeError('amount must be between 0 and 1') } this.#speed = Math.max(0, this.#speed - amount * 8) } }
この ElectricCar では、速度とバッテリー残量を private なフィールドにしています。外部は値を直接変更できず、範囲を確認する accelerate や、速度を 0 未満にしない brake を通して操作します。これにより、オブジェクトが守るべき条件を一か所で維持できます。
カプセル化は、すべてのフィールドへ getter と setter を付けることではありません。内部データをそのまま公開すると、呼び出し側が内部構造へ依存します。「速度を設定する」より「加速する」のように、目的を表す操作だけを公開することが重要です。
インターフェースは「外に何を見せるか」、カプセル化は「内側をどう隠して守るか」。別の概念ですが、境界の表と裏として一緒に働きます。
ポリモーフィズム:同じ操作、違う中身
ポリモーフィズム(polymorphism、多態性)とは、ひとつのインターフェースを満たす複数の実装を、同じ呼び出し方で扱える性質です。利用者は具体的な種類ではなく、共通の約束に対してプログラムします。
運転するコード
CarControls
同じ呼び出し方
GasolineCar:エンジンで実現
ElectricCar:モーターで実現
ガソリン車ではアクセル操作が燃料とエンジンを制御し、電気自動車ではバッテリーからモーターへの電力を制御します。ブレーキも油圧式や回生式など中身は異なります。それでも両方が CarControls を満たせば、運転する側は同じコードを使えます。
class GasolineCar implements CarControls { steer(direction: number) { /* 前輪の向きを変える */ } accelerate(amount: number) { /* 燃料を送り、エンジン回転を上げる */ } brake(amount: number) { /* 油圧ブレーキを作動させる */ } } class ElectricCar implements CarControls { steer(direction: number) { /* モーターで操舵する */ } accelerate(amount: number) { /* 電力をモーターへ送る */ } brake(amount: number) { /* 回生ブレーキを使う */ } } function startDriving(car: CarControls) { car.accelerate(0.4) car.steer(0.2) car.brake(0.3) } startDriving(new GasolineCar()) startDriving(new ElectricCar()) // 呼び出し側は同じコード
呼び出し側に車種ごとの if や switch を並べず、実際に渡されたオブジェクトが自分の処理を実行します。新しい車種を追加しても、共通の約束が変わらなければ startDriving を書き換える必要はありません。
なぜ設計が楽になるのか
境界の約束を小さく明確にすると、利用する側と実装する側を分けて考えられます。内部を変更しても約束を守る限り、相手側への影響を抑えられます。
ただし、インターフェースを作るだけで良い設計になるわけではありません。曖昧な名前、巨大な操作一覧、守れない約束は依存を増やします。利用者が本当に必要とする最小限の操作を、意味と制約が伝わる形で定義することが大切です。
実装を差し替えられる
ガソリン車を電気自動車へ、実機をシミュレーターへ変えても、同じ約束を満たせば利用側を保てます。
分担しやすい
境界を先に合意すれば、利用側と実装側を別々に作れます。理解する範囲も部品の内側へ限定できます。
テストしやすい
本物の車両やテレビの代わりに、同じインターフェースを持つ偽物を渡し、利用側の振る舞いを独立して確認できます。
よくある誤解
インターフェースは画面だけではありません。API、関数の引数と返り値、クラスの公開操作、機器間の信号など、部品どうしの境界もインターフェースです。
カプセル化は private を付けるだけでも、getter と setter を増やすことでもありません。内部状態を守り、利用者が内部構造へ依存しない操作を設計することです。
ポリモーフィズムは種類を調べる if や switch の別名ではありません。共通の約束を通じて呼び出し、具体的な実装自身に振る舞いを選ばせることが中心です。
まとめ
インターフェースは、境界の外から使える操作の約束。「何ができるか」を示し、「どう実現するか」から利用者を切り離す。
カプセル化は、実装と状態を内側へ閉じ込め、公開した操作を通して正しい状態を守る。インターフェースとは別概念だが、同じ境界の表と裏になる。
ポリモーフィズムは、同じインターフェースを満たす異なる実装を、同じコードで扱える性質。呼び出し側の種類分けを減らせる。
明確で小さな約束は、実装の差し替え、分担、テストを容易にする。ただし型への適合だけで、実装の正しさまで保証されるわけではない。