🐘 設計の学習 - SOLID原則 -
作成日: 2021/07/14
0

注意)このTicketはあくまで自分用のメモ。厳密な定義などは参考サイトを参照のこと

SOLID原則について学ぶ

参考サイト1の動画で紹介されているコード例を自分なりにpythonに書き直して学習する。

SOLID原則とは

S(Single Responsibility Principle): 単一責任の原則
O(Open/Closed principle): 開放閉鎖の原則
L(Liskov substitution principle): リスコフの置換原則
I(Interface segregation principle): インターフェース分離の原則
D(Dependency inversion principle): 依存性逆転の原則

S:単一責任の原則(SRP)

1つのソフトウェア構成要素(サブシステムやモジュール、クラス、関数など)に、変更する理由が2つ以上あるようではいけない(参考サイト1より引用)

単一責任の原則(SRP)を適用することで、ソフトウェア構成要素が持つ単一の責務が変更されたときのみ、それが変更されるようにすることで、変更による影響を最小限に抑えることができます。(参考サイト1より引用)

NGの例

class Employee():
    # 給与計算処理
    def calculatePay(self):
        pass
    # レポート出力処理
    def reportHours(self):
        pass
    # データベースへの保存処理
    def save(self):
        pass

OKの例(参考サイト1)

class Employee():
    # 給与計算処理
    def calculatePay(self):
        pass

OKの例(参考サイト2)

#共有データ
class Employee():
    def __init__(self,id,name,salary):
        self.id = id
        self.name = name
        self.salary = salary

class PayCalculator():
    def __init__(self,employee):
        self.employeeData = employee

    # 給与計算処理
    def calculatePay(self):
        pass

class HourReporter():
    def __init__(self,employee):
        self.employeeData = employee

    # レポート出力処理
    def reportHours(self):
        pass

class EmployeeSaver():
    def __init__(self,employee):
        self.employeeData = employee

    # データベースへの保存処理
    def save(self):
        pass

書いた感想(まとめ)

NGの例は一つのクラスに複数の異なる処理(互いに関係のない処理)が列挙されている。各々のメソッドを変更する理由があり、この例では変更する理由が3つある状態。
これがいけない!
一つの要素には一つの責任としなければならない
OKの例1、Employeeに一つのメソッドのみと変更
OKの例2、Employeeを共有データのクラスとして、そのインスタンスを持つ各処理を行うクラスを一つ一つ実装していく
いずれの例も一つのクラスに一つの責任になっている。

O:開放/閉鎖の原則(OCP)

ソフトウェアの構成要素は拡張のために開いていて、修正のために閉じていなければならない(参考サイト1より引用)

ソフトウェアの構成要素に機能拡張が発生した場合、既存のコードには修正を加えずに(閉じている)、新しくコードを追加するだけで対応できる(開いている)ようにする、ということです。※既存のコードは、バグがあった場合のみ修正する。(参考サイト1より引用)

NGの例

class Employee():
    def __init__(self):
        self.salary = 0

    # 給与計算処理
    def calculatePay(self,workingDays,position,housingAllowance,familyAllowance):
        if workingDays > 0:
            self.salary += workingDays * 8000
        
        if position == "課長":
            self.salary *= 1.2
            return
        elif position == "部長":
            self.salary *= 1.5
            return
        else:
            self.salary *= 1.0

        if housingAllowance:
            self.salary += 3000
            return
        
        if familyAllowance:
            self.salary += 2000
            return

if __name__=='__main__':
    tanaka = Employee()
    tanaka.calculatePay(workingDays=30,position="課長",housingAllowance=False,familyAllowance=False)
    print(tanaka.salary)

    yamada = Employee()
    yamada.calculatePay(workingDays=30,position="平社員",housingAllowance=True,familyAllowance=False)
    print(yamada.salary)

    suzuki = Employee()
    suzuki.calculatePay(workingDays=30,position="平社員",housingAllowance=False,familyAllowance=True)
    print(suzuki.salary)

OKの例

class Employee():
    def __init__(self):
        self.salary = 0

    # 給与計算処理
    def calculatePay(self,workingDays):
        if workingDays > 0:
            self.salary += workingDays * 8000
        
    #役職による給与計算処理
    def calculatePayByPosition(self,workingDays,position):
        self.calculatePay(workingDays)

        if position == "課長":
            self.salary *= 1.2
        elif position == "部長":
            self.salary *= 1.5
        else:
            self.salary *= 1.0
    
    #住宅手当による給与計算処理
    def calculatePayByHousingAllowance(self,workingDays,housingAllowance):
        self.calculatePay(workingDays)

        if housingAllowance:
            self.salary += 3000

    #家族手当による給与計算処理
    def calculatePayByFamilyAllowance(self,workingDays,familyAllowance):
        self.calculatePay(workingDays)

        if familyAllowance:
            self.salary += 2000

