🍍

Rubyでさらっと学ぶSOLID原則⑤「依存性逆転の原則」

2022/04/23に公開2

SOLID原則とは

ソフトウェアの拡張性や保守性を高めるための下記5つの原則のこと。

依存性逆転の原則(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はたくさんのことを知りすぎています。例えば以下に挙げることです。

  • SlackNotifierMailNotifierという名前のクラスが存在する
  • SlackNotifierMailNotifiertextを属性として持つ
  • SlackNotifierMailNotifiernotifyというメソッドを持つ

これはNotificationManagerという上位モジュールがSlackNotifierMailNotifierという下位モジュールに依存していることを表しています。つまり、依存性逆転の原則に違反しています。

この原則に違反するデメリットとして、例えばLineNotifierというクラスを新しく作ることを考えると、新しくLineNotifierを作るだけでなくNotificationManagernotify_to_lineメソッドも用意しなければならないというデメリットがあります。

解決策

NotificationManagerの中でSlackNotifierMailNotifierを呼び出すのではなく、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

NotificationManagerSlackNotifierMailNotifierというクラスが存在することを知らなくてよくなりました。notifyというメソッドに応答できるnotifierという抽象(インターフェース)を渡せば、それが何のクラスであっても問題ありません。

LineNotifierを用意しなければいけなくなった場合もLineNotifierクラスとnotifyメソッドを新しく作るだけで対応できます(NotificationManagerに変更を加える必要がなくなります)。

依存性の注入(Dependency Injection)との関係

用語が似ているので混合しがちですが、「依存性逆転の原則(DIP)」と「依存性の注入(DI)」は異なる概念です。

依存性の注入(DI)とは、解決策の例で用いた「notifierNotificationManagerのinitialize時に渡す」というテクニックのこと(つまり依存するオブジェクトを外部から注入すること)を指します。

関係性を文章で表すと「依存性の注入(DI)を使うことで依存性逆転の原則(DIP)違反を解消した」と表現できます。

参考文献

Discussion

at_sushiat_sushi

記事ありがとうございます。

質問なのですが、
この解決策だと代わりにNotificationManagerを呼び出す側で必要な知識が増えていませんか?

これまでは、以下で簡単にslack通知できていました。
(細かいことは内部でやってくれていた)

NotificationManager.new('おはよう').notify_to_slack

リファクタ後は、SlackNotifierのインスタンスを用意しないといけなくなりました。

slack_notifier = SlackNotifier.new('おはよう')
NotificationManager.new(slack_notifier).notify

前者のほうが細かいことがカプセル化されていただけ良いのではと思ってしまいます。

TKDTKD

ご質問ありがとうございます。

たしかに呼び出し側が2行になっているので手間が増えたようにも見えますが、呼び出し時の違いは

  • 前者:notify_to_slackを呼び出す
  • 後者:slack_notifierを引数で渡す

なのに対して、今後の拡張性という観点で考えると(LineNotifierを追加することを想定した場合を考えると)

  • 前者:LineNotifierクラスの追加と、NotificationManagernotify_to_lineメソッドの追加
  • 後者:LineNotifierクラスの追加(NotificationManagerは変更を加えなくて済む)

となり、後者の方がNotificationManagerLineNotifierの知識を持たせずに実装できる分メリットがあるように思います。

とはいえ、何でもかんでも抽象にして引数で渡した方がいいとも言えないので、ケースバイケースだとは思いますが、、、