依存性逆転の原則(Dependency Inversion Principle)

a. 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである。

b. 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。

アジャイルソフトウェア開発の奥義 第2版 p.163

簡単にいえば「抽象」というのは規格やプロトコルのようなもので、上位のモジュールにとっては「このように使える」、下位のモジュールにとっては「このように使えるようにする」という取り決めである。

以下のようなメソッドを考える。

def sum1(array)
  i = 0
  sum = 0
  while i < array.length
    sum += array[i]
    i += 1
  end
  sum
end

このsum1メソッドは引数arrayが配列であることを前提に記述されている。Rubyらしいコードにするなら以下のようになる。

# list.inject(:+)の1行で済むが対比のため冗長に記述している
def sum2(list)
  sum = 0
  list.each do |x|
    sum += x
  end
  sum
end

sum2メソッドは引数listeachメソッドを実装していること、つまり、1つ1つ要素を取得するという操作ができることを前提に記述されている。これにより、具体的にどのように要素を取得するか(実装の詳細)は頓着せずに済ませられ、1つ1つ要素を取得するという操作(抽象)にのみ依存している。

この「1つ1つ要素を取得するという操作(抽象)」はダックタイピングな言語であるRubyではコード上明示されないが、静的型付言語ではインターフェースという形で以下のように指定される。

# Crystalの例
def sum2(list : Enumerable)
  # ...
end
// Javaの例
Integer sum(Iterable<Integer> list) {
  // ...
}

ここからは、アーキテクチャレベルの例として、以下のようなデータベースからユーザーを検索する例を考える。

def foo(user_id)
  # 疑似的なコード
  user_data = DB.connection.sql('select * from users where id = ?', user_id)
  # user_dataを利用したコード
end

この場合、fooメソッドが上位モジュールであり、データベースが下位モジュールになる。

fooメソッドはSQLを発行しているため、RDBのデータベースに依存してしまっている。もし、このfooメソッドがライブラリに含まれるメソッドでRDBの他にNoSQLやCSVをデータソースとしてサポートしなければならないのだとしたら、このコードでは問題がある。あるいは、現状はRDBにデータがあるがマイクロサービス化してREST APIを叩く形に変えようと考えているような場合もやはり問題がある。

SQLは実装の詳細であり、このコードで行いたいことは「IDを指定してユーザー情報を取得する」という操作である。したがって、以下のようなコードにすればよい。

def foo(user_id)
  user_data = RdbUserFinder.find(user_id)
  # ...
end

findメソッドにSQLなどの具体的な検索処理をカプセル化した。もちろん、RdbUserFinderクラスに依存しているのでデータソースがRDBであるという実装の詳細を隠しきれていない。なので、さらに以下のようにする。

# user_finderはコンストラクタで受け取ってインスタンス変数に格納しておいても良い
def foo(user_id, user_finder)
  user_data = user_finder.find(user_id)
  # ...
end

こうすることで、fooメソッドはRDBへの依存がなくなった。下位モジュールに依存せず「IDを指定してユーザー情報を取得する操作(findメソッド)」(抽象)に依存しているため、上位モジュールであるfooメソッドを変更することなく下位モジュールを交換可能となった。

下位モジュールからすると、「IDを指定してユーザー情報を取得する操作(findメソッド)」(抽象)に依存しさえすれば、上位モジュールであるfooメソッドを修正せずに交換可能な下位モジュールを追加することができるようになった。

「IDを指定してユーザー情報を取得する操作(findメソッド)」(抽象)をJavaで表現するなら以下のようになる。

interface DataFinder {
  public Row find(Integer id);
  // その他の検索メソッド
}

interface UserFinder extends DataFinder {
}

class Klazz {
  public void foo(Integer userId, UserFinder userFinder) {
    Row userData = userFinder.find(userId);
    // ...
  }
}

abstract class AbstractRdbDataFinder implements DataFinder {
  public Row find(Integer id) {
    // ...
  }

  // 検索対象のテーブルを特定するメソッド
  protected String targetTable();
}

class RdbUserFinder implements UserFinder {
  protected String targetTable() {
    // ...
  }
}

CsvUserFinderクラスやMongoDbUserFinderクラスを作ったとしても、AbstractRdbDataFinderクラスやRdbUserFinderクラスと同じように実装してさえいれば、fooメソッドを変更することなく引数に指定することができる。

しかし、AbstractRdbDataFinderクラスの都合でDataFinderインターフェースにメソッドを追加するようなことはできない。他のDataFinderインターフェースを実装する全てのクラスに影響を与えてしまうためだ。DataFinderインターフェースを実装する全てのクラスにとって妥当と認められる場合にしか追加してはならない。

まとめ

依存性逆転の原則によって、上位モジュールが下位モジュールに依存してしまう状況を回避できる。

抽象は、下位モジュール全体に対する規格やプロトコルのようなもの。一部の下位モジュールの都合で変更したり、安易に変更するようなことは避けなければならない。