if __name__=='__main__':
    tanaka = Employee()
    tanaka.calculatePayByPosition(30,"課長")
    print(tanaka.salary)

    yamada = Employee()
    yamada.calculatePayByHousingAllowance(30,True)
    print(yamada.salary)

    suzuki = Employee()
    suzuki.calculatePayByFamilyAllowance(30,False)
    print(suzuki.salary)

書いた感想(まとめ)

想定したシーンは、勤務日数から計算するcalculatePayメソッドのみある状態で、役職や住宅手当、家族手当の計算を追加するというのを想定した。また、通常は役職手当、住宅手当、家族手当は複数付く場合が考えられるが、この例では一つのみ付くということにした。

開放/閉鎖の原則では、変更を加える際に、既存のコードを変更しないで、新たに追加することで実装する(実装できるようにする)。呼び出す側も既存のコードを編集することなく、追加することで実装できるようにする。
NGの例だとcalculatePayメソッドを編集しているし、加えて呼び出す側も引数の追加という形で編集が必要。
OKの例だとcalculatePayメソッドには触れることなく機能を追加できている。
このNGとOKの例は自分で書いたコード。NGのコードがダメダメ。問題の要件を満たせていない。例えば、家族手当が入った場合の給与を求めたいときに、課長であったら役職手当がついた給与が計算されてしまう。NGのコードは既存コードを編集していくことになるというのが分かりさえすればいいので、直す気が余り起きない。時間があったら直す。

I:インターフェース分離の原則(ISP)

利用者にとって不要なインターフェースに依存させてはいけない(参考サイト1より引用)

ここで言うインターフェースとは?

要素を利用する側に提供する仕様のこと。メソッドでいうと、メソッド名、パラメータ、戻り値の型など、そのメソッドは「何をするのか(What)」を示す部分がインターフェース(メソッドのシグニチャといいます)で、それを「どう実現するのか(How)」であるメソッドの実装部分は含みません。(参考サイト1より引用)

利用者にとって不要なインターフェースがあると、それだけ余計なリスクとコストが発生し、保守性を落とすことになる(参考サイト1より引用)

NGの例

先ほどのOKの例がNGのコード。呼び出し側がcalculatePayByPositionのみ使う場合は、calculatePayByHousingAllowanceとcalculatePayByFamilyAllowanceは余計なインターフェースということになる。

class Employee():
    def __init__(self):
        self.salary = 0

    # 給与計算処理
    def calculatePay(self,workingDays):
        if workingDays > 0:
            self.salary += workingDays * 8000
        
    #役職による給与計算処理
    def calculatePayByPosition(self,workingDays,position):
        self.calculatePay(workingDays)

        if position == "課長":
            self.salary *= 1.2
        elif position == "部長":
            self.salary *= 1.5
        else:
            self.salary *= 1.0
    
    #住宅手当による給与計算処理
    def calculatePayByHousingAllowance(self,workingDays,housingAllowance):
        self.calculatePay(workingDays)

        if housingAllowance:
            self.salary += 3000

    #家族手当による給与計算処理
    def calculatePayByFamilyAllowance(self,workingDays,familyAllowance):
        self.calculatePay(workingDays)

        if familyAllowance:
            self.salary += 2000

if __name__=='__main__':
    tanaka = Employee()
    tanaka.calculatePayByPosition(30,"課長")
    print(tanaka.salary)

    yamada = Employee()
    yamada.calculatePayByHousingAllowance(30,True)
    print(yamada.salary)

    suzuki = Employee()
    suzuki.calculatePayByFamilyAllowance(30,False)
    print(suzuki.salary)

OKの例

class Employee():
    def __init__(self):
        self.salary = 0

    # 給与計算処理
    def calculatePay(self,workingDays):
        if workingDays > 0:
            self.salary += workingDays * 8000

class EmployeePositon(Employee):

    def __init__(self):
        super().__init__()

    #役職による給与計算処理
    def calculatePayByPosition(self,workingDays,position):
        super().calculatePay(workingDays)

        if position == "課長":
            self.salary *= 1.2
        elif position == "部長":
            self.salary *= 1.5
        else:
            self.salary *= 1.0

