Rubyでさらっと学ぶSOLID原則⑤「依存性逆転の原則」
SOLID原則とは
ソフトウェアの拡張性や保守性を高めるための下記5つの原則のこと。
- S(Single-responsibility principle): 単一責任の原則
- O(Open-closed principle): オープン・クローズドの原則
- L(Liskov substitution principle): リスコフの置換原則
- I(Interface segregation principle): インターフェース分離の原則
- D(Dependency inversion principle): 依存性逆転の原則
依存性逆転の原則(DIP)とは
- 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも抽象に依存すべきである
- 抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである
という原則。
これだけでは何を言っているのかさっぱり分からないと思うので、用語を整理したあと具体例で解説します。
用語
- モジュール
- 主に「クラス」のこと
- 上位のモジュール
- 下位モジュールを使う側
- 下位のモジュール
- 上位モジュールに使われる側
- 抽象
- 静的型付け言語でよく出てくる「インターフェース」のこと
- インターフェース
- 実装の中身を持たず、メソッド名や引数の型、戻り値の型を定義したもの
依存性逆転の原則に違反している例
以下3つのクラスがあるとします。
-
NotificationManager: 通知したあとの共通処理をさせるために用意したクラス-
SlackNotifier: Slackへの通知を行うクラス -
MailNotifier: メールでの通知を行うクラス
-
class NotificationManager
def initialize(text)
@text = text
end
def notify_to_slack
SlackNotifier.new(@text).notify
puts 'ここで通知後の共通処理をしたい'
end
def notify_by_mail
MailNotifier.new(@text).notify
puts 'ここで通知後の共通処理をしたい'
end
end
class SlackNotifier
def initialize(text)
@text = text
end
def notify
# Slackに通知する処理
end
end
class MailNotifier
def initialize(text)
@text = text
end
def notify
# メールで通知する処理
end
end
# Slackに通知したいとき
NotificationManager.new('おはよう').notify_to_slack
# メールで通知したいとき
NotificationManager.new('おはよう').notify_by_mail
依存性逆転の原則の用語に当てはめると、以下のとおりです。
- 上位モジュール:
NotificationManager - 下位モジュール:
SlackNotifier,MailNotifier
この例のNotificationManagerはたくさんのことを知りすぎています。例えば以下に挙げることです。
-
SlackNotifierやMailNotifierという名前のクラスが存在する -
SlackNotifierやMailNotifierはtextを属性として持つ -
SlackNotifierやMailNotifierはnotifyというメソッドを持つ
これはNotificationManagerという上位モジュールがSlackNotifierやMailNotifierという下位モジュールに依存していることを表しています。つまり、依存性逆転の原則に違反しています。
この原則に違反するデメリットとして、例えばLineNotifierというクラスを新しく作ることを考えると、新しくLineNotifierを作るだけでなくNotificationManagerにnotify_to_lineメソッドも用意しなければならないというデメリットがあります。
解決策
NotificationManagerの中でSlackNotifierやMailNotifierを呼び出すのではなく、notifierという抽象(インターフェース)を用意することで解消できます。
class NotificationManager
# `notify`メソッドに応答できる`notifier`を渡すようにする
def initialize(notifier)
@notifier = notifier
end
def notify
notifier.notify
puts 'ここで通知後の共通処理をしたい'
end
end
# Slackに通知したいとき
slack_notifier = SlackNotifier.new('おはよう')
NotificationManager.new(slack_notifier).notify
# メールで通知したいとき
mail_notifier = MailNotifier.new('おはよう')
NotificationManager.new(mail_notifier).notify
依存性逆転の原則の用語に当てはめると、以下のとおりです。
- 上位モジュール:
NotificationManager - 抽象:
notifier
NotificationManagerはSlackNotifierやMailNotifierというクラスが存在することを知らなくてよくなりました。notifyというメソッドに応答できるnotifierという抽象(インターフェース)を渡せば、それが何のクラスであっても問題ありません。
LineNotifierを用意しなければいけなくなった場合もLineNotifierクラスとnotifyメソッドを新しく作るだけで対応できます(NotificationManagerに変更を加える必要がなくなります)。
依存性の注入(Dependency Injection)との関係
用語が似ているので混合しがちですが、「依存性逆転の原則(DIP)」と「依存性の注入(DI)」は異なる概念です。
依存性の注入(DI)とは、解決策の例で用いた「notifierをNotificationManagerのinitialize時に渡す」というテクニックのこと(つまり依存するオブジェクトを外部から注入すること)を指します。
関係性を文章で表すと「依存性の注入(DI)を使うことで依存性逆転の原則(DIP)違反を解消した」と表現できます。
Discussion
記事ありがとうございます。
質問なのですが、
この解決策だと代わりに
NotificationManagerを呼び出す側で必要な知識が増えていませんか?これまでは、以下で簡単にslack通知できていました。
(細かいことは内部でやってくれていた)
リファクタ後は、
SlackNotifierのインスタンスを用意しないといけなくなりました。前者のほうが細かいことがカプセル化されていただけ良いのではと思ってしまいます。
ご質問ありがとうございます。
たしかに呼び出し側が2行になっているので手間が増えたようにも見えますが、呼び出し時の違いは
notify_to_slackを呼び出すslack_notifierを引数で渡すなのに対して、今後の拡張性という観点で考えると(
LineNotifierを追加することを想定した場合を考えると)LineNotifierクラスの追加と、NotificationManagerにnotify_to_lineメソッドの追加LineNotifierクラスの追加(NotificationManagerは変更を加えなくて済む)となり、後者の方が
NotificationManagerにLineNotifierの知識を持たせずに実装できる分メリットがあるように思います。とはいえ、何でもかんでも抽象にして引数で渡した方がいいとも言えないので、ケースバイケースだとは思いますが、、、