😆

SOLID原則を説明してみる(SRP: 単一責任原則)

に公開

はじめに

ソフトウェア設計において、SOLID原則は保守性の高いコードを書くための重要な指針です。
本記事では、その中でも特に誤解されやすい「単一責任原則(Single Responsibility Principle, SRP)」について解説します。

(Claude Codeと殴り合った結果をまとめてみました)

SOLID原則を説明してみるシリーズ

ぜひこちらもお願いします😆

単一責任原則とは

単一責任原則は、Robert C. Martin(Uncle Bob)によって提唱されたSOLID原則の一つです。後年の著書「Clean Architecture」において、より明確な定義が示されました。

単一責任原則は「モジュールはたった1つのアクターに対して責任を負うべき」という原則です。ここでいう「モジュール」とは、関数、クラス、パッケージなど、まとまった機能単位を指します。重要なのは、「1つのことだけをする」という意味ではなく、「1つのアクターからの変更理由にのみ応答する」という点です。

アクターとは「ビジネス上の関心事を持つグループ」を指します。例えば、営業部門やマーケティング部門などです。個人ではなく、共通の関心事を持つ人々の集まりと捉えるのが適切です。

アクターの同一性は静的に決まるものではありません。複数のグループが完全に同じ理由で同じ変更を要求し続ける限り、それらは同一のアクターとみなせます。しかし、一方のグループ独自の関心事が混入した瞬間、それらは異なるアクターになったと言えます。

異なるアクターの関心事を同一モジュールに混在させると、一方のアクターからの変更要求が他方に意図しない影響を与えるリスクが生じます。

そのため、単一責任原則は以下のように言えます。

サンプルコード

自動生成なのでコードとしては適当です。。。

単一責任原則に違反したコード例

この例では、Customer構造体とCustomerServiceの関数が 営業部門マーケティング部門 という2つの異なるアクターに対して責任を負っています。

package customer

import "fmt"

type Customer struct {
    ID         string
    Name       string
    /*
        1つの構造体を複数の異なるアクター(部門)に対応するためのフィールド。
        SRPに違反しているとこのような定義が入り込みやすい。
    */
    department string
    
    // 営業部門の関心事
    SalesRep     string
    TotalRevenue float64
    SalesStatus  string
    
    // マーケティング部門の関心事
    CampaignSegment  string
    EngagementScore  int
    MarketingStatus  string
}

type CustomerService struct {}

// 営業部門への責務
func (s CustomerService) GetRevenue(customer Customer) string {        
    if customer.TotalRevenue > 5000000 {
        return "VIP"
    }
    return "Standard"
}

// マーケティング部門への責務
func (s CustomerService) GetEngagement(customer Customer) string {
    if customer.EngagementScore > 80 {
        return "Hot"
    }
    return "Cold"
}

// 複数のアクターの責務が混ざった結果、関数の内部でわけることになった例
func (s CustomerService) Report(customer *Customer, department string) {
    // ...(共通処理)
    // 部門識別による条件分岐 = SRP違反のシグナル
    if customer.department == "Sales" {
        // 営業部門特有の処理
    } else if customer.department == "Marketing" {
        // マーケティング部門特有の処理
    }
    // ...(共通処理)
}

単一責任原則を順守したコード例

アクターごとにモジュールを分離することで、営業部門の要求による変更がマーケティング部門に影響を与えることがなくなります。

package sales

type SalesCustomer struct {
    ID           string
    Name         string
    TotalRevenue float64
}

type SalesCustomerService struct {}

func (s SalesCustomerService) GetRevenue(customer SalesCustomer) string {
    if customer.TotalRevenue > 5000000 {
        return "VIP"
    }
    return "Standard"
}

func (s SalesCustomerService) Report(customer SalesCustomer) {
    // ...(営業のレポート処理)
}
package marketing

type MarketingCustomer struct {
    ID               string
    Name             string
    EngagementScore  int
}

type MarketingCustomerService struct {}

func (s MarketingCustomerService) GetEngagement(customer MarketingCustomer) string {
    if customer.EngagementScore > 80 {
        return "Hot"
    }
    return "Cold"
}

func (s MarketingCustomerService) Report(customer MarketingCustomer) {
    // ...(マーケティング部門のレポート処理)
}

よくある誤解

DRYによる共通化

よくあるのが、DRY原則があるから、同じコードは何が何でも共通化するべきだという意見です。

しかし、異なるアクターの関心事における重複は「偶発的重複」であり、これを無理に共通化することは将来の独立した変更を妨げる可能性があります。

例えば、違反例の CustomerService.Report() のようにアクターによって処理を分けるための条件分岐が入り込むことがあります。
小さな例では問題は少ないですが、ソフトウェアが大きくなると将来的に分岐まみれになりかねません。可読性が悪化しますし、分離するときにバグを引き起こす要因になります。

そのため、DRY原則によるコードの共通化は、同じ関心事を持つアクター同士でコードが重複したときに限るべきです。
同じアクターの関心事における重複が「本質的重複」であり、このような重複がDRY原則の対象です。

結論

単一責任原則の本質は「変更理由の分離」です。アクターという概念を軸に、誰の要求で変更されるのかを常に意識することで、保守性の高い設計が実現できます。

Discussion