依存関係逆転の原則 とは?
依存関係逆転の原則(Dependency Inversion Principle, DIP)とは、SOLID原則の一つであり、以下の2点を示しています。
- 上位レベルのモジュールは低位レベルのモジュールに依存してはならず、どちらも抽象に依存すべきである。
- 抽象は詳細に依存してはならず、詳細が抽象に依存すべきである。
簡単に言えば、具体的な実装に依存するのではなく、抽象的なインターフェースに依存することです。
なぜ 依存関係逆転の原則 が重要なのか?
依存関係逆転の原則を守ることで、ソフトウェア設計の柔軟性や拡張性が劇的に改善されます。具体的なクラスに強く依存していると、変更に弱く、保守が難しくなります。しかし抽象に依存することで、新たな機能や変更を加える際の影響範囲を最小限に抑えることができます。
例えば、通知機能を持ったコードを考えてみましょう。メール通知だけを想定し直接メール送信クラスに依存している場合を示します。
修正前のコード
class EmailNotifier
def send(message)
puts "メールで通知: #{message}"
end
end
class Order
def initialize
@notifier = EmailNotifier.new
end
def complete_order
# 注文処理...
@notifier.send("注文が完了しました。")
end
end
問題点
Order
クラスが具体的な通知手段であるEmailNotifier
に直接依存しています。
- 通知手段を変更・追加するとき、
Order
クラスの修正が必要になります。
修正後のコード
# 抽象インターフェースを定義
class Notifier
def send(message)
raise NotImplementedError, "Notifier#sendは実装が必要です"
end
end
# 具体的な通知手段(メール)
class EmailNotifier < Notifier
def send(message)
puts "メールで通知: #{message}"
end
end
# 注文クラスは抽象的なNotifierに依存
class Order
def initialize(notifier)
@notifier = notifier
end
def complete_order
# 注文処理...
@notifier.send("注文が完了しました。")
end
end
# 利用例
email_notifier = EmailNotifier.new
order = Order.new(email_notifier)
order.complete_order
解決された問題
- 注文クラスは具体的な通知方法を知らなくてもよくなり、通知手段を後から追加・変更しても修正の必要がなくなりました。
- 柔軟性が向上し、保守性が高まりました。
まとめ
依存関係逆転の原則を守ることで、直接的な依存を避け、変更に強いシステムを構築できます。抽象に依存することで、システムはより拡張性が高く、保守性が向上します。
練習問題 (1)
修正前のコード
class MysqlDatabase
def save(data)
puts "MySQLに#{data}を保存しました"
end
end
class Report
def initialize
@db = MysqlDatabase.new
end
def generate
@db.save("レポートデータ")
end
end
修正後のコード
class Database
def save(data)
raise NotImplementedError, "Database#saveは実装が必要です"
end
end
class MysqlDatabase < Database
def save(data)
puts "MySQLに#{data}を保存しました"
end
end
class Report
def initialize(db)
@db = db
end
def generate
@db.save("レポートデータ")
end
end
# 利用例
db = MysqlDatabase.new
report = Report.new(db)
report.generate
直接具体的なMysqlデータベースに依存するのをやめて抽象Databaseクラスに依存させました。これにより、後からPostgreSQLやMongoDBなど他のDBを簡単に使えるようになります。
練習問題 (2)
修正前のコード
class CsvExporter
def export(data)
puts "#{data}をCSV出力しました"
end
end
class Analytics
def initialize
@exporter = CsvExporter.new
end
def export_report
@exporter.export("分析データ")
end
end
修正後のコード
class Exporter
def export(data)
raise NotImplementedError, "Exporter#exportは実装が必要です"
end
end
class CsvExporter < Exporter
def export(data)
puts "#{data}をCSV出力しました"
end
end
class Analytics
def initialize(exporter)
@exporter = exporter
end
def export_report
@exporter.export("分析データ")
end
end
# 利用例
exporter = CsvExporter.new
analytics = Analytics.new(exporter)
analytics.export_report
Exporterという抽象を導入することで、後でJSONやExcelなどの別形式出力への変更が容易になりました。
練習問題 (3)
修正前のコード
class StripePayment
def pay(amount)
puts "Stripeで#{amount}円支払い"
end
end
class Checkout
def initialize
@payment = StripePayment.new
end
def process(amount)
@payment.pay(amount)
end
end
修正後のコード
class PaymentMethod
def pay(amount)
raise NotImplementedError, "PaymentMethod#payは実装が必要です"
end
end
class StripePayment < PaymentMethod
def pay(amount)
puts "Stripeで#{amount}円支払い"
end
end
class Checkout
def initialize(payment_method)
@payment = payment_method
end
def process(amount)
@payment.pay(amount)
end
end
支払い方法を抽象化することで、将来的にPaypalなど別手段への変更が簡単になります。
練習問題 (4)
修正前のコード
class LocalStorage
def store(data)
puts "#{data}をローカルストレージに保存しました"
end
end
class UserSession
def initialize
@storage = LocalStorage.new
end
def save_session(data)
@storage.store(data)
end
end
修正後のコード
class Storage
def store(data)
raise NotImplementedError, "Storage#storeは実装が必要です"
end
end
class LocalStorage < Storage
def store(data)
puts "#{data}をローカルストレージに保存しました"
end
end
class UserSession
def initialize(storage)
@storage = storage
end
def save_session(data)
@storage.store(data)
end
end
# 利用例
storage = LocalStorage.new
session = UserSession.new(storage)
session.save_session("ユーザーセッションデータ")
UserSession
クラスがLocalStorage
に直接依存しないように修正しました。これにより、クラウドストレージやデータベースなど別の保存手段を簡単に導入できるようになります。
練習問題 (5)
修正前のコード
class PdfGenerator
def generate(content)
puts "PDFに#{content}を出力しました"
end
end
class Document
def initialize
@generator = PdfGenerator.new
end
def create_document(content)
@generator.generate(content)
end
end
修正後のコード
class DocumentGenerator
def generate(content)
raise NotImplementedError, "DocumentGenerator#generateは実装が必要です"
end
end
class PdfGenerator < DocumentGenerator
def generate(content)
puts "PDFに#{content}を出力しました"
end
end
class Document
def initialize(generator)
@generator = generator
end
def create_document(content)
@generator.generate(content)
end
end
# 利用例
generator = PdfGenerator.new
document = Document.new(generator)
document.create_document("ドキュメント内容")
Document
クラスがPdfGenerator
に直接依存しないようにし、DocumentGenerator
という抽象クラスを導入しました。将来的にWordやHTML形式での出力も可能になります。
以上のように、依存関係逆転の原則により具体的な実装への依存を避け、柔軟でエレガントな設計が可能になります。