Railsで学ぶSOLID(5)依存関係逆転の原則(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Railsで学ぶSOLID(5)依存関係逆転の原則

「SOLIDの原則シリーズ」へようこそ。このシリーズ記事では、SOLIDの原則をひとつずつ詳しく説明し、分析します。シリーズの最後にはいくつかのヒントや考察を含む総括記事をお送りしますのでどうぞご期待ください。

それでは始めましょう。「SOLIDの原則」とはそもそも何なのでしょうか?SOLIDとは、オブジェクト指向プログラミング設計における一般的な原則であり、ソフトウェアをより理解しやすくし、拡張性やメンテナンス性やテストのしやすさを向上させることを目的としています。

  1. 単一責任の原則SRP: Single responsibility principle)
  2. オープン/クローズの原則OCP: Open/closed principle)
  3. リスコフの置換原則LSP: Liskov Substitution Principle)
  4. インターフェイス分離の原則ISP: Interface Segregation Principle)(原文
  5. 依存関係逆転の原則DIP: Dependency Inversion Principle)(本記事)

今回は、5番目の「依存関係逆転の原則」を見ていきましょう。

依存関係逆転の原則(DIP)

この原則は2文からなります。

  • 高レベルのモジュールは低レベルのモジュールに依存すべきではない。両者とも抽象に依存すべきである。

  • 抽象は(実装の)詳細に依存すべきではない。逆に詳細は抽象に依存すべきである。

この原則については、素直に例で考えるのがベストだと思います。

以下のコードは、DIP違反の例です(Gist[solid-5-violation.rb))。

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

全シリーズのまとめ

これまでのシリーズのどこかで申し上げたように、これらの原則(に限らずあらゆるパターン)は、そこにメリットがあるから適用するのであり、「プロならそうする」とばかりに単純に適用するものではありません。目に見えるはっきりしたメリットがなければ、時間の無駄でしかありません。原則を満たすだけのためにルールなりパターンなりを適用すると、ほとんどの場合さまざまな問題を誘発し、コードベースが手に負えないほど複雑になるという無残な結果に終わります。これらの原則は皆さんを手助けする(コードを改善する)ために書かれたものであることを肝に銘じ、必要以上にコードを複雑にしないよう心がけましょう。銀の弾丸はないのです。とにかく、あらゆる作業において常識を働かせましょう。これらの原則の本来の意図を常に思い出し、よいガイダンスとして扱うことです。

最後に率直に申し上げておきたいことがあります。さまざまな規則、原則、パターンをすべて読みとおすだけでその日から完璧なコードが書ける、ということはありえません。コードの改善は終わりのないプロセスであり、必要なのはコードを書いて書いて書きまくることです。

関連記事

Railsで学ぶSOLID(1): 単一責任の原則(翻訳)

Railsで学ぶSOLID(2)オープン/クローズの原則(翻訳)

Railsで学ぶSOLID(3)リスコフの置換原則(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