class EmployeeHousingAllowance(Employee):
    def __init__(self):
        super().__init__()

    #住宅手当による給与計算処理
    def calculatePayByHousingAllowance(self,workingDays,housingAllowance):
        super().calculatePay(workingDays)

        if housingAllowance:
            self.salary += 3000

class EmployeeFamilyAllowance(Employee):
    def __init__(self):
        super().__init__()

     #家族手当による給与計算処理
    def calculatePayByFamilyAllowance(self,workingDays,familyAllowance):
        super().calculatePay(workingDays)

        if familyAllowance:
            self.salary += 2000

if __name__=='__main__':
    tanaka = EmployeePositon()
    tanaka.calculatePayByPosition(30,"課長")
    print(tanaka.salary)

    yamada = EmployeeHousingAllowance()
    yamada.calculatePayByHousingAllowance(30,True)
    print(yamada.salary)

    suzuki = EmployeeFamilyAllowance()
    suzuki.calculatePayByFamilyAllowance(30,False)
    print(suzuki.salary)

書いた感想(まとめ)

クラスを継承させて分けることでインターフェースを分離させた。これをすることでインターフェース分離の原則だけでなく、単一責任の原則も満たせていると思われる。
継承したクラスの名前がイケてない、思いつかなった。

L:リスコフの置換原則(LSP)

もし、SがTの派生型であれば、プログラム内でT型のオブジェクトが使われている箇所は全てS型のオブジェクトで置換可能にする(参考サイト1より引用)

派生オブジェクトAを同じ基底クラスを持つ派生オブジェクトBに置き換えても問題ないようにする。

NGの例

先ほどのOKの例がNGのコード。

class Employee():
    def __init__(self):
        self.salary = 0

    # 給与計算処理
    def calculatePay(self,workingDays):
        if workingDays > 0:
            self.salary += workingDays * 8000

class EmployeePositon(Employee):

    def __init__(self):
        super().__init__()

    #役職による給与計算処理
    def calculatePayByPosition(self,workingDays,position):
        super().calculatePay(workingDays)

        if position == "課長":
            self.salary *= 1.2
        elif position == "部長":
            self.salary *= 1.5
        else:
            self.salary *= 1.0

class EmployeeHousingAllowance(Employee):
    def __init__(self):
        super().__init__()

    #住宅手当による給与計算処理
    def calculatePayByHousingAllowance(self,workingDays,housingAllowance):
        super().calculatePay(workingDays)

        if housingAllowance:
            self.salary += 3000

class EmployeeFamilyAllowance(Employee):
    def __init__(self):
        super().__init__()

     #家族手当による給与計算処理
    def calculatePayByFamilyAllowance(self,workingDays,familyAllowance):
        super().calculatePay(workingDays)

        if familyAllowance:
            self.salary += 2000

if __name__=='__main__':
    tanaka = EmployeePositon()
    tanaka.calculatePayByPosition(30,"課長")
    print(tanaka.salary)

    yamada = EmployeeHousingAllowance()
    #置き換えたとき下記メソッドがなくエラーになる
    yamada.calculatePayByHousingAllowance(30,True)
    print(yamada.salary)

    suzuki = EmployeeFamilyAllowance()
    #置き換えたとき下記メソッドがなくエラーになる
    suzuki.calculatePayByFamilyAllowance(30,False)
    print(suzuki.salary)

OKの例

class Employee():
    def __init__(self,workingDays,position,housingAllowance,familyAllowance):
        self.salary = 0
        self.workingDays = workingDays
        self.position = position
        self.housingAllowance = housingAllowance
        self.familyAllowance = familyAllowance

    # 給与計算処理
    def calculatePay(self):
        if self.workingDays > 0:
            self.salary += self.workingDays * 8000

class EmployeePositon(Employee):

    #役職による給与計算処理
    def calculatePay(self):
        super().calculatePay()

        if self.position == "課長":
            self.salary *= 1.2
        elif self.position == "部長":
            self.salary *= 1.5
        else:
            self.salary *= 1.0

class EmployeeHousingAllowance(Employee):

    #住宅手当による給与計算処理
    def calculatePay(self):
        super().calculatePay()

        if self.housingAllowance:
            self.salary += 3000

class EmployeeFamilyAllowance(Employee):
    
     #家族手当による給与計算処理
    def calculatePay(self):
        super().calculatePay()

        if self.familyAllowance:
            self.salary += 2000

if __name__=='__main__':
    tanaka = EmployeePositon(30,"課長",False,False)
    tanaka.calculatePay()
    print(tanaka.salary)

    yamada = EmployeeHousingAllowance(30,"平社員",True,False)
    yamada.calculatePay()
    print(yamada.salary)

    suzuki = EmployeeFamilyAllowance(30,"平社員",False,True)
    suzuki.calculatePay()
    print(suzuki.salary)

