[Clean Architecture] SOLID
76225 ワード
SOLID
SRP: The Single Responsibility Principle
この章を勉強している間に、混乱した部分がありました.これは、ウェブページを検索する際に出てくるThe Single Responsibilityの定義が、本に記載されている定義とは異なるためです.
Webからの結果の多くは、次のように定義されています.
しかし、実戦では時間が経つにつれてチームを分け、プロジェクトの拡大が俳優を増やすなど様々な変化をもたらす可能性があり、その場合もある程度予想し、構造設計を行う必要がある.また、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つの責任があります.したがって、次のように変更します.
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つの部分に分けることができます.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)
筆者が上記のコードにも満足していない理由は以下の通りである.OCP: The Open-Closed Principle
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_database
、save_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)
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/
Reference
この問題について([Clean Architecture] SOLID), 我々は、より多くの情報をここで見つけました https://velog.io/@jungbumwoo/Clean-Architecture-SOLIDテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol