はじめに
前回の記事「SwiftでSOLID原則(3a/3)」では、SOLID原則のインターフェース分離の原則(ISP)について解説しました。この記事では、SOLID原則の依存性逆転の原則(DIP)に関して、Swiftを使った例で、紹介したいと思います。
依存性逆転の原則(DIP)
依存性逆転の原則(DIP)は、「高レベルモジュールは低レベルモジュールに依存してはならず、両者は抽象化に依存するべき」という考え方です。抽象化を使用することで、モジュール間の依存を減らし、柔軟で保守性の高い設計が可能になります。特にテストの際、依存性の注入によりテストが簡単になる点も大きなメリットです。
概念的な例:
家の電気システムを例にしてみましょう。家電製品(冷蔵庫、テレビ、電子レンジなど)は直接的に電源(配線やコンセント)に依存しているように見えるかもしれませんが、実際には抽象化(コンセントやプラグの規格)を通して電力供給を受けています。
抽象化の役割:
- 家電製品は「電力を受け取る」ことだけを知っています
- 電力を供給する側(発電所や家庭内の配線)は、「どの家電製品に接続されるか」を意識していません
もし家電製品がそれぞれ異なる規格のプラグや電圧に直接依存していたらどうなるでしょうか?家の配線を新しい家電に合わせて変更しなければならず、非効率的で保守性が低くなります。
解決策: 抽象化(インターフェース)の導入
家電製品と電源の間に共通のインターフェース(例えば、コンセントとプラグ)を設けることで、どちらもお互いの詳細を知らなくても動作できるようにします。
現実世界の例:
二つのコードを見てみましょう、
例1:
以下のコードでは、NotificationManager
クラスが具体的な実装である EmailService
および SMSService
に直接依存しています。この結果、コードは密結合となり、拡張性が損なわれます。
class EmailService {
func sendEmail(message: String) {
print("Email sent: \(message)")
}
}
class SMSService {
func sendSMS(message: String) {
print("SMS sent: \(message)")
}
}
class NotificationManager {
private let emailService = EmailService()
private let smsService = SMSService()
func sendEmailNotification(message: String) {
emailService.sendEmail(message: message)
}
func sendSMSNotification(message: String) {
smsService.sendSMS(message: message)
}
}
// 使用例
let manager = NotificationManager()
manager.sendEmailNotification(message: "こんにちは、メール!")
manager.sendSMSNotification(message: "こんにちは、SMS!")
この例の問題点は、NotificationManager
クラスが EmailService
と SMSService
に直接依存しているため、例えば PushNotificationService
のような新しい通知サービスを追加する場合、NotificationManager
を修正しなければなりません。コードのテストが難しくなり、わかりにくくなります。EmailService
や SMSService
をモックとして置き換えることができません。
具体的な実装に直接依存することでコードが密結合となり、保守性が低下します。
例2:
protocol NotificationService {
func sendNotification(message: String)
}
class EmailService: NotificationService {
func sendNotification(message: String) {
print("Email sent: \(message)")
}
}
class SMSService: NotificationService {
func sendNotification(message: String) {
print("SMS sent: \(message)")
}
}
class NotificationManager {
private let notificationService: NotificationService
init(notificationService: NotificationService) {
self.notificationService = notificationService
}
func notify(message: String) {
notificationService.sendNotification(message: message)
}
}
// 使用例
let emailService = EmailService()
let smsService = SMSService()
let emailManager = NotificationManager(notificationService: emailService)
emailManager.notify(message: "こんにちは、メール!")
let smsManager = NotificationManager(notificationService: smsService)
smsManager.notify(message: "こんにちは、SMS!")
この例では、NotificationManager
クラスが NotificationService
プロトコルという抽象化に依存しています。この設計により、具体的な実装に対する依存が排除され、コードが柔軟かつテストしやすくなります。
この例の改善点は、新しい PushNotificationService
を追加しても、NotificationManager
を変更する必要がありません。NotificationService
プロトコルをモックとして簡単に置き換えることができ、単体テストが容易になります。実行時に異なるサービスを簡単に切り替えることができます。依存性逆転の原則に準拠することで、抽象化に依存する設計の力を示し、コードの拡張性、テスト可能性、柔軟性を大幅に向上させています。
適用性:
この原則は、ソフトウェア設計において、結合を低くし、柔軟性を向上させるために有効です。
実装方法:
抽象化に依存させ、特定の実装には依存しないようにします。
メリット:
コードの拡張性が高まり、依存性の注入によってテストが容易になります。また、コードの再利用性も向上します。
制限:
抽象化によって複雑さが増し、特に設計が大規模になると管理が難しくなる場合があります。
結論:
これで、SOLID原則の全ての原則について掘り下げて学ぶことで、SOLID原則の理解が深まりました。これらの基本原則を実践することで、保守しやすく、柔軟性のあるSwiftコードの設計が可能になります。
次の記事まで、ハッピーコーディング!