2. 後半:インターフェースの使い方
ここまで理解できれば、インターフェースが価値を発揮するケースを理解することができます。
ケーススタディ形式で説明します。
インターフェースがない状態から始め、改修が発生し、インターフェースを導入する流れを見ていきます。
2-1. 改修:新しいPersonの定義
吠えられたらコンソールにログを吐くのではなく、脳内に記憶するタイプのPersonを導入したくなったとします。
2-2. サンプルコード
先程の例から、インターフェースを取り除きます。
DogはSomePersonを受け取っています。
[型]
class Dog {
bark(target: SomePerson) {
target.hear('ワン');
}
}
class SomePerson {
hear(sound: string) {
console.log(sound)
}
}
[スクリプト]
const dog = new Dog();
const person = new SomePerson();
dog.bark(person); // console.log("ワン")となる
interface定義を抜いただけに見えますが、インターフェースが誰のものかを意識できるようになると、クラスの関係性が全く異なることに気がつくと思います。
先程のイメージと比べてみてください。
2-3. 改修手順
2-3-1. MemorizingPersonを定義する
インターフェースを定義しないまま、サンプルコードに新しくMemorizingPerson
を追加します。
新しいPersonの定義自体は、これで完了です。
[型]
...
// 脳内に記憶するPerson
class MemorizingPerson {
sounds:string[] = [];
hear(sound: string) {
this.sounds.push(sound);
}
}
2-3-2. 定義したクラスを使う(エラー発生)
これらのクラスを使って以下のようなスクリプトを書きます。
[スクリプト]
const dog = new Dog();
const somePerson = new SomePerson();
const memorizingPerson = new MemorizingPerson();
dog.bark(somePerson)
dog.bark(memorizingPerson); // ここでエラー
dog.bark(memorizingPerson); // ここでエラー
for (const sound of memorizingPerson.sounds) {
console.log('memory: ' +sound)
}
このようなコードは、機能しません。
Dog
は引数として具体的にSomePerson
を指定しており、MemorizingPerson
はSomePerson
の一種ではないためです。
例えば普通の静的型付け言語だと、コンパイルエラーになります。
※言語によっては、このコードは動いてしまいますが、望ましくない点は同様です。
2-3-3. インターフェースを導入する
ここで、インターフェースを導入します。
Dog
がSomePerson
に依存するのではなく、Dog
はPerson
というインターフェースを宣言し、SomePerson
やMemorizingPerson
がこのインターフェースを満たしに来るようにします。
このとき、頭の中に以下のようなイメージを描きます。
コードにすると次のようになります。
[型]
class Dog {
bark(target: Person) {
target.hear('ワン');
}
}
interface Person {
hear(sound: string): void;
}
class SomePerson implements Person {
hear(sound: string) {
console.log(sound)
}
}
class MemorizingPerson implements Person {
sounds:string[] = [];
hear(sound: string) {
this.sounds.push(sound);
}
}
これにより、以下のコードは機能するようになります。
改修はこれで完了です。
[スクリプト(再掲)]
const dog = new Dog();
const somePerson = new SomePerson();
const memorizingPerson = new MemorizingPerson();
dog.bark(somePerson) // DogにとってのPersonを実装する、SomePersonインスタンスを渡す
dog.bark(memorizingPerson); // DogにとってのPersonを実装する、MemorizingPersonインスタンスを渡す
dog.bark(memorizingPerson); // 同上
for (const sound of memorizingPerson.sounds) {
console.log('memory: ' +sound)
}
フォルダを分けるなら、クイズから学んだとおり、以下のような構成になります。
これで、このケーススタディは完了です。
2-4. インターフェースによって何が得られたか
インターフェースを導入したことで、Dog
クラスは複数のPersonクラスの実装と一緒に使うことができるようになりました。
加えて、今回はインターフェースを導入するためにDogクラスの改修が必要になりましたが、今後はDog
を一切改修せずに新しいPerson
を定義できるようになったことも、大きなメリットです。(疎結合性)
例えばDog
クラスのみをライブラリとして世界に公開するとします。
ライブラリは全世界で使われるため、ライブラリの利用者が定義するPersonに応じて、都度Dogを改修するわけにはいきません。
このような場合、Dog
にとってのPerson
インターフェースを定義することは不可欠になります。
まとめ
「誰のもの?」を意識するようにすることで、インターフェースを上手く定義することができます。
Dog
クラス、SomePerson
クラス、Person
インタフェースがあれば、Person
インターフェースはたいていDog
クラスのものです。
飼い主
クラス、ペット
インターフェース、Someペット
クラスがあれば、ペット
インターフェースはたいてい飼い主
クラスのものです。
インターフェースから得られるのは、疎結合性です。今回の例では、Dog
クラスに一切修正を加えず、新たなPerson
実装クラスを定義し、連携させて使うことができるようになります。
だからこそ、この目的に従ってインターフェースを使っていれば、インターフェースはDog
クラスなど、オブジェクトを受け取る側のものになります。
Person
インターフェースがSomePerson
クラスの持ち物だとしたら、新しいPersonが定義されるたびにPerson
インターフェースが影響を受け、結果としてDog
クラスを何度も修正することになるからです。
参考情報
実務経験の他には、分厚い古典から学んだ知識が多いように思います。
読んだのは3〜5年ほど前なので、具体的にどの書籍に何が載ってた、というレベルでは覚えていませんが、「インターフェースについて良く学んだ気がする」ベスト3を載せておきます。
※個人的には、この記事に記載されている内容が分かっていれば、(特にアプリケーション開発であれば)わざわざ時間割いて読まなくても良いかと思います。
追伸
オブジェクト指向にハマったのは5年くらい前でした。
オブジェクト指向分析設計のような沼にもハマったので、「オブジェクト指向は、神が与え給うた、現実世界を忠実にモデリングできる手法である」とでも言わんとするオブジェクト中二病を、1年くらいこじらせていた気がします。
一度発信してみたかったのですが、こうして5年越しに記事を書けてよかったです。
需要がありそうなら、インターフェースの実践的な使い方や、「オブジェクト指向プログラミングらしく書くコツ」といったテーマで、オブジェクト指向シリーズをいくつか書くかもしれません。