書いた感想(まとめ)

リスコフの置換原則に則るためにメソッドに引数を渡していたのをクラスのコンストラクタで渡すように変えた。後クラス名を同じものに変えた。
呼び出し側が各クラスに個別にあるメソッド名を知らなくてもよくなった。
つまり、呼び出し側と呼び出される側の依存度が減った。
将来拡張するときも同じように実装すれば呼び出し側は特に新しいルールを覚えることなく利用できる。
つまり、拡張性がよくなった。

動画に則ってOKの例を更に改良する。現状だとEmployeeから直接継承しているので、クラスが増えたときに拡張する必要があるcalculatePayメソッド以外も継承されてしまい保守性が悪い(余計なインターフェースを継承することになり、インターフェース分離の原則に反すると言えると思う)。そのため下記のように書き換えた。計算メソッドを委譲して部品化する。

OKの例2

class Employee():
    def __init__(self,payCalculator):
        self.payCalculator = payCalculator

    # 給与計算処理
    def calculatePay(self):
        return self.payCalculator.calculatePay()

class PayCalculator():
    def __init__(self,workingDays,position,housingAllowance,familyAllowance):
        self.workingDays = workingDays
        self.position = position
        self.housingAllowance = housingAllowance
        self.familyAllowance = familyAllowance

     # 給与計算処理
    def calculatePay(self):
        if self.workingDays > 0:
            return self.workingDays * 8000

class PositonPayCalculator(PayCalculator):

    #役職による給与計算処理
    def calculatePay(self):
        salary = super().calculatePay()

        if self.position == "課長":
            salary *= 1.2
        elif self.position == "部長":
            salary *= 1.5
        else:
            salary *= 1.0
        
        return salary

class HousingAllowancePayCalculator(PayCalculator):

    #住宅手当による給与計算処理
    def calculatePay(self):
        salary = super().calculatePay()

        if self.housingAllowance:
            salary += 3000

        return salary

class FamilyAllowancePayCalculator(PayCalculator):
    
     #家族手当による給与計算処理
    def calculatePay(self):
        salary = super().calculatePay()

        if self.familyAllowance:
            salary += 2000

        return salary

if __name__=='__main__':
    tanaka = PositonPayCalculator(30,"課長",False,False)
    print(tanaka.calculatePay())

    yamada = HousingAllowancePayCalculator(30,"平社員",True,False)
    print(yamada.calculatePay())

    suzuki = FamilyAllowancePayCalculator(30,"平社員",False,True)
    print(suzuki.calculatePay())

    sudou = Employee(PayCalculator(30,"社長",True,True))
    print(sudou.calculatePay())

D:依存性逆転の原則(DIP)

抽象(上位要素)は詳細(下位要素)に依存してはならない。両方とも抽象に依存すべきである。(参考サイト1より引用)

NGの例

先ほどのOKの例がNGのコード。Employee(上位)がPayCalculator(下位)に依存している(PayCalculatorのcalculatePayメソッドに変更が加わると上位のcalculatePayに影響を及ぼす)。

class Employee():
    def __init__(self,payCalculator):
        self.payCalculator = payCalculator

    # 給与計算処理
    def calculatePay(self):
        return self.payCalculator.calculatePay()

class PayCalculator():
    def __init__(self,workingDays,position,housingAllowance,familyAllowance):
        self.workingDays = workingDays
        self.position = position
        self.housingAllowance = housingAllowance
        self.familyAllowance = familyAllowance

     # 給与計算処理
    def calculatePay(self):
        if self.workingDays > 0:
            return self.workingDays * 8000

class PositonPayCalculator(PayCalculator):

    #役職による給与計算処理
    def calculatePay(self):
        salary = super().calculatePay()

        if self.position == "課長":
            salary *= 1.2
        elif self.position == "部長":
            salary *= 1.5
        else:
            salary *= 1.0
        
        return salary

class HousingAllowancePayCalculator(PayCalculator):

    #住宅手当による給与計算処理
    def calculatePay(self):
        salary = super().calculatePay()

        if self.housingAllowance:
            salary += 3000

        return salary

class FamilyAllowancePayCalculator(PayCalculator):
    
     #家族手当による給与計算処理
    def calculatePay(self):
        salary = super().calculatePay()

        if self.familyAllowance:
            salary += 2000

        return salary

