はじめに
数年ぶりに投稿ですが、Javaの頃からオブジェクト指向の説明でInterfaceをちゃんと説明している記事が少ない気がするので一度書き出してみる。
昔書いた気がするけど気のせいかな。。。
結論
下記のようなTypeScriptのコードでの説明は正しい、と思う。
interface Task {
name: string;
task: () => string;
}
class Test implements Task {
name: string;
task() {
return `${this.name} san`;
}
constructor(name: string) {
this.name = name;
}
}
class Hoge implements Task {
name: string;
id: number;
task() {
return `${this.name} #${this.id} chan`;
}
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
function task_twise(task: Task): string {
return task.task() + task.task();
}
よくある説明その1
一番多いやつ
コード
interface Task {
name: string;
task: () => string;
}
class Test implements Task {
name: string;
task() {
return `${this.name} san`;
}
constructor(name: string) {
this.name = name;
}
}
const test = new Test("test");
console.log(test.task())
中身の解説
- Taskインターフェースはnameという変数とtaskという関数を定義している
- TestクラスはTaskインターフェースを実装していて、taskを実行すると名前に"san"を付けて返してくれる
- Testクラスを作って実行するとtaskが実行される
何がまずいか
- 初心者がこれを見ても「なんでインターフェースなんていう面倒なことしないといけないの?」という疑問しか抱かない
- 別にTaskインターフェースが無くても成り立つ
- 「こうやってインターフェースを定義しておくことが大事なんだよ」みたいな謎説明をする人もいる
よくある説明その2
コード
最後から2行目だけが下記のように変化
const test:Task = new Test("test");
何がまずいか
- Taskで受け取っているので一見するとTaskが役に立っているように見えるがやっぱり無くていいように思える
- というか無くても別に問題ない
よくある説明その3
実際に見たわけではないけれども、こういう似たような形のは結構ある、という例。
コード
class Test implements Task {
name: string;
task() {
return `${this.name} san`;
}
constructor(name: string) {
this.name = name;
}
}
class Hoge implements Task {
name: string;
id: number;
task() {
return `${this.name} #${this.id} chan`;
}
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
let test: Task = new Test("test");
console.log(test.task());
test = new Hoge("hoge", 123)
console.log(test.task());
解説
- Testクラスと同じくTaskインターフェースを実装したHogeクラスが登場
- 同じTask型の変数に別のオブジェクトを入れて実行できる
何がまずいか
- ぶっちゃけそんなにまずくない
- ただ、これも変数を使い回す必要性を感じない
- 別に2つ宣言すればいいじゃん、となる(というかそうする方がキレイ)
ということで最初のコードに
再掲
interface Task {
name: string;
task: () => string;
}
class Test implements Task {
name: string;
task() {
return `${this.name} san`;
}
constructor(name: string) {
this.name = name;
}
}
class Hoge implements Task {
name: string;
id: number;
task() {
return `${this.name} #${this.id} chan`;
}
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
function task_twise(task: Task): string {
return task.task() + task.task();
}
解説
- Test, Hogeのクラスを作るところは一緒
- それとは別に、Taskを受け取って2回実行するという関数を用意する
- この関数はTestやHogeの実装とは分離しているのでTestやHogeの実装を変更しても影響を受けない
- クラス名を変えても影響を受けないし他のクラスが出てきてもtask_twiseは実行できる
何を教えないといけないか
- この他にも作業者AとBに分けて、それぞれがインタフェースファイルを見ながら実装側・利用側に分けて作業ができる、というような説明も良いと思う
- いわゆるスタブとしてのインターフェース
- インターフェースは機能の境界面であるということを教える必要がある
- 機能の境界面の向こうとこちらではインターフェース以外の実装が干渉しないように作らないと意味がない
- ソースコードは書き換わっていくものだ、という前提がまず必要
- 書き換えるときに影響を最小限にとどめたい、という動機が必要
- 最小限に留めるために抽象化できるところは抽象化する、ということを教える必要がある
- 抽象化・インターフェース化することでバグの伝播を防ぐ、という教え方が必要になる
さいごに
ということで、正直なところこのソースだけ見てもやっぱり分からない人には分からないだろう、ということになるので何かしらインターフェースを定義しないことでソースの書き換えが大変になる、ということを追体験できるような説明が良いのだろうな、と思う。