SOLID原則の依存性逆転の原則(DIP)についてRubyで解説
プログラミング #ruby #SOLID #オブジェクト指向 #クリーンコードオブジェクト指向プログラミングにおいて、コードの保守性、可読性、再利用性を高めるために、SOLID原則と呼ばれる5つの重要な設計原則があります。
今回は、このSOLID原則のうち、依存性逆転の原則(Dependency Inversion Principle, DIP)についてRubyのサンプルコードを用いて解説していきます。
目次 / Index
SOLIDとは
SOLIDとは、5つの原則の頭文字を取った略語です。
- Single Responsibility Principle(SRP): 単一責任原則
- Open Closed Principle(OCP): オープン・クローズドの原則
- Liskov Substitution Principle(LSP): リスコフの置換原則
- Interface Separation Principle(ISP): インターフェース分離の原則
- Dependency Inversion Principle(DIP): 依存性逆転の原則
依存性逆転の原則 (Dependency Inversion Principle, DIP)とは
依存性逆転の原則は、以下2つの規則で構成されています。
- 上位のモジュールは、下位のモジュールに依存してはならない。両者とも抽象に依存すべきである。
- 抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである。
小難しくて理解しにくいですが、ざっくり言うと、元のクラス(上位モジュール)から他クラス(下位モジュール)を利用したい場合、他クラスを直接コールするのではなく、インターフェースや抽象クラスなどの抽象を介してコールすべきということです。
以降、実際のコードで解説していきます。
依存性逆転の原則に違反しているコード
以下のMySQLDatabase
とUserRepository
クラスを例にします。
# 低レベルのモジュール
class MySQLDatabase
def initialize
# ...
end
def query(sql)
# ...
end
end
# 高レベルのモジュール
class UserRepository
def initialize
@db = MySQLDatabase.new
end
def find_user(user_id)
sql = "SELECT * FROM users WHERE id = #{user_id}"
@db.query(sql)
end
end
この例では、高レベルのモジュールUserRepository
が、低レベルのモジュールMySQLDatabase
を直接初期化しており、具体的なクラスに依存しています。
これでは、別のDBを使用したくなった場合やメソッド名変更などの変更がMySQLDatabase
に生じた場合に、UserRepository
も変更を強いられます。
また、UserRepository
の単体テストを作成する時、MySQLDatabase
の初期化を強制されるため、MySQLDatabase
の処理によっては、必要なデータの用意や条件分岐のハンドリングなどが必要となり、単体テストがしにくいです。
依存性逆転の原則に則ったコード
以下が則ったコードとなります。
class Database
def query(sql)
raise NotImplementedError
end
end
class MySQLDatabase < Database
def initialize
# ...
end
def query(sql)
# ...
end
end
class UserRepository
def initialize(database)
@db = database
end
def find_user(user_id)
sql = "SELECT * FROM users WHERE id = #{user_id}"
@db.query(sql)
end
end
UserRepository
クラスでMySQLDatabase
を直接初期化せず、コンストラクタでDatabase
という抽象クラスを受け取る(依存性注入)ようにしています。(Rubyは言語的にインターフェースがないため、疑似抽象クラスによるダックタイピングでの実装としている。)
これにより、下記の図のようにMySQLDatabase
に対する依存の矢印が逆転していることがわかります。
これが依存性逆転の原則の名前の意味となります。
依存性逆転の原則のメリット
主に以下のメリットがあります。
- 変更容易性の向上
- 下位モジュールを変更する際、抽象を介しているため実装の詳細が隠蔽されており、変更に伴う修正範囲が限定され、バグの発生リスクが低減する。
- テスタビリティの向上
- 抽象に依存しているため、抽象を満たすモックオブジェクトを作成し、コンストラクタ経由でセットすることが可能。
- これにより、テスト対象でないクラス用にテストデータの用意や条件分岐などの考慮をする必要がなくなり、テストがしやすくなる。
- 抽象に依存しているため、抽象を満たすモックオブジェクトを作成し、コンストラクタ経由でセットすることが可能。
- 拡張性の向上
- 新しい下位モジュールを追加する際、同じ用に抽象を満たすように実装するだけで済み、上位モジュールへの影響を最小限に抑えられる。
- 再利用性の向上
- 抽象を満たす複数の下位モジュールを容易に切り替えて使用できる。
最後に
これにて、SOLID原則の全ての解説が終了となります。
気をつけたいことですが、これはあくまでも原則であり、必ず守らなければいけないという訳ではない点です。
今回ご紹介したDIPも、すべての箇所で適用しようとすると、逆にメンテナンスがしにくくなる場面もあったりします。
大事なのはそれぞれの原則の特徴を知り、今の実装に当てはめるべきかを正しく判断出来るようにすることだと思います。
この部分は経験則が物を言ったりするので、日々この設計は将来どういう問題を起こすだろうか?と自問しながら試行錯誤していくのが良いと思います。