if __name__=='__main__':
    tanaka = PositonPayCalculator(30,"課長",False,False)
    print(tanaka.calculatePay())

    yamada = HousingAllowancePayCalculator(30,"平社員",True,False)
    print(yamada.calculatePay())

    suzuki = FamilyAllowancePayCalculator(30,"平社員",False,True)
    print(suzuki.calculatePay())

    sudou = Employee(PayCalculator(30,"社長",True,True))
    print(sudou.calculatePay())

OKの例

class Employee():
    def __init__(self,payCalculator):
        self.payCalculator = payCalculator

    # 給与計算処理
    def calculatePay(self):
        return self.payCalculator.calculatePay()

from abc import ABCMeta, abstractmethod
class PayCalculator(metaclass=ABCMeta):

     # 給与計算処理
    @abstractmethod
    def calculatePay(self):
        pass

class WorkingDaysPayCalculator(PayCalculator):
    def __init__(self,workingDays):
        self.workingDays = workingDays

    #役職による給与計算処理
    def calculatePay(self):
        salary = None

        if self.workingDays > 0:
            salary = self.workingDays * 8000
            
        return salary

class PositonPayCalculator(PayCalculator):
    def __init__(self,payCalculator,position):
        self.payCalculator = payCalculator
        self.position = position

    #役職による給与計算処理
    def calculatePay(self):
        salary = self.payCalculator.calculatePay()
        
        if self.position == "課長":
            salary *= 1.2
        elif self.position == "部長":
            salary *= 1.5
        else:
            salary *= 1.0
        
        return salary

class HousingAllowancePayCalculator(PayCalculator):
    def __init__(self,payCalculator,housingAllowance):
        self.payCalculator = payCalculator
        self.housingAllowance = housingAllowance

    #住宅手当による給与計算処理
    def calculatePay(self):
        salary = self.payCalculator.calculatePay()
        
        if self.housingAllowance:
            salary += 3000

        return salary

class FamilyAllowancePayCalculator(PayCalculator):
    def __init__(self,payCalculator,familyAllowance):
        self.payCalculator = payCalculator
        self.familyAllowance = familyAllowance

     #家族手当による給与計算処理
    def calculatePay(self):
        salary = self.payCalculator.calculatePay()
        
        if self.familyAllowance:
            salary += 2000

        return salary

if __name__=='__main__':
    tanaka = PositonPayCalculator(WorkingDaysPayCalculator(30),"課長")
    print(tanaka.calculatePay())

    yamada = HousingAllowancePayCalculator(WorkingDaysPayCalculator(30),True)
    print(yamada.calculatePay())

    suzuki = FamilyAllowancePayCalculator(WorkingDaysPayCalculator(30),True)
    print(suzuki.calculatePay())

    sudou = Employee(WorkingDaysPayCalculator(30))
    print(sudou.calculatePay())

    satou = FamilyAllowancePayCalculator(HousingAllowancePayCalculator(PositonPayCalculator(WorkingDaysPayCalculator(30),"課長"),True),True)
    print(satou.calculatePay())

書いた感想(まとめ)

動画に沿って作成したところPayCalculatorのみで実施していた勤務日数のみの給与計算がなくなった。そのため、新規に勤務日数のみの給与計算クラスを作成した。結果Decoratorパターンのようになった気がする。これで依存度の低い、保守性の高いコードになった気がする。

総括

動画の例が秀逸で沿って行くと段々とコードがよくなっていく様が見事だった。リファクタリングをしていく際の勉強になった。
最終的にDecoratorパターンのようになったことで気づいたが、この問題最初からDecoratorパターンで解けばよかったと思った。ベースとなる勤務日数での給与計算があり、他の計算方法がDecorationされると考えるとまさにDecoratorパターンである。結果的に出来上がったコードは、勤務日数による計算をして役職手当をつけて住宅手当を付けて家族手当も付けるということが可能になった。
こう考えるとSOLID原則とGoFのデザインパターンは別物ではなく、SOLID原則を適用していって出来た形がデザインパターンと言えるのではないだろうか。
デザインパターンを適用できれば一番楽だが、適用できないとき思いつかないときはSOLID原則を意識してコードを作成すれば綺麗なコードになると学んだ。

書いたコード

github

参考サイト

  1. youtube:変化に強いソフトウェアの設計法
  2. 自分なりにSOLIDの原則を理解する

フリーランスでpythonエンジニアとして働きたく勉強中。 前職は運用SEとセキュリティエンジニア。 開発は学校で学んだ経験と趣味でやっていた程度。一応前職でも業務の自動化のためにExcel VBAやセキュリティテスト用のWebサイトの開発やセキュリティテストツールの改良などで開発はやっていた。