Solid原則 - SRP
Single Responsibility Principle (単一責任の原則)
“クラス変更する理由は1つ以上存在してはならない”
「これはね、Webも DBも メールも 何でもこなしている便利なサーバーなんだよ。」
「それって下手に触ると全部止まってしまうってことかい?」
SRPはクラスの責務を限定することで、保守の範囲を限定化するための原則です。
概要
冒頭の会話の例だと、最近ではコンテナを活用して環境を分離するのではないでしょうか。コンテナは一つのプロセスに責任を持つ存在です。一つの目的のために用意された環境なので、その目的に沿わない部品は気軽に変更・削除ができます。同じ考え方はプログラムにも適用可能です。プログラムはプロセスの構成要素ですから責務は更に細分化されます。
細分化はアプリケーションが提供する機能による細分化、または1つの機能を実現するための役割による細分化、といった複数の切り口から行うことができます。機能による細分化は作るアプリケーションに依って異なりますが、役割による細分化の具体例としては、次のようになります。
- [情報系] メモリ上の情報保持に責任を持つモデル
- [処理系-Biz] アクターとのユースケースを制御するコントローラ
- [処理系-Biz] アトミックなロジックに責任を持つサービス
- [処理系-Biz] 永続データのやり取りに責任を持つリポジトリ
- [処理系] ビジネスを意識しない汎用的なユーティリティ
*1) Biz = ビジネスロジック系
こうして責務を細分化してゆくと、クラスが負う責務は1つになり、責務が絞られるとそのクラスを変更する理由も1つに絞られます。つまり単一の責任しか負わないわけです。単一の責任しか負わないクラスでは次のような変化が起こります。
- コード量減とノイズ減を実現することができ
- 読み込む際の理解容易性が向上し
- 更新する際の副作用が低下する
これが保守をする際の安全性に繋がります。この様に、単一の責任だけを持ったクラスが協調して全体を構築するべきだという原則が、単一責任の原則となります。
特徴
単一責任の原則には幾つかの特徴的な部分があります。
1つ目はクラスの抽象化観点(縦軸)における継承やポリモーフィズムとの関連の薄さです。リスコフの置換原則やインターフェイス分離の法則、依存性逆転の法則は継承やポリモーフィズムといった言語機能の活用を前提としたパターンですが、単一責任の原則にはその前提がありません。単なるクラスにも適用可能です。単一責任の原則は純粋に責任を分割するだけの原則なので、特に言語機能には依存しないのです。しかし、依存しないとは言え無関係な訳ではありません。継承時には追加した実装分に単一の責任をもたせ、インターフェイス実現時にはその実装に対して単一の責任を持たせる…というデザインで付加価値を与えることは可能だからです。
2つ目はクラス間連携(横軸)における適用局面の広さです。単なるクラス間の関係にも、継承やインターフェイスの実現といった"is-a"関係にも関連する原則だからです。特定の言語機能に依存しない考え方の原則というのはこういう点で強力です。更にはサブシステム間、マイクロサービス間、システム間、といったより大きな粒度においても適用可能ですし、逆にクラスより粒度の細かいメソッドの責務設計にも適用できます。具体的には「ビジネストランザクション全体」「再利用可能なビジネスフロー」「非ビジネスユーティリティ」といった粒度での責務分割がこれに相当します。こういった適用範囲の広さも特徴的なわけです。
3つ目は適用局面の広さに関連しますが、初期フェーズにおける適用の重要さ(時間軸)です。デザイン全体に適用可能なものですから、設計初期に単一責任の原則を理解した設計者が居るか居ないかで、その後のシステムにおける保守性は大きく変わります。それは原野を前に都市計画を用意する様なもので、その有無で区画に用途を定めた整然とした街ができるのか、または予測不可能な蚤の市の様になるのかが分かれます。
以上の様に、非常に大きな効果を得られるので、最初に覚える原則としては最もおすすめできるもの、それが単一責任の原則となります。
コードサンプル
(JVM系の言語のほうが得意ですが、勉強がてらGolangで書いてみました)
Before
以下のコードはサーバーを表現しています。このサーバーは Webサーバ・DBサーバ・Mailサーバの3種類の役割を果たしています。昔はこの様なサーバも多く見かけましたが、1つのサーバー内に複数の責務をもたせると、このサーバーをメンテナンスする理由が増えます。しかも、メンテナンス時には他の機能に影響が出ないように慎重にならなければなりません。これは現実の世界でもコードの世界でも同じです。単一の責任をもたせるようにリファクタリングしなければなりません。
// 複数の責務を負ってしまっている状態
type Server struct {
HTMLs []*web.HTML
DBConn *db.Conn
Mails []*mail.Mail
}
func (s Server) ServeWeb() error {
}
func (s Server) ServeDB() error {
}
func (s Server) ServeMail() error {
}
After
以下のコードはサーバーの問題点を修正したコンテナを表現しています。コンテナはインターフェイスで表現され、実体はWebコンテナ・DBコンテナ・Mailコンテナに分かれ、それぞれが独自の役割を果たします。つまり、各コンテナは単一の責任しか負いませんので、修正の理由もその責務の範囲内のものに限定されます。こうすると修正対象のコードが限定され、保守しやすくなります。さらに、修正の影響もコンテナ内に閉じられますから、他の機能に変更の副作用が出ることはありません。これもまた現実の世界でもコードの世界でも同じです。
// 共通項としてのインターフェイス
type Container interface {
Serve() error
}
// Web特化
type WebContainer struct {
HTMLs []*web.HTML
}
func (w WebContainer) Serve() error {
}
// DB特化
type DBContainer struct {
DBConn *db.Conn
}
func (d DBContainer) Serve() error {
}
// Mail特化
type MailContainer struct {
Mails []*mail.Mail
}
func (m MailContainer) Serve() error {
}
// 組み合わせて提供したい場合はこれも特化して用意
type Server struct {
Containers []*Container
}
func (s Server) Serve() {
for _, c := range s.Containers {
go c.Serve()
}
}
まとめ
今回は、Single Responsibility Principle(単一責任の原則)を学びました。SRPは様々な局面で利用可能な基本的な設計原則です。威力を発揮するのはシステムやコードの保守が始まってからですが、その際の効率が飛躍的に変わるいぶし銀の設計です。ぜひ身につけてください。
それでは、SRPのある良きプログラミングライフを!
comments powered by Disqus