依存性逆転の原則(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
メソッドは引数list
がeach
メソッドを実装していること、つまり、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
インターフェースを実装する全てのクラスにとって妥当と認められる場合にしか追加してはならない。
まとめ
依存性逆転の原則によって、上位モジュールが下位モジュールに依存してしまう状況を回避できる。
抽象は、下位モジュール全体に対する規格やプロトコルのようなもの。一部の下位モジュールの都合で変更したり、安易に変更するようなことは避けなければならない。