こんにちは、増田です。
今回は"依存性反転の原則"についてObjective-Cで解説します(Objective-Cは筆者が好きな言語です)。
私はこの原則を理解する前は依存関係がスパゲティ状態になったプログラムを書いていました。
プログラムの処理はそうめんのように道筋の立った分かりやすいコードを書いたとしても、依存関係がスパゲティであることがよくあります。そのようなコードを書くと、一箇所を変更するために既に動いているところをいじらなければならず、この際のテストにかかるコストはとてつもなく膨大になってしまいます。
モジュール間の依存は必ずしも悪になるというわけではないですが、「意図して依存を残す」という意識がないと後で痛い目を見ます。依存性を残すところ、断ち切るところを意図してプログラムを組むことができるようになれば、何かしらの変更を迫られた際、変更箇所だけのテストでシステム全体の正常な動作が保証されるようになります。これは大きなアドバンテージです。
前置きが長くなりましたが、本題に移りましょう。
さてこの原則ですが、英語では"Dependency Inversion Principle(DIP)"、日本語では"依存性反転の原則"とか、"依存関係逆転の原則"などと呼ばれています。
教科書的な説明は下記のようなものになります。
上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「インターフェース」に依存すべきである。
なんだかよくわからない説明ですね。ではまず「上位のモジュールは下位のモジュールに依存してはならない」について考えてみます。
なぜ、上位モジュールが下位モジュールに依存してはいけないのでしょう?
例えば、下記のようなCameraを表すクラスを実装したとします。(@implementationはひとまず置いておきます)
@interface Camera
@property (strong, nonatomic) GoodStorage storage;
@end
CameraクラスはGoodStorageクラスのインスタンスを保持しています。ここで、Cameraクラスの実装ではこのstrageに様々な処理を命令するためのメソッドがたくさんあると仮定しましょう。
(例えば、写真を保存する、写真を検索する、写真一覧を取得する、写真のプロパティーを変更する...などなどです)
ですが、後になって別の機能を持ったストレージが欲しくなった場合どうすればよいでしょうか?例えば、「既にあるGoodStorageクラスを新しく作ったGreatStorageクラスと入れ替えたい」という要求に答えるためには、既に存在しているCameraクラスは以下のように変更しなくてはならなくなります。
@interface Camera
@property (strong, nonatomic) GreatStorage greatStorage;
@end
この変更がもたらすCameraクラスへの影響はこの程度のクラスでしたら、大したことは無いでしょう。
しかし、〜ここは大いに想像力を働かせていただきたいのですが〜 もしCameraクラスが他にもいろいろなメソッドでstorageプロパティを参照し、strageの様々なメソッドを呼び出したりしていたらどうでしょう?例えば、写真をstorageから取得したりするメソッドや、storageに特定の写真があるかどうか問い合わせをするメソッド、などです。
そして、もしそれらの中にGreatStorageとGoodStorageクラスで異なった呼び出し方(引数の数や型とか、メソッド名の違いとか)があったらどうしましょう?
- Cameraクラスではそれらの箇所をGreatStorage仕様に変更しなければならなくなります(既に正常に動いているコードを一度壊すことになります!)
- テストのやり直しが必要になります(コスト大幅アップ!)
- さらに良くないことにGreatStorageを別のStorageモジュールと取り替えたくなったら、また同じ問題が起きます(まさに外道)
この問題は@property (strong, nonatomic) GoodStorage storage;という文でGoodStorageクラスをCameraクラスにハードコード(ベタ書き)したことで起きています。
ベタ書きしたことで、Cameraクラス(上位モジュール)がある特定のGoodStorageクラス(下位モジュール)にどっぷり依存してしまっているのが問題なのです。
このように、あとになって別のモジュールと取り替えたくなりそうである可能性が高そうな箇所は「交換可能」にした方が良いでしょう。
(ここの見極めは設計者の腕の見せ所です)
やり方としては次のようにします。
@protocol Storage
- (void)savePhoto:(Photo *)photo;
- (NSArray *)findAllPhotos;
- (Photo *)findPhotoById:(NSInteger)id;
//...などなど
@end
@interface Camera
@property (strong, nonatomic) id <Storage> storage;
@end
この様に@protocolでStrageインターフェース(型)を定義し、Cameraクラスにはその型に準拠したクラスをなんでも入れられるようにしてしまいます。
さらに、Cameraクラスがstorageのメソッドを呼び出す際には@protocolで定義されたメソッドからしか呼び出しを行えなくなります(逆に言うと、メソッドの名前、返り値の型、引数の数と型などは完全に規定付けられています)。
よって、Cameraクラスのstorageにどんなクラスが入ろうとも、Cameraクラスはそのクラスがどのようなメソッドを実装しているのかは全く知る必要はなく、@protocol(インターフェース)だけ知っていれば良いことになります。
この設計ではGoodStorageからGreatStrageへのモジュールの変更はコストになりません。なぜならば、Cameraクラスで使われているメソッド名、引数の数や型など(シグネチャと言います)は全て
@property (strong, nonatomic) id <Storage> storage;
という一文により保証されているからです。
これにより、Cameraクラスは<Storage>インターフェースに依存するようになります。
また、GoodStorageクラスやGreatStrageクラスは<Strage>インターフェースの定義に対して実装することになります。
今までは
Camera → GoodStorage, GreatStorage
という依存関係だったのに対し、今では
Camera → < Storage > ← GoodStorage, GreatStrage
というように、上位モジュール(Camera)も下位モジュール(GoodStrage, GreatStorage)もインターフェース(<Storage>)に依存していることがわかります。
依存性がくるっと反転している様から、この原則は"依存性反転の原則"と呼ばれているというわけです。
ここまでくれば、最初に言及した命題が何を意味しているのかがわかっていただけたかと思います。
この原則はSOLIDと呼ばれているソフトウェアデザインの原則の中でも最も重要なものであると私は考えています(SOLIDのDです)。
GoFの23デザインパターンもこの原則の応用であるケースが多いです。
このような設計方法はハードウェアの世界でも見ることができます。USBはまさにそうで、PCはどんなマウスやキーボードがUSBに差し込まれているのかは知りません。
PCが唯一知っているのはそれらのハードウェアがUSBの「インターフェースに準拠している」ということだけで、PCはそのルールを前提に自身の実装をしています。
逆にマウスやキーボードも自分がどのPCに差し込まれているのかわかりません。
彼らも決められたインターフェース(ルールといっても良いかもしれません)に従って自身の実装をしているだけなのです。
また、この原則を理解すれば他人に使ってもらえるコードを書くことができるようになります。利用者がカスタマイズしたくなりそうな部分にはインターフェースを提供して、そのインターフェースの範囲内で利用者に自由に実装を組ませてあげれば良いのです。
以上で依存性反転の原則の解説は終わりです。ソフトウェアのデザインって奥が深くて本当に面白いですね!
それではまた会いましょう!さようなら!