일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 자료구조 및 실습
- 1단계
- ssd
- test-helper
- MySQL
- 실전알고리즘
- STL
- CS231n
- Python
- 머신러닝
- 코드수행
- 프로그래머스
- pytorch
- 파이썬
- 3단계
- 딥러닝
- C++
- 백준
- 모두를 위한 딥러닝 강좌 시즌1
- Object detection
- cs
- 그리디
- SWEA
- docker
- 2단계
- 이것이 코딩테스트다 with 파이썬
- AWS
- 전산기초
- ubuntu
- 구현
- Today
- Total
곰퓨타의 SW 이야기
CH 4. SOLID 원칙 본문
SOLID
객체 지향 소프트웨어 설계 (OOP)의 핵심 5가지 원
S : 단일 책임 원칙 (Single responsibility principle)
O : 개방 / 폐쇄의 원칙 (Open/closed principle)
L : 리스코프 (Liskov) 치환 원칙 (Liskov's substitution principle)
I : 인터페이스 분리 원칙 (Interface segregation principle)
D : 의존성 역전 원칙 (Dependency inversion principle)
파이썬은 유연한 언어이기 때문에 항상 따를 필요는 없다.
1. 단일 책임 원칙 (SRP - Single Responsibility Principle)
컴포넌트 (클래스)가 단 하나의 책임을 져야 한다.
신(god) 객체 : 필요 이상의 일을 하거나 너무 많은 것을 알고 있는 객체 -> 유지보수 어려움.
너무 많은 책임을 가진 클래스
ex ) 로그 파일이나 데이터 베이스와 같은 소스에서 이벤트의 정보를 읽어서 로그별로 필욯나 액션을 분류하는 애플리케이션 만들기
"""Clean Code in Python - Chapter 4, The SOLID Principles
> SRP: Single Responsibility Principle
"""
class SystemMonitor:
def load_activity(self):
"""Get the events from a source, to be processed."""
def identify_events(self):
"""Parse the source raw data into events (domain objects)."""
def stream_events(self):
"""Send the parsed events to an external agent."""
문제 : 독립적인 동작을 하는 메서드를 하나의 인터페이스에 정의
책임 분산
솔루션을 관리하기 쉽도록 모든 메서드를 다른 클래스로 분리하여 각 클래스마다 단일 책임을 갖게하기
각각의 클래스는 나머지와 독립적인 특정한 메서드를 캡슐화한 상태이므로 수정이 필요하더라도 나머지 객체에 영향 X
▶ 담당해야 하는 로직이 맞다면 한 클래스에 여러 메서드를 가질 수 있다.
2. 개방/폐쇄 원칙 (OCP - Open/Close Principle)
모듈이 개방되어 있으면서도 폐쇄되어야 한다.
클래스 디자인 시, 유지보수가 쉽도록 로직을 캡슐화하여 확장에는 개방되고 수정에는 폐쇄되도록 한다.
-> 로직을 캡슐화(추상화)하여, 확장에 용이성을 확보하고 수정에는 보수적으로 한다.
이는 다형성의 효과적인 사용과 관련이 있다.
개방/폐쇄 원칙을 따르지 않을 경우 유지보수의 어려움
ex ) 다른 시스템에서 발생하는 이벤트 분류하는 기능
새로운 유형을 판단하는 로직은 SystemMonitor의 identify 안에서 이뤄지기 때문에 SystemMonitor는 새로운 유형의 이벤트에 완전히 종속되어 있다.
해결방법
"""Clean Code in Python - Chapter 4
The open/closed principle
Counter-example of the open/closed principle.
An example that does not comply with this principle and should be refactored.
"""
from dataclasses import dataclass
@dataclass
class Event:
raw_data: dict
class UnknownEvent(Event):
"""데이터 만으로 식별할 수 없는 이벤트"""
class LoginEvent(Event):
"""로그인 사용자에 의한 이벤트"""
class LogoutEvent(Event):
"""로그아웃 사용자에 의한 이벤트"""
class SystemMonitor:
"""시스템에서 발생한 이벤트 분류
>>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
>>> l1.identify_event().__class__.__name__
'LoginEvent'
>>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}})
>>> l2.identify_event().__class__.__name__
'LogoutEvent'
>>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}})
>>> l3.identify_event().__class__.__name__
'UnknownEvent'
"""
def __init__(self, event_data):
self.event_data = event_data
def identify_event(self):
if (
self.event_data["before"]["session"] == 0
and self.event_data["after"]["session"] == 1
):
return LoginEvent(self.event_data)
elif (
self.event_data["before"]["session"] == 1
and self.event_data["after"]["session"] == 0
):
return LogoutEvent(self.event_data)
return UnknownEvent(self.event_data)
이 다자인의 문제점
- 이벤트 유형을 결정하는 로직이 단일 메서드에 중앙 집중화된다.
- 이 메서드가 수정을 위해 닫히지 않았다. 새로운 유형의 이벤트를 시스템에 추가할 때마다 elif로 메서드를 수정해야 한다.
확장성을 가진 이벤트 시스템으로 리팩토링
SystemMonitor 클래스가 분류하려는 구체 클래스와 직접 상호작용한다는 문제점이 있었다.
-> SystemMonitor 클래스를 추상적인 이벤트와 협력하도록 변경하고, 이벤트에 대응하는 개별 로직은 각 이벤트 클래스에 위임!
"""Clean Code in Python - Chapter 4
The open/closed principle.
Example with the corrected code, in order to comply with the principle.
"""
from dataclasses import dataclass
@dataclass
class Event:
raw_data: dict
@staticmethod
def meets_condition(event_data: dict) -> bool:
return False
class UnknownEvent(Event):
"""데이터만으로 식별할 수 없는 이벤트"""
class LoginEvent(Event):
@staticmethod
def meets_condition(event_data: dict):
return (
event_data["before"]["session"] == 0
and event_data["after"]["session"] == 1
)
class LogoutEvent(Event):
@staticmethod
def meets_condition(event_data: dict):
return (
event_data["before"]["session"] == 1
and event_data["after"]["session"] == 0
)
class SystemMonitor:
"""시스템에서 발생한 이벤트 분류
>>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
>>> l1.identify_event().__class__.__name__
'LoginEvent'
>>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}})
>>> l2.identify_event().__class__.__name__
'LogoutEvent'
>>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}})
>>> l3.identify_event().__class__.__name__
'UnknownEvent'
"""
def __init__(self, event_data):
self.event_data = event_data
def identify_event(self):
for event_cls in Event.__subclasses__():
try:
if event_cls.meets_condition(self.event_data):
return event_cls(self.event_data)
except KeyError:
continue
return UnknownEvent(self.event_data)
이 인터페이스를 따르는 제네릭들은 모두 meets_condition 메서드를 구현하여 다형성을 보장한다.
이벤트 시스템 확장
모니터링하는 시스템에서 새로운 트렌젝션이 실행되었음을 알려주는 이벤트가 추가되었다고 가정하자.
TransactionEvent라는 새로운 클래스를 추가하고 meets_condition 메서드도 구현하면, 기존 코드가 예상한대로 잘 동작한다.
class TransactionEvent(Event):
"""시스템에서 발생한 트랜젝션 이벤트"""
@staticmethod
def meets_condition(event_data: dict):
return event_data["after"].get("transaction") is not None
"""
>>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
>>> l1.identify_event().__class__.__name__
'LoginEvent'
>>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}})
>>> l2.identify_event().__class__.__name__
'LogoutEvent'
>>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}})
>>> l3.identify_event().__class__.__name__
'UnknownEvent'
>>> l4 = SystemMonitor({"after": {"transaction": "Tx001"}})
>>> l4.identify_event().__class__.__name__
'TransactionEvent'
"""
3. 리스코프 치환 원칙 (LSP - Liskov Substitution Principle)
안정성을 위해 객체타입이 유지해야하는 특성
어떤 클래스라도 하위 타입을 사용할 수 있어야 한다.
S가 T의 하위 타입이면 프로그램을 변경하지 않고 T 타입의 객체를 S 타입의 객체로 치환 가능해야 한다.
-> 하위 클래스는 상위 클래스에서 정의한 계약을 따르도록 디자인해야 한다.
도구를 사용해 LSP 문제 검사
- mypy로 잘못된 메서드 서명 검사
- 파생 클래스가 부모 클래스에서 정의한 파라미터와 다른 타입을 사용한 경
class Event:
...
def meets_condition(self, event_data: dict) -> bool:
return False
class LoginEvent(Event):
def meets_condition(self, event_data: list) -> bool:
return bool(event_data)
# error: Argument 1 of "meets_condition" incompatible with supertype "Event"
- pylint로 호환되지 않은 서명 검사
- 메서드의 서명 자체가 다른 경우
"""Clean Code in Python - Chapter 4, The SOLID Principles
> Liskov's Substitution Principle (LSP)
Detecting violations of LSP through tools (mypy, pylint, etc.)
"""
class Event:
...
def meets_condition(self, event_data: dict) -> bool:
return False
class LoginEvent(Event):
def meets_condition(self, event_data: list) -> bool:
return bool(event_data)
class LogoutEvent(Event):
def meets_condition(self, event_data: dict, override: bool) -> bool:
if override:
return True
...
# Parameters differ from overridden 'meets_condition' method (argumentsdiffer)
애매한 LSP 위반 사례
- 하위 클래스는 부모 클래스에 정의된 것보다 사전 조건을 엄격하게 만들면 안된다.
- 하위 클래스는 부모 클래스에 정의된 것보다 약한 사후 조건을 만들면 안된다.
ex ) event_data가 올바른 조건인지 사전 검사
-> event data가 "before", "after" key를 필수로 갖고 각각의 값이 사전 타입인지 확인
"""Clean Code in Python - Chapter 4, The SOLID Principles
Liskov's Substitution Principle (LSP)
Detecting violations of LSP on methods that violate the contract defined.
* Events are defined by a contract (DbC, Design by Contract).
* The contract precondition is exercised only once, and after that the
``SystemMonitor`` should be able to work with any of them interchangeably.
"""
from collections.abc import Mapping
class Event:
def __init__(self, raw_data):
self.raw_data = raw_data
@staticmethod
def meets_condition(event_data: dict) -> bool:
return False
@staticmethod
def validate_precondition(event_data: dict):
"""인터페이스 계약의 사전 조건
'event_data'' 파라미터가 적절한 형태인지 유효성 검사
"""
if not isinstance(event_data, Mapping):
raise ValueError(f"{event_data!r} is not a dict")
for moment in ("before", "after"):
if moment not in event_data:
raise ValueError(f"{moment} not in {event_data}")
if not isinstance(event_data[moment], Mapping):
raise ValueError(f"event_data[{moment!r}] is not a dict")
class UnknownEvent(Event):
"""A type of event that cannot be identified from its data"""
class LoginEvent(Event):
@staticmethod
def meets_condition(event_data: dict) -> bool:
return (
event_data["before"].get("session") == 0
and event_data["after"].get("session") == 1
)
class LogoutEvent(Event):
@staticmethod
def meets_condition(event_data: dict) -> bool:
return (
event_data["before"].get("session") == 1
and event_data["after"].get("session") == 0
)
class TransactionEvent(Event):
"""Represents a transaction that has just occurred on the system."""
@staticmethod
def meets_condition(event_data: dict) -> bool:
return event_data["after"].get("transaction") is not None
class SystemMonitor:
"""시스템에서 발생한 이벤트 분류
>>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
>>> l1.identify_event().__class__.__name__
'LoginEvent'
>>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}})
>>> l2.identify_event().__class__.__name__
'LogoutEvent'
>>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}})
>>> l3.identify_event().__class__.__name__
'UnknownEvent'
>>> l4 = SystemMonitor({"before": {}, "after": {"transaction": "Tx001"}})
>>> l4.identify_event().__class__.__name__
'TransactionEvent'
"""
def __init__(self, event_data):
self.event_data = event_data
def identify_event(self):
# 올바른 이벤트 유형 탐지하기 전에 사전조건 검사
Event.validate_precondition(self.event_data)
event_cls = next(
(
event_cls
for event_cls in Event.__subclasses__()
if event_cls.meets_condition(self.event_data)
),
UnknownEvent,
)
return event_cls(self.event_data)
4. 인터페이스 분리 원칙 (ISP - Interface Segregation Principle)
인터페이스 ? (객체 지향 용어) 객체가 제공하는 Method의 집합
파이썬의 인터페이스는 클래스 Method의 형태를 보고 암시적으로 정의됨 (duck typing)
=> duck typing ? "어떤 새가 오리처럼 걷고 오리처럼 꽥꽥 소리를 낸다면 오리여야ㅎ만 한다."
추상 기본 클래스 (abstract base class)?
파생 클래스가 반드시 구현해야 할 것들을 명시적으로 가리키기 위한 유용하고 강력한 도구
@abstractmethod 데코레이터로 마킹한 메서드는 반드시 파생 클래스에서 모두 구현을 해야만 인스턴스화가 가능하다.
너무 많은 일을 하는 인터페이스
ex ) 데이터 소스에서 이벤트를 파싱하는 인터페이스를 가정
이 추상 클래스를 상속한 이벤트는 구체적인 유형의 이벤트를 처리할 수 있도록 이 메서드들을 구현해야만 한다.
하지만 어떤 클래스는 XML 메서드를 필요로 하지 않고 JSON 메서드만 필요로 할 수도 있다.
-> 결합력을 높이고 유연성을 떨어뜨린다.
인터페이스는 작을수록 좋다.
위의 경우 하나의 메서드를 가진 두 개의 다른 인터페이스로 분리하는 것이 좋다.
구현한 코드
"""Clean Code in Python - Second edition
Chapter 04 - The SOLID Principles
ISP: Interface Segregation Principle
"""
from abc import ABCMeta, abstractmethod
class XMLEventParser(metaclass=ABCMeta):
@abstractmethod
def from_xml(xml_data: str):
"""XML 형태의 데이터를 파싱"""
class JSONEventParser(metaclass=ABCMeta):
@abstractmethod
def from_json(json_data: str):
"""JSON 형태의 데이터를 파싱"""
class EventParser(XMLEventParser, JSONEventParser):
"""XML과 JSON 형태의 데이터를 파싱"""
def from_xml(xml_data):
pass
def from_json(json_data: str):
pass
이는 SRP와 유사하지만 ISP는 인터페이스에 대해 이야기 하고 있다는 점이 차이점이다.
5. 의존성 역전 (DIP - Dependency Inversion Principle)
코드가 손상되는 취약점으로부터 보호해주는 원칙
추상화를 통해 세부 사항에 의존하지 않도록 해야 하지만, 반대로 세부 사항 (구체적인 구현)은 추상화에 의존해야 한다.
일반적으로 구체적인 구현은 Abstract Component 보다 자주 바뀜
(변경될 가능성이 있기 때문에 유연성을 확보하기 위해 추상화를 사용하는 것)
강한 의존성을 가진 예
이벤트 모니터링 시트템의 마지막 부분은 식별된 이벤트를 데이터 수집기로 전달하여 분석하는 것이었다. 데이터를 목표지에 전송하는 이벤트 전송 클래스 Syslog 을 만들었다고 가정.
이는 저수준의 내용에 따라 고수준의 클래스가 변경되어야 하므로 좋지 않다.
Syslog로 데이터를 보내는 방식이 변경됨녀 EventStreamer를 수정해야 한다.
만약 다른 데이터에 대해서는 전송 목적지를 변경하거나 새로운 데이터를 추가하려면 stream() method를 요구사항에 따라 지속적으로 수정해야 한다. (문제!)
의존성을 거꾸로
EventStream를 구체 클래스가 아닌 인터페이스와 대화하도록 수정하는 것이 좋다.
-> 세부 구현사항을 가진 저수준 클래스가 담당하게 된다.
EventStreamer는 특정 데이터 대상의 구체적인 구현과 관련이 없어졌다.
구현 내용이 바뀌어도 수정할 필요가 없다.
실제 인터페이스를 정확하게 구현하고 변화를 수용하는 것은 인터페이스 구현체가 담당한다.
이는 런타임 중에도 send() 메서드를 구현한 객체의 프로퍼티를 수정해도 잘 동작한다.
-> 의존성 주입 (dependency injection) : 의존성을 동적으로 제공
인터페이스 정의의 장점
is a 관계 : The apple is fruit : apple과 friut은 상속 관계 -> 가독성이 높아진다.
의존성 주입 (Dependency injection)
"""Clean Code in Python - Second edition
Chapter 04 - The SOLID Principles
DIP: Dependency Inversion Principle
Demo of the dependency injection section.
"""
from __future__ import annotations
import json
from abc import ABCMeta, abstractmethod
class Event:
def __init__(self, content: dict) -> None:
self._content = content
def serialise(self):
return json.dumps(self._content)
class DataTargetClient(metaclass=ABCMeta):
@abstractmethod
def send(self, content: bytes):
"""Send raw content to a particular target."""
class Syslog(DataTargetClient):
def send(self, content: bytes):
return f"[{self.__class__.__name__}] sent {len(content)} bytes"
class EventStreamer:
def __init__(self, target: DataTargetClient):
# self._target = Syslog() 처럼 필요로 하는 것들을 직접 관리하지 말고 제공 받기
self.target = target # 다형성을 지원 받음
def stream(self, events: list[Event]) -> None:
for event in events:
self.target.send(event.serialise())
__init__ method 에서 의존성이 있는 것들을 직접 생성하지 않도록 하자. 대신 의존성을 파라미터로 전달하도록 하여 보다 유게 대처 가능하도록 하자.
만약 복잡한 초기화 과정을 가졌다거나 초기화 인자가 많은 경우라면, 종속성 그래프를 만들고 관련 라이브러리가 생성을 담당하도록 한다.
-> 객체를 연결하기 위한 글루 코드 (glue code)에서 보일러플레이드(boilderplate)코드를 제거할 수 있다.
glue code : 프로그램의 기본 동작과는 관련이 없짐나 프로그램 구성 요소 간의 호환성을 위해 접착제 역할을 하는 코드
ex ) pinject 라이브러리를 적용한 경우
"""Clean Code in Python - Second edition
Chapter 04 - The SOLID Principles
DIP: Dependency Inversion Principle
Demo of the dependency injection section using a library
"""
from __future__ import annotations
import json
from abc import ABCMeta, abstractmethod
import pinject
class Event:
def __init__(self, content: dict) -> None:
self._content = content
def serialise(self):
return json.dumps(self._content)
class DataTargetClient(metaclass=ABCMeta):
@abstractmethod
def send(self, content: bytes):
"""Send raw content to a particular target."""
class Syslog(DataTargetClient):
def send(self, content: bytes):
return f"[{self.__class__.__name__}] sent {len(content)} bytes"
class EventStreamer:
def __init__(self, target: DataTargetClient):
self.target = target
def stream(self, events: list[Event]) -> None:
for event in events:
self.target.send(event.serialise())
# pinject 라이브러리 사용
# binding specification : 의존성 주입이 필요한 클래스에 의존성이 어떻게 주입될 지 결정하는 객체 정의
class _EventStreamerBindingSpec(pinject.BindingSpec):
# provide_<depencdnecy> : <dependency>와 같은 이름의 변수에 대해 의존성 반환
# 이 경우에는 Syslog() 반환
def provide_target(self):
return Syslog()
object_graph = pinject.new_object_graph(
binding_specs=[_EventStreamerBindingSpec()]
)
# EventStreamer의 생성자 파라미터 중 target에 대해서
# Syslog()객체를 사용해야 한다고 알려주는 스펙의 역할을 한다.
만약 설정해야 할 의존성이 많고, 객체 간의 관계가 복잡하담녀 명시적으로 관계를 선언하고, 도구에서 초기화하는 것이 좋다 -> 팩토리 객체와 유사
Reference
'TIL > 클린코드' 카테고리의 다른 글
CH 6. 디스크립터로 더 멋진 객체 만들기 (0) | 2023.06.26 |
---|---|
CH 5. 데코레이터를 사용한 코드 개선 (0) | 2023.05.16 |
CH 3. 좋은 코드의 일반적인 특징 (0) | 2023.05.07 |
CH 2. 파이썬스러운 코드 (0) | 2023.05.02 |
CH1. 코드 포매팅과 도구 (0) | 2023.04.26 |