CleanArchitecture憧れ系なんだけど、凄く重要な要素なのに、Rubyエンジニアだから実感がわかない「インターフェース」について、考え方の整理もかねて、自分なりの解釈を纏めなおしてみた。
インターフェースの役割の復習
依存関係の整理による保守性の確保
「変更の可能性が高いコードをインターフェースの向こう側に閉じ込めてしまう」事で、保守が容易になる。
無インターフェース例
Aクラスは、Bクラスを使っている。
ある時、Bクラスの実装が大幅に変更された。(これをB'クラスとする)
この時、Aクラスのコードは、どうなるか?
もしかすると大幅な変更が必要かもしれないし、もしかすると何も変えずに済むかもしれない。
それを知るためには、AはBにどうメッセージを投げていて、Bはどうそれを受けていて、B'ではどう変更されているのか等、関連するクラスの全てを調べ直す必要がある。
つまりAは、Bの存在自体に、大きな影響を受けていると言える。
この様に、AがBを前提に書かれている場合、「AはBに依存している」という。
コードの再調査は、大変な作業だ。
クラスが2つなら良いが、大規模なシステムになると、依存関係は急激に複雑になる。
Bに依存しているクラスはAだけではなく、5個10個と出て来るかもしれないし(Anとする)、Anの変更も必要であれば、Anに依存しているクラスもまた洗い出し、全て再調査する必要がある。
とてもやっていられない。
なので、この依存関係は、システムが巨大化していく段階のどこかで、あらかじめ断ち切っておく必要がある。
その為に使う武器の一つが「インターフェース」になる。
インターフェース例
AクラスとBクラスを「インターフェース」を用いた実装で考えると、例えば以下の様になる。
- Aクラスは、Iインターフェースを使用している。
- Iインターフェースの実態は、Iインターフェースの仕様通りに実装された、Bクラスである。
ある時、Bクラスの実装が大幅に変更された。(これをB'クラスとする)
この時、Aクラスのコードはどうなるか?
何も変えないで済む。
何故なら、BクラスがB'クラスになっても、Iインターフェース自体は何も変更されていないからだ。
B'クラスはIインターフェースの仕様を忠実に守りつづけなければ、Iインターフェースの実態となる事が出来ない。
AはIを使うため、Iに依存している。さらは、BもIの仕様を守るため、Iに依存している。
処理自体ではAがBを使用しているのだが、「依存は、AもBも、Iにのみしている」。
BではなくIに依存しているため、AはBの変更の影響を受けない。
影響を受けないから、BがB'になっても、AやAnの再調査は必要が無い。
当然、それらに依存している無数のクラスの再調査も必要が無い。
とても修正が簡単になる。
Bを修正する時は、Bの事だけを考えていれば良い。
まとめ
-
「変更の可能性が高いコードをインターフェースの向こう側に閉じ込めてしまう」事で、コードが保守しやすくなる。
- これを実現するために、依存をインターフェースに集める様に実装しておく
依存関係の整理による拡張性の確保
「拡張の可能性が高いコードをインターフェースの向こう側に置いておく」事で、保守が容易になる
無インターフェース例
AクラスがBクラスを使用している。
ある時、新しくC機能が必要になった。
C機能はBクラス(この機能をB機能とする)と良く似た性質を持っており、B機能とは排他的に実行される。
この時、C機能はどの様に実装されるか?
どの様にでも書く事が出来る。
Aクラスに手続き的に追加されるかもしれないし、Cクラスを追加してAクラスから呼び出すのかもしれない。
もしくは、Bクラスの中でB機能とC機能の実行分岐が追加されるのかもしれない。
どういう形でも実現が可能だが、どの場合でも、それぞれのクラスに、大幅な修正や構成変更が必要になる。そして、依存関係は断ち切れていないので、その影響範囲は無限に広い。
これがC機能だけなら良いが、D機能、E機能と拡張機能が追加される様な事があると、修正箇所が爆発的に増え、手に負えなくなる。
機能の拡張は、簡単に行える様にしておく必要がある。
そのための武器の一つとして、ここでも「インターフェース」を用いる事が出来る。
インターフェース例
AクラスとBクラスを「インターフェース」で実装すると考えると、例えば以下の様になる。
- Aクラスは、Iインターフェースを引数から取得している。
- Aクラスは、Iインターフェースの関数を実行している。
- Iインターフェースの実態は、Iインターフェースの仕様通りに実装された、Bクラスである。
ある時、新しくCという機能が必要になった。
C機能はBクラス(この機能をB機能とする)と良く似た性質を持っており、B機能とは排他的に実行される。
この時、C機能はどの様に実装されるか?
どの様にでも書く事が出来る。
が、簡単に解決する方法がある。
- Cクラスを、Iインターフェースの仕様通りに実装する。
- A機能に引数として渡すIインターフェースの実態を、BもしくはCクラスとする。
この方法であれば、AクラスもBクラスも、何も修正をする必要が無い
Bとはそもそもが別クラスだし、AはあくまでIインターフェースを使用しているのだから、その実態がBでもCでも関係が無い。
どういう形でも実現が可能だが、この実装をした場合に、影響範囲を最低限に抑える事が出来る。
例えD機能、E機能と拡張機能が追加される事があっても、D/Eそれぞれについて、クラスとIインターフェース作成時の分岐を追加するだけで、実装を終える事が出来る。
とても拡張が簡単になる。
機能拡張に必要な作業は、以下の2つとして纏められる
- 新機能クラスを追加
- インターフェース作成時に、新機能クラス用の分岐を追加
- インターフェースの作成場所は、それ専用のクラスとする
まとめ
-
「拡張の可能性が高いコードをインターフェースの向こう側に置いておく」事で、拡張が容易になる
- これを実現するためには、クラスをインターフェース経由で呼び出すように実装しておく
※注
(インターフェースではなく、facadeパターンを使う手もあった。この構成であればそれだけで良いのかも)
(インターフェースは引数ではなく、factoryクラスで実態クラスを分岐させる方が綺麗な気もする)
インターフェースまとめ
インターフェースの目的は以下の2つ
- 「変更の可能性が高いコードをインターフェースの向こう側に閉じ込めてしまう」事で、保守を容易にする。
- 「拡張の可能性が高いコードをインターフェースの向こう側に置いておく」事で、拡張を容易にする。
そのために必要な実装方針は、以下の2つ
- 変更の可能性が高いコードは、直接使用するのではなくインターフェース経由で呼び出す。
- 拡張の可能性が高いコードは、直接使用するのではなくインターフェース経由で呼び出す。