[Clean Architecture] SOLID


SOLID


SRP: The Single Responsibility Principle


この章を勉強している間に、混乱した部分がありました.これは、ウェブページを検索する際に出てくるThe Single Responsibilityの定義が、本に記載されている定義とは異なるためです.
Webからの結果の多くは、次のように定義されています.
  • A module(source file) should be reponsible one, and only one, reason to change.
  • しかし、本には偽の定義が間違っていると書かれており、定義は以下の通りである.
  • A module(source file) should be responsible one, and only one, actor(user or stakeholder) to change
  • どちらの定義もロバート・マーティンによって定義されていますが、筆者の理解の仕方で説明すると、厳密に言えば、以下が正しい言い方だと思います.同じ理由で、1つのクラスの1つの関数、メソッド(機能)が2つの開発チームで共通に使用されている場合は、分離が必要です.逆に、2つの方法がある場合、それぞれの方法を使用する理由は異なりますが、同じチームまたは1人のユーザーだけが、同じ場合に使用する場合は、分離する必要はありません.
    しかし、実戦では時間が経つにつれてチームを分け、プロジェクトの拡大が俳優を増やすなど様々な変化をもたらす可能性があり、その場合もある程度予想し、構造設計を行う必要がある.また、1人でフルセット開発すれば、理性を考慮して設計するのが理想的な方向になります.俳優は1人しかいませんが、実際のコードは複数のチーム、複数の俳優がいるようなものです.
    本の中ではよく説明されていますが、サンプルコードが不十分なので、Googleのサンプルコードを添付して見たいと思います.

    SRP example 1.

    class Person:
        def __init__(self, name):
            self.name = name
    
        def __repr__(self):
            return f'Person(name={self.name})'
    
        @classmethod
        def save(cls, person):
            print(f'Save the {person} to the database')
    
    
    if __name__ == '__main__':
        p = Person('John Doe')
        Person.save(p)
    上のPerson類には2つの責任があります.
  • Manage the person’s property
  • Store the person in the database.
  • Personのプロパティを定義し、2つのストレージ・プロパティを指定する必要があります.
    したがって、次のように変更します.
    class Person:
        def __init__(self, name):
            self.name = name
    
        def __repr__(self):
            return f'Person(name={self.name})'
    
    
    class PersonDB:
        def save(self, person):
            print(f'Save the {person} to the database')
    
    
    if __name__ == '__main__':
        p = Person('John Doe')
    
        db = PersonDB()
        db.save(p)
    このように分離されたPersonクラスはPersonの属性を専門に管理することができ、PersonDBクラスはDBAチームによって記憶に関連する部分を専門に管理することができる.
    クラスは2つに分けられます.コードとクラスは別々にできますが、コードを次の場所に接続できます.( facade pattern )
    class PersonDB:
        def save(self, person):
            print(f'Save the {person} to the database')
    
    
    class Person:
        def __init__(self, name):
            self.name = name
            self.db = PersonDB() # PersonDB Class 를 가져올 것임
    
        def __repr__(self):
            return f'Person(name={self.name})'
    
        def save(self):
            self.db.save(person=self)
    
    
    if __name__ == '__main__':
        p = Person('John Doe')
        p.save()

    SRP example 2.

    class Car:
        prices={'BMW': 100000, 'Audi': 200000, 'Mercedes': 300000}
        def __init__(self, name):
    
            if name not in self.prices:
                print("Sorry, we don't have this car")
                return
    
            self.name = name
            self.price = self.prices[name]        
    
        def testDrive(self):
            print("Driving {}".format(self.name))
    
        def buy(self, cash):
            if cash < self.price:
                print("Sorry, you don't have enough money")
            else:
                print("Buying {}".format(self.name))
    
    if __name__ == '__main__':
        car = Car('BMW')
        car.buy(100000)
    以上のコードは3つの部分に分けることができます.
  • Manage the Car’s property
  • Test drive a Car
  • Buy a Car
  • 以下は参考にした文章の作者が上のコードを分けているので、何もきれいに感じられません.
    class Car:
        prices={'BMW':200000, 'Audi': 200000, 'Mercedes': 300000}
        def __init__(self, name):
            if name not in self.prices:
                print("Sorry, we don't have this car")
                return
    
            self.name = name
            self.price = self.prices[name]        
    
        def testDrive(self):
            
            print("Driving {}".format(self.name))
    
    class Finances:
        def buy(car, cash):
            if cash == car.price:
                print("Buying {}".format(car.name))
            elif cash > car.price/3:
                print("Buying {} on installments".format(car.name))
            else:
                print("Sorry, you don't have enough money")
    
    if __name__ == '__main__':
        car = Car('BMW')
        Finances.buy(car, 100000)
    筆者が上記のコードにも満足していない理由は以下の通りである.
  • Manage the Car’s property
  • Test drive a Car
  • Buy a Car
  • Manage、TestDrive、Buyを分けます.管理車の単位で管理とテストを行い、個別のディーラーでBuyの概念を行う.しかしTestDriveは単独のお客様ですか?消費者側でやっているのか、Buyもディーラーの立場ではなく、消費者側でやっているBuyなのか、さらにBuyのコードを見ると、ディーラーが処理している部分が正しいので、関数の命名ミスを間違っていると見なすべきで、あまり満足していない例ではありません.

    OCP: The Open-Closed Principle

  • A software artifact should be open for extension but closed for modification.
  • オープンクローズドの原則は、ソースコードは機能拡張ではオープンであるが、機能修正ではクローズドであるべきである.

    OCP example 1.

    """
    Open-Closed Principle
    Software entities(Classes, modules, functions) should be open for extension, not
    modification.
    """
    class Animal:
        def __init__(self, name: str):
            self.name = name
        
        def get_name(self) -> str:
            pass
    
    animals = [
        Animal('lion'),
        Animal('mouse')
    ]
    
    def animal_sound(animals: list):
        for animal in animals:
            if animal.name == 'lion':
                print('roar')
    
            elif animal.name == 'mouse':
                print('squeak')
    
    animal_sound(animals)
    
    """
    The function animal_sound does not conform to the open-closed principle because
    it cannot be closed against new kinds of animals.  If we add a new animal,
    Snake, We have to modify the animal_sound function.  You see, for every new
    animal, a new logic is added to the animal_sound function.  This is quite a
    simple example. When your application grows and becomes complex, you will see
    that the if statement would be repeated over and over again in the animal_sound
    function each time a new animal is added, all over the application.
    """
    
    animals = [
        Animal('lion'),
        Animal('mouse'),
        Animal('snake')
    ]
    
    def animal_sound(animals: list):
        for animal in animals:
            if animal.name == 'lion':
                print('roar')
            elif animal.name == 'mouse':
                print('squeak')
            elif animal.name == 'snake':
                print('hiss')
    
    animal_sound(animals)
    
    
    """
    어떻게 OCP를 적용해 볼 수 있을까??
    """
    class Animal:
        def __init__(self, name: str):
            self.name = name
        
        def get_name(self) -> str:
            pass
    
        def make_sound(self):
            pass
    
        def __repr__(self):
            return f'Animal(name={self.name})'
    
    
    class Lion(Animal):
        def make_sound(self):
            return 'roar'
    
    
    class Mouse(Animal):
        def make_sound(self):
            return 'squeak'
    
    
    class Snake(Animal):
        def make_sound(self):
            return 'hiss'
    
    
    def animal_sound(animals: list):
        for animal in animals:
            print(animal.make_sound())
    
    animals = [
        Lion('Lion'),
        Mouse('Mouse'),
        Snake('Snake')
    ]
    
    animal_sound(animals)
    
    """
    추가될 수 있는 요소들을 Animal에서 상속되게 하여 각 Class로 만들어주었다.
    
    Animal now has a virtual method make_sound. We have each animal extend the
    Animal class and implement the virtual make_sound method.
    Every animal adds its own implementation on how it makes a sound in the
    make_sound.  The animal_sound iterates through the array of animal and just
    calls its make_sound method.
    Now, if we add a new animal, animal_sound doesn’t need to change.  All we need
    to do is add the new animal to the animal array.
    animal_sound now conforms to the OCP principle.
    """
    

    OCP example 2.

    
    """
    또 다른 예시.
    Let’s imagine you have a store, and you give a discount of 20% to your favorite
    customers using this class: When you decide to offer double the 20% discount to
    VIP customers. You may modify the class like this:
    """
    
    class Discount:
        def __init__(self, customer, price):
            self.customer = customer
            self.price = price
    
        def give_discount(self):
                if self.customer == 'fav':
                    return self.price * 0.2
                if self.customer == 'vip':
                    return self.price * 0.4
    
    
    """
    새로운 타입의 고객이 추가될 경우 위의 코드가 수정되어야한다.
    타입별로 별도의 클래스를 만들고 Discount 의 부모로부터 상속받도록 하자.
    아래의 예시는 각 타입별로 묶는 것이 아니라 상속에 상속으로 구현했다.
    No, this fails the OCP principle. OCP forbids it. If we want to give a new
    percent discount maybe, to a diff.  type of customers, you will see that a new
    logic will be added.
    To make it follow the OCP principle, we will add a new class that will extend
    the Discount.  In this new class, we would implement its new behavior:
    """
    
    class Discount:
        def __init__(self, customer, price):
            self.customer = customer
            self.price = price
    
        def get_discount(self):
                return self.price * 0.2
    
    
    class VIPDiscount(Discount):
        def get_discount(self):
            return super().get_discount() * 2
    
    """
    If you decide 80% discount to super VIP customers, it should be like this:
    You see, extension without modification.
    """
    
    class SuperVIPDiscount(VIPDiscount):
        def get_discount(self):
            return super().get_discount() * 2

    OCP example 3.

    class Person:
        def __init__(self, name):
            self.name = name
    
        def __repr__(self):
            return f'Person(name={self.name})'
    
    
    class PersonStorage:
        def save_to_database(self, person):
            print(f'Save the {person} to database')
    
        def save_to_json(self, person):
            print(f'Save the {person} to a JSON file')
    
    
    if __name__ == '__main__':
        person = Person('John Doe')
        storage = PersonStorage()
        storage.save_to_database(person)
    上記の例では、次の形式を示します.
    PersonStorageには、save_to_databasesave_to_jsonの2つの方法がある.
    ただし、XMLファイルとして保存するコンテンツを追加するには、既存のクラスを変更して追加する必要があります.つまり、これはOCPの原則に反している.

    変更後、これらのサブはPersonStorageのクラスに格納することができる.
    from abc import ABC, abstractmethod
    
    
    class Person:
        def __init__(self, name):
            self.name = name
    
        def __repr__(self):
            return f'Person(name={self.name})'
    
    
    class PersonStorage(ABC):
        @abstractmethod
        def save(self, person):
            pass
    
    
    class PersonDB(PersonStorage):
        def save(self, person):
            print(f'Save the {person} to database')
    
    
    class PersonJSON(PersonStorage):
        def save(self, person):
            print(f'Save the {person} to a JSON file')
    
    
    class PersonXML(PersonStorage):
        def save(self, person):
            print(f'Save the {person} to a XML file')
    
    
    if __name__ == '__main__':
        person = Person('John Doe')
        storage = PersonXML()
        storage.save(person)

    LSP(Liskov Substitution Principle)

  • The Liskov substitution principle states that a child class must be substitutable for its parent class. Liskov substitution principle aims to ensure that the child class can assume the place of its parent class without causing any errors.
  • この規則は、親タイプのオブジェクトを子タイプのオブジェクトに変換する場合、操作に問題が発生しないことを示します.つまり、BがAのサブアイテムである場合、Aタイプを使用している部分からBに変換された場合、それは問題ないはずです.

    LSP example 1.

    from abc import ABC, abstractmethod
    
    class Notification(ABC):
        @abstractmethod
        def notify(self, message, email):
            pass
    
    class Email(Notification):
        def notify(self, message, email):
            print(f'Send {message} to {email}')
    
    class SMS(Notification):
        def notify(self, message, phone):
            print(f'Send {message} to {phone}')
    
    
    if __name__ == '__main__':
        notification = SMS()
        notification.notify('Hello', '[email protected]')
    
    # 실행결과
    # Send Hello to [email protected]
    構造から離れて、コード自体にもエラーがあります.SMSを番号で送るつもりでしたが、メールで入力した入力が効いています.

    LSP example 2.


    正方形と長方形の例が追加されます.

    ISP: Interface Segregation Principle


    インタフェース分離の原則(The Interface Segregation Principle)とは、クライアントが不要なインタフェースに依存する必要がないことを意味する.

    ISP example 1.


    from abc import ABC, abstractmethod
    
    
    class Vehicle(ABC):
        @abstractmethod
        def go(self):
            pass
    
        @abstractmethod
        def fly(self):
            pass
            
    class Aircraft(Vehicle):
        def go(self):
            print("Taxiing")
    
        def fly(self):
            print("Flying")
    
    class Car(Vehicle):
        def go(self):
            print("Going")
    
        def fly(self):
            raise Exception('The car cannot fly')
          
    車がflyを必要としなくても、不要な方法が含まれています.次の構造に変更できます.
    class Movable(ABC):
        @abstractmethod
        def go(self):
            pass
    
    
    class Flyable(Movable):
        @abstractmethod
        def fly(self):
            pass
    
    class Aircraft(Flyable):
        def go(self):
            print("Taxiing")
    
        def fly(self):
            print("Flying")
            
    class Car(Movable):
        def go(self):
            print("Going")
    
    上記の例から見ると、インタフェースは小さく保つべきであり、勝手に結合することは困難になる可能性がある.使用不可能なインタフェースを構成することなく、特定の、小さなインタフェースを実装する必要があります.

    DIP: The Dependency Inversion Principle


  • 依存関係の原則

  • High-level modules should not depend on the low-level modules. Both should depend on abstractions.

  • Abstractions should not depend on details. Details should depend on abstractions.
  • すなわち、親モジュールはサブモジュールの実装に依存することはできず、親モジュールとサブモジュールは抽象的な内容に依存しなければならない.

    DIP example 1

    class FXConverter:
        def convert(self, from_currency, to_currency, amount):
            print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
            return amount * 1.2
    
    
    class App:
        def start(self):
            converter = FXConverter()
            converter.convert('EUR', 'USD', 100)
    
    
    if __name__ == '__main__':
        app = App()
        app.start()
    
    # 실행결과
    # 100 EUR = 120.0 USD
    上のコードに何か問題がありますか?

    現在の状況では、FXConverterが変更された場合、またはFXConverterではなく他のAPIを使用したい場合は、Appクラスを変更する必要があります.
    このようなことを防止するためには,依存性の変更(逆依存性)が必要である.
    Appは、FXConverterではなく抽象化されたクライアント(インタフェース)に依存するように、上記の構造に変更された.
    from abc import ABC
    
    class CurrencyConverter(ABC): # 추상화 클래스 (Interface)
        def convert(self, from_currency, to_currency, amount) -> float:
            pass
    
    
    class FXConverter(CurrencyConverter):
        def convert(self, from_currency, to_currency, amount) -> float:
            print('Converting currency using FX API')
            print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
            return amount * 1.15
    
    
    class AlphaConverter(CurrencyConverter):
        def convert(self, from_currency, to_currency, amount) -> float:
            print('Converting currency using Alpha API')
            print(f'{amount} {from_currency} = {amount * 1.2} {to_currency}')
            return amount * 1.2
    
    
    class App:
        def __init__(self, converter: CurrencyConverter): # App이 추상화 클래스를 참고함
            self.converter = converter
    
        def start(self):
            self.converter.convert('EUR', 'USD', 100) # 추상화 클래스에 의존하여 메소드 호출
    
    
    if __name__ == '__main__':
        converter = AlphaConverter() # 추상화 된 클래스를 통해 어떤 API를 이용할지
        app = App(converter)
        app.start()
        
    # 실행결과
    # Converting currency using Alpha API
    # 100 EUR = 120.0 USD

    DIP example 2

    class XMLHttpService(XMLHttpRequestService):
        pass
    
    class Http:
        def __init__(self, xml_http_service: XMLHttpService):
            self.xml_http_service = xml_http_service
        
        def get(self, url: str, options: dict):
            self.xml_http_service.request(url, 'GET')
    
        def post(self, url: str, options: dict):
            self.xml_http_service.request(url, 'POST')
    Httpの高レベルコンポーネントでは、XMLHttpServiceの低レベルコンポーネントに依存する.
    したがって、これは次のように変更できます.
    from abc import ABC
    
    class Connection(ABC):
        def request(self, url: str, options: dict):
            raise NotImplementedError
    
    class XMLHttpService(Connection):
        xhr = XMLHttpRequest()
    
        def request(self, url: str, options:dict):
            self.xhr.open()
            self.xhr.send()
    
    class NodeHttpService(Connection):
        def request(self, url: str, options:dict):
            pass
    
    class MockHttpService(Connection):
        def request(self, url: str, options:dict):
            pass
    
    
    class Http:
        def __init__(self, http_connection: Connection):
            self.http_connection = http_connection
        
        def get(self, url: str, options: dict):
            self.http_connection.request(url, 'GET')
    
        def post(self, url: str, options: dict):
            self.http_connection.request(url, 'POST')
    
    if __name__ == '__main__':
        httpRequest = NodeHttpService() # 추상화 된 클래스를 통해 어떤 http를 이용할지
        http = Http(httpRequest)
        http.get()
    その他のサンプルリンク

  • LSP
    https://brownbears.tistory.com/579
    この文章には理解できないところがある.

  • OCP
    https://www.ezzeddinabdullah.com/posts/solid-principles-ocp-py
  • リファレンス
    https://www.pythontutorial.net/python-oop/
    =>ここではSOLID原則のほかに、他を見る必要があります.
    https://github.com/doorBW/python_clean_code
    =>ここにはいくつかの例があります
    https://docs.python.org/3/library/abc.html
    =>知っておく必要がありますが、勝手に見ているだけです.
    https://github.com/heykarimoff/solid.python
    https://www.geeksforgeeks.org/facade-method-python-design-patterns/