【オブジェクト指向プログラミング】誰のもの?で理解するインターフェース
前置き
背景
オブジェクト指向プログラミングのインターフェースについて、以下のような例で学んだ方は多いのではないでしょうか。
public interface Animal {
void bark();
}
public class Dog implements Animal {
void bark() {
System.out.println("ワン");
}
}
public static void main(String[] args){
Animal animal = new Dog(); // Animal型の変数にDogインスタンスを代入できる
animal.bark();
}
上記は極端な例ですが、
- 「
Dog dog = new Dog();
と比べて何がいいの?」
という疑問に答えてくれる情報をなかなか見つけることができず、言語仕様と割り切って覚えてきた方は、実際に多いのではないでしょうか。
筆者は、「エンタープライズアプリケーションアーキテクチャパターン」「GoFデザインパターン」「実践UML」「Object Design」「実装パターン」「アジャイルソフトウェア開発の奥義」「オブジェクト指向入門」「ドメイン駆動設計」などなど、名高いオブジェクト指向の古典を20冊以上読んできました。(※)
この記事では、元オブジェクト指向原理主義厨である筆者が、インターフェースを解説し、読者の方にインターフェースの本当の使い方を理解いただきます。
※現代に生きる皆さんがこうした古典を何冊も読むことは、推奨しません。現代においても通用するエッセンスはごく僅かなためです。この記事を読めば十分と思います。
指針
私はインターフェースを定義するとき、「インターフェースは誰のもの?」ということについて、強くイメージしています。
この感覚をシェアすることで、インターフェースについて皆さんの理解が深まると考えます。
この記事では、前半で「インターフェースは誰のもの?」というテーマでインターフェースという概念を深く理解いただき、後半でケーススタディをなぞることで、実際にどのように便利に使うことができるのかを理解いただきます。
想定読者
以下のような方を読者として想定しています。
- 言語仕様は分かっているものの、いまいちインターフェースの使い道が分かっていない初学者
- 今までなんとなくインターフェースを使ってきた、あるいは避けてきたエンジニア
インターフェースの言語仕様を押さえていることが前提です。
※どの言語でも構いません。
【経験者の方向け】
オブジェクト指向プログラミングの経験者の方向けの記事ではありませんので、ご注意ください。
主にDIや依存性逆転のためにインターフェースを活用するユースケースをターゲットに解説しています。
この記事のゴール
ゴールは以下になります。
- 「インターフェースって言語仕様としては分かるけど、実際どういうときに使うんだろう?」という疑問を解消し、腹落ちいただくこと
以下はこの記事のスコープではありません。
- 具体的なドメイン(要件)に対して、どのようにモデリングしてコードに落とすべきか
サンプルコードの言語
オブジェクト指向プログラミングにおけるインターフェースを具体的に理解するために、サンプルコードを用いています。TypeScript風言語で記載します。
1. 前半:インターフェースは誰のもの?
1-1. サンプルコード
「インターフェースは誰のもの?」について考えるために、以下をサンプルコードとします。
Dog
はワンなど、鳴き声を管理する責務を持っています。SomePerson
は聞き取った物事を聞き取って何かをする責務を持っています。
そして、Person
インターフェースを定義しています。
※もちろんコードがこれだけであれば、インターフェースを定義することに大きな意味はありません。あくまでサンプルです。
[型]
class Dog {
bark(target: Person) {
target.hear('ワン');
}
}
interface Person {
hear(sound: string): void;
}
class SomePerson implements Person {
hear(sound: string) {
console.log(sound)
}
}
[スクリプト]
const dog = new Dog();
const person = new SomePerson();
dog.bark(person); // console.log("ワン")となる
1-2. クイズ:インターフェースは誰のもの?
本題の「インターフェースは誰のもの」について、解説を見る前に30秒くらい時間を取って考えてみていただきたいです。
このPerson
インターフェースは、SomePerson
のものでしょうか?それともDog
のものでしょうか?
もしフォルダを分けるとしたら、インターフェースはどちらに入れますか?
1-3. 正解発表
正解は、Bです。
もちろん絶対的正解はありませんが、通常のアプリケーション開発であれば、Bが正解になることが多いです(細かいことは割愛します)。
ここで迷いなくBを選べた方は、この記事を読む必要はないかもしれません。
ここで迷った方やAを選んでしまった方は、最後まで読んでいただきたいです。
1-4. 解説
現実世界のたとえ話でイメージを膨らませるところから始めます。
1-4-1. 現実世界の例でイメージを膨らませる
例えばここにType-Cで充電できるPCと、周辺機器メーカの充電器があるとします。
このとき、「充電器はこのような規格で接続する」という制約は、PC側が提示するものです。Mic●osoftがPCを作って、取扱説明書や仕様書に「充電はこのような規格であるべき」と記載しているはずです。
そしてエ●コム社がその制約を満たすように充電器を作っているため、この充電器でPCを充電することができます。
この図における、「充電器はこう」というPC側が提示している制約が、オブジェクト指向プログラミングにおけるインターフェースのイメージに近いです。
※現実世界では、Type-Cなど標準化された規格に依存する形でPC側も充電器側も作っているため、この例はベストではないかもしれません。ここでは、Mic●soft社のPCが世界に1つだけのオレオレ規格を定義しており、エ●コム社の充電器はそのPCのためだけに作られているものと考えてください。
1-4-2. Dogの例を理解する
コードでインターフェースを定義するときの感覚は、上記の構造に良く似ています。
この構造をDog
の例に適用すると、次のようなイメージになります。
Person
インターフェースがDog
のものである、という感覚がお分かりいただけますでしょうか。
このイメージを見た上で、もう一度コードを眺めてみてください。
Dog
とPerson
の強い結びつきが見えてくるかと思います。
[型(再掲)]
class Dog {
bark(target: Person) { // DogにとってのPersonを受け取る
target.hear('ワン');
}
}
// DogにとってのPersonはこう
interface Person {
hear(sound: string): void;
}
// DogにとってのPersonを満たす(実装する)
class SomePerson implements Person {
hear(sound: string) {
console.log(sound)
}
}
[スクリプト(再掲)]
const dog = new Dog();
const person = new SomePerson();
dog.bark(person); // DogにとってのPersonを実装する、SomePersonインスタンスを渡す
注意
- 例えば
Dog
インターフェースがあったとしたら、それはおそらくPerson
のもので、関係性が逆になります。
2. 後半:インターフェースの使い方
ここまで理解できれば、インターフェースが価値を発揮するケースを理解することができます。
ケーススタディ形式で説明します。
インターフェースがない状態から始め、改修が発生し、インターフェースを導入する流れを見ていきます。
2-1. 概要
吠えられたらコンソールにログを吐くのではなく、脳内に記憶するタイプの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
実装クラスを定義し、連携させて使うことができるようになります。
細かいことを言えばインターフェースの使われ方はこれだけではありません。しかし、この使い方が理解できていれば、インターフェースの価値を最大限に発揮させることができます。
追伸
この記事はMagicodeから移転しました。
2022年9月あたりの記事となります。
よければTwitterもフォローお願いします!
@sumiren_t
Discussion
Dog
とCat
がbark
を実装したとしても、Person
はDog
フォルダに入りますか?ありがとうございます!
Personを使いたいクラスが他にもでてきてしまったら、ということですね。
あくまでこの記事で説明したいのはイメージで、実際には状況によるところもあるかと思います。
ですが、それだけではゼロ回答と思いますので、一例を回答しますね...!
ざっくり2パターンあるかなと思っています。
【1. Catが本当にDogと同じPersonインターフェースを利用したいとき】
CatもPerson.hearを使いたく、要件的にDogに渡したいオブジェクトはCatにも渡せていいと思えるときですね。
こういうときは、DogとCatを同じモジュールやパッケージに含め、インターフェースをそのモジュールの持ち物と考えると良いと思います。
例えば、シンプルに考えるなら、以下のようなフォルダ構造です。
実際、実務ではそういったパターンのほうが多いかと思います。
レイヤ化アーキテクチャ系のアーキテクチャでは、こうした形で依存関係をレイヤ単位で考える傾向があります。
以下はヘキサゴナルアーキテクチャ/クリーンアーキテクチャのフォルダ構造例です。
※少しむずかしい話です。より理解度が深まればと思い一応書いていますが、分からなければ飛ばして大丈夫です。
【2. CatはSomePersonなどを使いたいものの、hearだけが使いたいわけじゃない場合】
例えば、CatはSomePerson等のwatch()メソッドを呼び出し、SomePersonに新たにwatchメソッドを生やしたいとします。
※要件は適当です。あくまで、そういう場合があったら、という前提です。
このように、本質的にCatもDogもPersonという概念とコラボレーションしたいが、それぞれ異なる振る舞いを呼び出したい場合には、インターフェースを分けてしまう手もあります。
こういった方針をインターフェース分離の法則と言ったりします。
以下は具体的な1つの改修例と、コード例です。
このとき、フォルダ構造の例は以下です。
さすがにそれは酷すぎます。
インターフェースの意味や意義を誤解しているのでは?
お返事ありがとうございます!
そういった考え方もあるかと思います。
記事がお役に立たなかったようで申し訳ありません!
記事拝見させていただきました。
いいねがたくさん付きつつ、疑問を持つ方も現れているのは以下のようなあたりにあるのではないかと思いました。
問題点
犬が人を観察して得た(人に依存した)情報を自身に持つことは、人の性質がdogや様々なディレクトリに分散していくことを示しているため、関心の分散を生じさせてしまいます。
また、2-3-2と2-3-3で矢印の方向がdogとperson両方を行き来しているように、この設計ですと事実上の共依存の関係になってしまいます。
よりわかりやすくしますと、ここに人が吠えて犬が聴く要件が増えた場合、personディレクトリにdog.tsが必要になってしまうことになるわけです。
これで猫やら虫(吠えるではなく鳴くになりますが)やら出てきてしまうと、全てに共依存が発生して物凄いことになってしまいます。
解決策
このような共依存の関係になる場合、依存先をお互いの中立の場所に抽象として切り出すべきということを表しています。
今回の例では、抽象化すると犬は何らかの聴ける対象に吠えるわけですから、Hearingというインタフェースをdogとpersonの外部に用意するのが一つの手段です。
犬は聴ける対象(Hearing)に吠え、人は耳で聴けるわけですからHearingを実装します。
こうすることで、犬はHearingに依存し、人もHearingに依存する関係となり、犬と人の間に直接の関係性がなくなります。
これなら人が吠えて犬が聴く場合も共依存にはなりません。
これは、PCと充電器の話でも同じです。
PC側のコネクタが何らかの統一規格に依存し、充電器のコネクタも同じ統一規格に依存しているはずです。
PCと充電器に直接の依存性はありません。
つまり、二者間で解決するべき問題ではなく、二者の間に一つの規格を挟む設計にするのが解決法かと思います。
犬側に置くインタフェース
犬(自身)のディレクトリにインタフェースを置いて問題ないパターンは、
犬(自身)はこういったものを受け入れることができますというように、
犬(自身)が外部に依存せず自由に規格を決められる(自身に依存する)ものになるはずです。
例えば、犬ではなく犬ロボットがあり、そのロボットは専用のコネクタから充電できるとします。
このコネクタの規格は犬ロボット専用なので、dog-robotのディレクトリにコネクタのインタフェースを置くことができます。
このインタフェースを利用して、各社が充電器を実装すれば、犬ロボットはどの会社が作った充電器であっても受け入れることができるでしょう。
各社の作った充電器は犬ロボットに依存していますが、犬ロボットは各社の作った充電器に依存せず、こちらも単方向の状態を保てます。
記事の「誰のもの」というのが「犬側に置くインタフェース」の例のようなものをイメージされているのであれば、
本来の意図は間違っていないのかもしれませんが、dogディレクトリにperson.tsを置くという例は、読者を誤った方向に導きかねないかなと感じました。
コメントありがとうございます!
大きく、
の2点が指摘と解釈しました。
基本的には同じ意見です!
【1. 循環依存について】
そうですね、おっしゃるとおりだと思います。
図解が分かりづらかったようで申し訳ありませんが、2-3-2はインターフェースが存在せずDogからPersonの実装に依存した場合の図で、「これではダメですよね」ということを表現している図でした。
2-3-3がこの記事のケーススタディにおいて正しいとしている図でして、これはコメントいただいているとおり、DogもPersonの実装も、Dog専用のPersonインターフェースに依存しているクラス図になる認識です。
しっかり覚えてもらいたい意味でインパクトを取ってしまったところがあるかもしれません...!
意図はご想像どおりです!
【2. クラスごとに専用のインターフェースを設けることについて】
ここもおっしゃるとおりだと思います。
PoEAAのドメインモデルパターンのような、いわゆるオブジェクト指向分析設計 / 狭義のオブジェクト指向プログラミングが求められるドメイン / アーキテクチャ / 開発方針であれば、オブジェクトモデリングをしっかり行う中で、コメントいただいている「統一規格」が出てくるベきと思います。
この記事では、そうした状況ではなく、主にDI / 依存性逆転の実現のためにインターフェースが使われる場合を想定して説明をしています。
これは、以下のような背景を踏まえ、このような意思決定をしております。
【結論とお礼】
ご意見は全て同意です!
一方で、初心者にもシンプルに説明しつつ、嘘にならない/誤解を招かないようにするのがなかなか難しく...!
私も改めて誤解を招かないための改善点を考えたいと思いますので、「ここをこうしたらわかりやすくなるんじゃないか」といった点がございましたら、是非ご指摘いただけますと幸いです!
この記事のコンセプトを否定するようなコメントになり恐縮なのですが、他の方も指摘している通り「インターフェースは誰のものなのか」という考え方自体、あまり適切とは思えませんでした。
personというディレクトリがあるのにdogにperson.tsがあるのはどう考えても不自然ですし、「誰のものなのか」という点を意識してしまうがために陥ってしまった気がします。misukenさんのおっしゃる通り、(ディレクトリ名云々の問題でなく)インターフェースはdogのものではなく切り出すのが自然に思えます。
インターフェースとは本来「ふるまいの共通化・契約(抽象化)」のためのもので、利用者側が実装に依存せず使えるものという点が、この記事ではあまり説明されておらず(DIやDDDの文脈であれば尚更)、大変失礼な表現で申し訳ないのですが、初心者向けだったり表現が難しい以前に、筆者の方の知識が不足していたり偏りがあるように感じました。
コメントいただきありがとうございます!
記事がお役にたたなかったようで申し訳ありません!
いえ、ありがとうございます。
有識者の方と議論をするつもりはないのですが、記事を読まれる方々がその記事の妥当性を判断するためにコメントを参照することもあろうかと思いますので、率直に記載いただき感謝いたします。
上記のとおり議論をする気はないですが、いただいたコメントについて、読者の方向けに筆者のコメントを記載しておきます。
【読者の方向け 筆者コメント】
野暮かと思い本文に書いていませんが、これはコメントいただいているとおり、状況に依りますので、なんでもこういうふうにしてほしいというわけではない旨、ご注意ください...!
多少、覚えていただくためにインパクトに残りやすい構成にしている部分はあります。(全体通して)
ただ、こういうフォルダ構造になることもある、ということは認識いただきたいです。
例えばクリーンアーキテクチャのような、ビジネスロジックとインフラ層の依存性を反転するタイプのアプリケーションアーキテクチャを採用していると、以下のようなフォルダ構造になります。
このとき、user-repository.tsをadaptersフォルダに入れてしまうと、レイヤ間の依存関係がごちゃごちゃになってしまいます。
筆者目線は自信があるので、その前提でコメントしますが(笑)、記載内容は多くの方が現場で実際に出くわすようなケースにフォーカスして書いています!
経験者の方向けに補足すると、逆に、DDDのドメインレイヤを自分でモデリングして実装するようなユースケースは全く想定していませんので、ご注意ください。
※経験者向けに書いていないということを冒頭記載していなかったため、そちらは記載しておきました。
本記事が想定していたような初学者です!
observerパターン実装の例を学ぶにあたりインタフェースを学習する必要が生まれたため、本記事を拝見させていただきました。
たしかに、他の方もおっしゃるように「所有」という言葉だけからは、本記事で伝えたいこととは直接関連しない印象を覚えます。例えば、それが何かしらの構成物(オブジェクト)の一部である、またはその階層構造が何かしらの子として配置されるといったものです。(所有を構成物の一部であると捉えても、
Person
インタフェースで定義した機能はDog
に実装はされてないので構成物の一部っぽさは無いですし、本文のフォルダの階層構造を以て所有を考えても、他の方がおっしゃるようにシステムを発展させると共依存に陥ってしまう)ただ実際に本記事を読み終えて、私としては、「インタフェースはそれを呼び出そうとする側の希望を、インタフェースを継承するクラスに反映させる仕組み」として理解いたしました!(初学者としては)継承する側にばかり目が行ってしまうインタフェースを、「誰の希望を反映させるものかを初学者に実感させるもの」として、本記事はとても素晴らしい効果を持つと思いました。
ディスカッションでも何でもありませんが、実際の初心者の感想として受け取っていただければ幸いです!