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
の知識を持たせずに実装できる分メリットがあるように思います。とはいえ、何でもかんでも抽象にして引数で渡した方がいいとも言えないので、ケースバイケースだとは思いますが、、、