保守性と拡張性を高めるためのSOLID原則とシステム設計
第2話:単一責任の原則(2)実践例
(最終更新日:2024.09.12)
(絵が小さい場合はスマホを横に)
単一責任の実践例
単一責任の原則(SRP)は、ソフトウェア設計において各クラスやモジュールが一つの責務のみを持つべきであるという原則だ。
今回は単一責任を実現するプログラミングを具体的な例を用いて解説する。
これにより、保守性と拡張性の高いシステム設計を目指す。
[目次]
1.単一責任の原則を実践する方法
このセクションでは、単一責任の原則を具体的にどのように実践するか、その方法や手順、考慮すべきポイントを解説する。
1.1 責任の特定と分類
最初のステップは、既存のクラスやモジュールが持っている責任を洗い出し、それらを明確にすることだ。
各コンポーネントがどの機能や役割を担当しているのかを一覧にし、複数の責任を持っている場合は、それらを分類する。
複数の責任を持つクラスを見つけたら、それぞれの責任をグループ化し、どのような観点から分類できるかを検討する。
たとえば、データアクセス、ビジネスロジック、UIロジックなど、異なる関心ごとに基づいて分類する。
1.2 責任の分離
一つのクラスやモジュールに複数の責任が含まれている場合、それを複数のクラスやモジュールに分割し、それぞれが単一の責任のみを持つようにする。
このプロセスは、リファクタリングの一環として行われる。
分割したクラスやモジュール間の依存関係を整理し、適切に管理する。
たとえば、依存性注入やインターフェースの利用を通じて、各クラスが独立して動作するように分ける。
依存性注入は責任を分離し、テストもしやすい
1.3 インターフェースの導入
各クラスやモジュールの責任を明確にするために、インターフェースを導入する。
インターフェースは、クラスが提供するべき機能や役割を定義し、それに従ってクラスを実装する。
これにより、クラスの責任が明確化され、役割がはっきりする。
インターフェースを用いることで、依存関係を抽象化し、クラス間の依存を減らす。
これにより、変更に対する影響が少なくなり、各クラスが独立して進化できるようになる。
1.4 リファクタリングの実施
単一責任の原則を維持するためには、定期的にコードをレビューし、リファクタリングを行うことが重要だ。
特に、プロジェクトが進行するにつれて、新しい機能の追加や要件の変更により、クラスが複数の責任を持つようになる。
リファクタリングの際には、単体テストや統合テストを利用して、変更が他の部分に影響を与えないか確認する。
テストを自動化することで、リファクタリングの安全性と効率性が向上する。
1.5 役割の委譲
単一責任の原則に従って設計されたクラスが、他の責任を持つことになった場合、それを適切に委譲する。
たとえば、データベースアクセスの責任を持たないクラスがデータベース操作を行う必要がある場合、
それを専用のデータアクセスクラスに委譲する。
クラス間で役割と責任を分担する際に、過度な結合を避けるため、依存性注入を活用し、クラス間の独立性を保つ。
これにより、役割の委譲がスムーズに行われ、システム全体の保守性が向上する。
1.6 実践例
また、下記はPythonのコードである。リファクタリング前は、注文の検証、支払い、メール送信が1つの関数で混在していたが、
それぞれを関数に分けることで、単一責任を実現している。
リファクタリング前
リファクタリング後
2.良い例と悪い例
このセクションでは、単一責任の原則を守った設計の良い例と、守れていない悪い例を具体的に示し、 どのようにリファクタリングすべきかを解説する。
2.1 悪い例: 複数の責任を持つクラス
例えば、下記のように、あるシステムでユーザーの登録とメール通知を行うJavaのクラスがある。
このクラスが、ユーザー情報の保存と確認メールの送信の両方を担当している場合(リファクタリング前)を考える。
リファクタリング前
ここでの問題点は、複数責任と蜜結合だ。
複数責任:UserServiceクラスは、ユーザー情報の保存(データベース操作)と確認メールの送信(メール通知)という、2つの異なる責任を持っている。
これにより、どちらかの機能に変更が必要になった場合、UserServiceクラス全体を修正する必要があり、他の機能にも影響を与えるリスクが高まる。
密結合:メール送信のロジックがUserServiceクラスに直接埋め込まれているため、
メール送信の仕組みを変更する場合にUserServiceクラスも変更する必要が生じる。
2.2 良い例: 責任を分離した設計
悪い例をリファクタリングし、単一責任の原則に従って設計する。
下記は、ユーザーの登録とメール送信の責任を別々のクラスに分離した。
リファクタリング後
改善点は以下の3点である。
単一責任の原則を遵守:UserServiceクラスはユーザーの登録プロセスのみを担当し、
ユーザー情報の保存はUserRepository、メール送信はEmailServiceがそれぞれ担当している。
これにより、各クラスが単一の責任を持つようになり、コードがよりモジュール化される。
疎結合:メール送信やデータ保存の具体的な実装は、それぞれの専用クラスに分離されている。
これにより、UserServiceクラスは特定のデータベースやメール送信の実装に依存せず、柔軟性が高まる。
変更に強い設計:たとえば、メール送信のロジックを変更する場合はEmailServiceクラスのみを変更すれば済み、
他のクラスには影響がない。これにより、変更による影響範囲が限定され、保守が容易になる。
2.3 さらに良い設計へのステップ(依存性注入の活用)
さらに、依存性注入(Dependency Injection, DI)を利用することで、UserServiceクラスのテストがしやすくなり、柔軟性がさらに向上する。
たとえば、UserServiceのテスト時にUserRepositoryやEmailServiceのモックを注入することで、ユニットテストを容易に行うことができる。
上記のリファクタリング後のUserRespositoryクラス、EmailNotificationServiceクラスに対して、それぞれのモックを生成し、UserServiceクラスに注入している。
ちなみに、MockitoはJavaのモックフレームワークの1つで、テスト時に依存するオブジェクトのモックを作成してくれる。
モックとは、実際データベースに登録を行わないが、モックを通して呼び出したらある値を返してくれるといったものである。振る舞いを真似てくれる。
依存性注入でモックを注入
3.まとめ
今回は、前回に引き続き単一責任の原則(SRP)を学んだ。 良い例、悪い例、リファクタリングを通して、単一責任を実践する意義が理解できたと思う。 今回学んだ方法を活かして、単一責任を実現するコードを作り、保守性と拡張性の高いプログラミングを行おう。