SOLID原則:単一責任の原則

単一責任の原則とは?

まずはざっくり理解してみる。

  • Single Responsibility Principle = SRP
  • クラスや関数、メソッドなどがもつ責任は1つにする ということ
  • もう少し分かりやすい表現にすると、クラスや関数などを 変更する理由は1つだけ持つようにしましょう という原則
  • クラスや関数のコードを変更する理由が2つ以上ある場合は良くないということ

良くない例

  • 以下のEmployeeクラスは変更する理由を3つ持つのでよくない
class Employee:
    def calculatePay(self):
        # 従業員の給与を計算するメソッド
        # 給与計算に関するビジネスルールが変更するたびに変更が入るメソッド
        pass

    def reportWorkingHours(self):
        # 従業員の勤怠をまとめるメソッド
        # 勤怠に関するビジネスルールが変更するたびに変更が入るメソッド
        pass

    def save(self):
        # 従業員に関する情報をDBに保存するメソッド
        # 従業員に関する情報が変わるたびに変更が入るメソッド
        pass
  • ↑のクラスは変更する理由を3つも持っており、不安定なクラス と言えるらしい

修正案

  • それぞれの変更理由ごとにクラスを分ける という修正
# 従業員の給与を計算するクラス
# 従業員の給与に関するビジネスルールが変わったらここだけ修正すれば良い
class EmployeePay():
    def calculatePay(self):
        pass

# 従業員の勤怠を管理するクラス
# 従業員の勤怠に関するビジネスルールが変わったらここだけ修正すれば良い
class EmployeeWorkingHour():
    def reportWorkingHours(self):
        pass

# 従業員のDBを操作するクラス
class EmployeeRepository():
    def save(self):
        pass

    def get(self)

https://プログラマが知るべき97のこと.com/エッセイ/単一責任原則/

https://zenn.dev/k_sato/articles/98f4b15747e191

単一責任の原則メリットを考える

単一責任の原則の概要を理解したので、今度は、メリットを考えてみる

単一責任の原則を守らないとどうなるのか?

わざとこの原則を破ってみる。

  • 複数の機能を持たせたクラスである Sample クラスを例に挙げてみる
  • Sample クラスには、A機能とB機能があると仮定する
  • イメージはこんな感じでしょうか・・・
  • (この時点でちょっとミスってるコードだが、気にしない、とりあえず)
class Sample:

    number: int

    def __init__(self, number: int):
        self.number = number
    
    def a(self):
        # A機能
        return self.number * 100

    def b(self):
        # B機能
        # B機能では、10以下の数値が来ることしか想定していない
        if self.number < 10:
            return self.number - 1
        else:
            raise ValueError
  1. 複数の機能(A機能、B機能)を一つのクラスや関数に持たせちゃう

    つまり、責任が一つではない、修正する理由が一つではないという状態

  2. A機能を動作させたところ正常に終了した

  3. 次にB機能を動作させたところ正常に終了した

    ⇒ ここまでは良さそう

  4. ある日、A機能に仕様が追加された。

    A機能では、100以上の数字しか扱わないとの仕様になった。そこで・・・

class Sample:

    number: int

    def __init__(self, number: int):
        
        # 100以上の数値しか扱わない
        if 100 <= number:
            self.number = number
        else:
            raise ValueError
    
    def a(self):
        # A機能
        return self.number * 100

    def b(self):
        # B機能
        if self.number < 10:
            return self.number - 1
        else:
            raise Error

numberは100以上じゃないと例外に飛ぶようにした

  1. 仕様変更したA機能を動作させたところ、正常に終了した

  2. 続いてB機能を動作させたところ、 ValueError になってしまった・・・!

これまで動作していた以下のコードでエラーになってしまう事態が起きた模様・・・

sample = Sample(number=3) ←ここ

result = sample.b()

新A機能を実現しようとしたら、既存のB機能が動かなくなってしまった

ソフトウェアにおける責任を全うさせるために存在すると理解

  • ソフトウェアというのは、とある機能をエラーなく実行したいという思いの元、作成されるはず。

  • つまり、エラーなく正常に処理し、正常に終了するという期待(責任)がソフトウェアにはある

  • この、正常にエラーなく動作するという責任を全うする可能性をなるべく上げるために、単一責任の原則は存在している (と一旦理解。)

  • 今回、例に挙げたA機能、B機能もクラスが分かれていれば、A機能の仕様変更がB機能の挙動に影響を与えることもなかった => つまり、B機能が ValueError になる事態は避けられたはず。

https://qiita.com/MinoDriven/items/76307b1b066467cbfd6a

単一責任の原則について、勉強してみたが、設計や設計思想って本当に奥が深いし、語れたら、本当にすごいエンジニアなのではないかと思う、今日このごろ。。

それに↑のQiitaの記事によると、ビジネスの関心がないと困難とのこと。。。

時間をかけて設計については勉強していきたいですね 🤕