概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: SOLID Principles #5 - Dependency Inversion Principle | Netguru Blog on Ruby/Ruby on Rails
- 原文公開日: 2018/04/26
- 著者: Marcin Jakubowski
Railsで学ぶSOLID(5)依存関係逆転の原則
「SOLIDの原則シリーズ」へようこそ。このシリーズ記事では、SOLIDの原則をひとつずつ詳しく説明し、分析します。シリーズの最後にはいくつかのヒントや考察を含む総括記事をお送りしますのでどうぞご期待ください。
それでは始めましょう。「SOLIDの原則」とはそもそも何なのでしょうか?SOLIDとは、オブジェクト指向プログラミング設計における一般的な原則であり、ソフトウェアをより理解しやすくし、拡張性やメンテナンス性やテストのしやすさを向上させることを目的としています。
- 単一責任の原則(SRP: Single responsibility principle)
- オープン/クローズの原則(OCP: Open/closed principle)
- リスコフの置換原則(LSP: Liskov Substitution Principle)
- インターフェイス分離の原則(ISP: Interface Segregation Principle)(原文)
- 依存関係逆転の原則(DIP: Dependency Inversion Principle)(本記事)
今回は、5番目の「依存関係逆転の原則」を見ていきましょう。
依存関係逆転の原則(DIP)
この原則は2文からなります。
- 高レベルのモジュールは低レベルのモジュールに依存すべきではない。両者とも抽象に依存すべきである。
-
抽象は(実装の)詳細に依存すべきではない。逆に詳細は抽象に依存すべきである。
この原則については、素直に例で考えるのがベストだと思います。
以下のコードは、DIP違反の例です(Gist)。
class ReportGeneratorManager
def initialize(data)
@data = data
end
def call
generate_xml_report
additional_actions
end
private
attr_reader :data
def generate_xml_report
XmlRaportGenerator.new(data).generate
end
def additional_actions
...
end
end
このコードのどこがまずいのでしょうか?まず、ReportGenaratorManage
という高レベルのクラスと、XmlReportGenerator
という低レベルのクラスが癒着しています。次に、別の種類のレポートジェネレータを追加する必要が生じた場合に、高レベルのクラスの修正も要求されます。つまり、低レベルのクラスが変更されることで高レベルのクラスまで修正を余儀なくされるということです。
ここでは、依存関係を逆転させるのが正解です。詳細は、個別の実装にではなく抽象に依存させます。Rubyは動的型付け言語なので、ダックタイピングの手法を使えます。Ruby世界には「抽象クラス」も「抽象インターフェイス」もないので、そうしたものを作成する必要はありません。
この他にDI(Dependency Injection)パターンを使って実現する方法もありますが、その場合1つ注意しなければならない次の点があります。
依存関係逆転の法則 ≠ DI
DIは、単に依存関係逆転の原則を満たすのに使えるテクニックであり、原則そのものではありませんに過ぎません(Gist)。
class ReportGeneratorManager
def initialize(data, generator = XmlRaportGenerator)
@data = data
@generator = generator
end
def call
generate_report
additional_actions
end
private
attr_reader :data, :generator
def generate_report
generator.new(data).generate
end
def additional_actions
...
end
end
上のコードでは、特定のジェネレータクラスをこのマネージャにコンストラクタ経由で注入できるようになりました(かつデフォルトのジェネレータも提供しています)。これで、この高レベルクラスの操作は、具体的なジェネレータクラスすべてに共通する一般的なインターフェイスでのみ行われるようになります。実装クラスの差し替えは簡単です(ReportGeneratorManager
クラスを別の場所にある異なる実装で使うなど)。
上述のソリューションは柔軟性が遥かに高まり、テストもずっと容易になります。
単体テスト
上述のソリューションでの単体テストは、簡単かつ快適です。既に依存をコンストラクタで注入するようになっているので、doubleへの依存を作成して実物の代わりに注入するのも簡単です(Gist)。次のようなことをする必要はありません。
any_instance_of(ClassToMock) ...
# または
ClassToMock.new.stub(:method_to_stub)
この方法は一部で「ダメなテスト」と呼ばれています。
これは、まさに単体テストの主要な目的です。つまり、一切の依存性を考慮する必要のない、独立した単独のユニットをテストすることです。上のソリューションを使うことで、この目的を容易に達成できます。
ここからさらに一歩進めると、クラスの依存性をモック化しづらい場合、融通の利かない依存性がそこに頑固にこびりついていると言えます。これに限らず、テストを書くことで「コードの臭い」を見つけたり、何かが複雑になりすぎていることがわかるようになります。
概念を取り違えないこと
上述のとおり、「依存関係逆転の原則(DIP)」と「DI(Dependency Injection)」を混ぜこぜにしないようにすべきです。しかし、前述の2つと紛らわしい用語がもうひとつあります。それは「制御の反転(IoC: Inversion of Control)」と呼ばれているものです。IoCは本記事の本来の目的ではないので、Martin FowlerのDIP in the Wildという良記事のリンクをご紹介するにとどめます。その要点が1行にまとまったものを以下に引用します。
DIは「接続方法」であり、IoCは「指針」であり、DIPは「かたち」である
Martin Fowler
全シリーズのまとめ
これまでのシリーズのどこかで申し上げたように、これらの原則(に限らずあらゆるパターン)は、そこにメリットがあるから適用するのであり、「プロならそうする」とばかりに単純に適用するものではありません。目に見えるはっきりしたメリットがなければ、時間の無駄でしかありません。原則を満たすだけのためにルールなりパターンなりを適用すると、ほとんどの場合さまざまな問題を誘発し、コードベースが手に負えないほど複雑になるという無残な結果に終わります。これらの原則は皆さんを手助けする(コードを改善する)ために書かれたものであることを肝に銘じ、必要以上にコードを複雑にしないよう心がけましょう。銀の弾丸はないのです。とにかく、あらゆる作業において常識を働かせましょう。これらの原則の本来の意図を常に思い出し、よいガイダンスとして扱うことです。
最後に率直に申し上げておきたいことがあります。さまざまな規則、原則、パターンをすべて読みとおすだけでその日から完璧なコードが書ける、ということはありえません。コードの改善は終わりのないプロセスであり、必要なのはコードを書いて書いて書きまくることです。