会社のSlackや技術書などで「依存性の注入」という言葉をよく目にするんですが、内容がわかっていなかったので、備忘録としてまとめます。
サンプルコードはRubyとRSpecで書くので、Rubyの経験しかないエンジニアの参考になれば幸いです。
そもそも依存とは?
「依存性の注入」を理解するためにはまず、そもそも「依存とは何か?」から知らなくてはいけません。
依存とは、「ある処理を実行するために別の処理を必要とすること」を指します。
たとえばクラスAのなかでクラスBのメソッドを呼んでいる場合、「クラスAはクラスBに依存している」と言えます。
ざっくり「呼び出す側=依存する側」、「呼び出される側=依存される側」の理解でOKです。
依存は完全にゼロにはできませんが、「依存度が高い=密結合」なので、できるかぎり依存度が低い状態、つまり疎結合をめざす必要があります。
依存性の注入とは?
そこで登場するのが、「依存性の注入」です。
依存性の注入は、依存しているオブジェクトをクラス内で直接生成するのではなく、外部から渡すことで、結合度を下げる手法です。
これだけじゃ意味がわからないので、通知サービスを例にサンプルコードで説明します。
依存性の注入なしの例
まずは依存性の注入を使わない場合の例です。
EmailService
クラスはメール通知ロジックを、NotificationService
クラスはEmailService
クラスを用いて通知の実行を担います。
require 'net/smtp'
# メール通知のロジックを実装
class EmailService
def initialize(from, smtp_server, port = 587)
@from = from
@smtp_server = smtp_server
@port = port
end
def send(to, subject, message)
msg = <<~MESSAGE
From: #{@from}
To: #{to}
Subject: #{subject}
#{message}
MESSAGE
Net::SMTP.start(@smtp_server, @port) do |smtp|
smtp.send_message msg, @from, to
end
rescue StandardError => e
puts "Failed to send email: #{e.message}"
end
end
# 通知の送信を実装
class NotificationService
def initialize
# EmailService クラスのインスタンスを NotificationService クラス内で直接生成している
@emailer = EmailService.new('noreply@example.com', 'smtp.example.com')
end
def notify(user_email, subject, message)
@emailer.send(user_email, subject, message)
end
end
# 使用例
notification_service = NotificationService.new
notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')
上記の場合、NotificationService
クラスはEmailService
クラスのインスタンス生成時にfrom
とsmtp_server
の2つが必要であることを知ってしまっています。
そのため相対的に依存度が高い(=密結合)状態です。
依存性の注入を用いた例
つづいて依存性の注入を用いた例です。
各クラスが担う役割はおなじです。
require 'net/smtp'
# EmailServiceクラスは変更なし
class EmailService
def initialize(from, smtp_server, port = 587)
@from = from
@smtp_server = smtp_server
@port = port
end
def send(to, subject, message)
msg = <<~MESSAGE
From: #{@from}
To: #{to}
Subject: #{subject}
#{message}
MESSAGE
Net::SMTP.start(@smtp_server, @port) do |smtp|
smtp.send_message msg, @from, to
end
rescue StandardError => e
puts "Failed to send email: #{e.message}"
end
end
class NotificationService
# EmailService クラスのインスタンスを直接生成せず、引数として受け取る
def initialize(sender)
@sender = sender
end
def notify(user_contact, subject, message)
@sender.send(user_contact, subject, message)
end
end
# 使用例
email_service = EmailService.new('noreply@example.com', 'smtp.example.com')
# email_service を引数として渡す
notification_service = NotificationService.new(email_service)
notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')
↑のように書けば、EmailService
クラスのインスタンス生成時にどんな引数が必要であるか、NotificationService
クラスが知っている必要はなく、生成済みのインスタンスを受け取ればよいだけです。
つまり、より疎結合(=依存度が相対的に下がった)な状態と言えます。
これが「依存性の注入」です。
依存性の注入のメリット
この「依存性の注入」を使用するメリットとして以下の3つが挙げられます。
- コードを変更しやすい
- 機能を追加しやすい
- テストがカンタン
なるべくカンタンな言葉でより詳しく説明します。
1. コードを変更しやすい
たとえば先ほどの通知サービスの例の場合、依存性の注入を使わないと、EmailService
クラスのinitialize
メソッドの引数が変更された場合に、NotificationService
クラスまで修正が必要になってしまいます。
# 依存性の注入を使っていない場合
class NotificationService
def initialize
# SMTPサーバのホスト名を変更する場合、第2引数の 'smtp.example.com' も修正が必要になってしまう
@emailer = EmailService.new('noreply@example.com', 'smtp.example.com')
end
def notify(user_email, subject, message)
@emailer.send(user_email, subject, message)
end
end
# 使用例
notification_service = NotificationService.new
notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')
しかし、依存性の注入を使えば、たとえEmailService
クラスに必要な情報が変化したとしても、NotificationService
クラス側には影響しません。
# 依存性の注入を使っている場合
class NotificationService
# EmailServiceクラスのインスタンスを引数で受け取る
def initialize(sender)
@sender = sender
end
def notify(user_contact, subject, message)
@sender.send(user_contact, subject, message)
end
end
# SMTPサーバのホスト名を 'smtp.example.com' から 'new_smtp.example.com' へ変更する
email_service = EmailService.new('noreply@example.com', 'new_smtp.example.com')
# ホスト名が変わったとしても email_service を引数として渡せばいいだけ
notification_service = NotificationService.new(email_service)
notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')
このように、後から変更が発生したとしても、他のコードに影響を及ぼさず、修正箇所を最小限におさえられるのが「依存性の注入」の1つ目のメリットです。
2. 機能を追加しやすい
依存性の注入を使わない場合のNotificationService
クラスはEmailService
クラスに依存してしまっている状況でした。
# 依存性の注入を使っていない場合
class NotificationService
def initialize
# NotificationService クラス内で EmailService クラスのインスタンスを生成(=依存している)
@emailer = EmailService.new('noreply@example.com', 'smtp.example.com')
end
def notify(user_email, subject, message)
@emailer.send(user_email, subject, message)
end
end
# 使用例
notification_service = NotificationService.new
notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')
これだと、メール以外の方法で通知したくなったら、NotificationService
クラスも修正する or NotificationService
とは別の通知用の〇〇Service
クラスを作成しないといけません。
しかし、依存性の注入を採用すれば、他の通知方法もカンタンに追加できます。
以下のコードはSMSでの通知機能のために追加したSmsService
クラスの実装です。
require 'twilio-ruby'
# 新たにSMS通知の機能も実装
class SmsService
def initialize(api_key, password)
@api_key = api_key
@password = password
@client = Twilio::REST::Client.new(api_key, password)
end
def send(user_phone_number, subject, message)
begin
message = "[#{subject}] #{message}"
@client.messages.create(
from: '+15555555555',
to: user_phone_number,
body: message
)
rescue Twilio::REST::RequestError => e
puts Failed to send sms: #{e.message}"
end
end
end
たとえ通知がメールだろうとSMSだろうとNotificationService
クラスの実装に修正は必要ありません。
# 依存性の注入を使っている場合
class NotificationService
# 引数が EmailService クラスのインスタンスでも SmsService クラスのインスタンスでも変わらない
def initialize(sender)
@sender = sender
end
def notify(user_contact, subject, message)
@sender.send(user_contact, subject, message)
end
end
SmsService.new(ENV['TWILIO_API_KEY'], ENV['TWILIO_PASSWORD'])
# email_service ではなく sms_service を引数として渡せば後は同じ
notification_service = NotificationService.new(sms_service)
notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')
このように、他の種類の機能追加をカンタンに実装できるのが「依存性の注入」の2つ目のメリットです。
3. テストがカンタン
一言で表すと、「テストがしやすくなる」ということです。
NotificationService
クラスのテストを書く際には、EmailService
クラスのインスタンスをモック化する必要があります。
依存性の注入を使わなかった場合、NotificationService
クラス内で直接EmailService
クラスのインスタンスを生成しているため、EmailService
クラスの内部挙動に依存したモックとなってしまいます、、。
# 依存性の注入を使っていない場合
describe NotificationService do
it '通知の送信に成功する' do
# EmailService の実際のインスタンスが生成されるため、メールサーバーへの接続が発生しうる
notification_service = NotificationService.new
# テスト実行時に実際にメールを送信しないよう、Net::SMTP をモックするなどの工夫が必要
# ↑はEmailServiceクラスの内部挙動に依存してしまっている
allow(Net::SMTP).to receive(:start).and_return(true)
expect(notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')).to be_truthy
end
end
依存性の注入を使えば、このテストはより簡潔になります。
# 依存性の注入を使う場合
describe NotificationService do
it '通知の送信に成功する' do
# double メソッドで EmailService クラス自体をモック化
mock_email_service = double('EmailService')
# mock_email_service が send メソッドを受け取った時に true を返すよう設定
allow(mock_email_service).to receive(:send).and_return(true)
notification_service = NotificationService.new(mock_email_service)
expect(notification_service.notify('user@example.com', 'Welcome!', 'Welcome to our service!')).to be_truthy
end
end
このテストコードは、以下の2点をテストしています。
NotificationService
クラスがEmailService
クラスのsend
メソッドを呼び出しているかどうかnotify
メソッドがEmailService
のsend
メソッドに適切な引数を渡しているかどうか
EmailService
クラスの内部挙動には依存せずに済みます。
このように、シンプルかつ堅牢なテストを書けるのが「依存性の注入」を用いる3つ目のメリットです。
おわりに
ようやく依存性の注入が理解できました。
ドメイン駆動設計やクリーンアーキテクチャといった設計思想では、この依存性の管理が肝になるので、そういった概念を学ぶ最初の一歩を進めた気がします。
コメント