데이터과학 삼학년

SOLID-python 원칙 : clean code 본문

Python

SOLID-python 원칙 : clean code

Dan-k 2023. 7. 5. 12:00
반응형

SOLID Principles : improve Object-Oriented Design in Python

SOLID 원칙

- oop ?! → 간단히 말해 pyhton의 class 기능 → 붕어빵 틀을 만들어 여러 붕어빵을 찍어내는 것과 같은 것

- 코드를 보다 효율적이고 유지보수하기 쉬우며, 효과적으로 작성하고자 만든 어떤 원칙이 있는데…대표적인 것이 SOLID 원칙

- SOLID 원칙별로 한글자씩 따서 만든 것으로 생각한 것 처럼 총 5가지 원칙이 있음

S**ingle-responsibility principle (SRP)

O**pen–closed principle (OCP)

L**iskov substitution principle (LSP)

I**nterface segregation principle (ISP)

D**ependency inversion principle (DIP)

Single-responsibility principe (단일 책임 원칙)

- 한 클래스는 하나의 책임만 가져야 한다. → 목적 기능단위로 class를 만드는 것!

- Filemanger라는 class가 있다면 내부 method에 read, write, compress, decompress 가 있다면 단일 책임 원칙을 위배한 거로 볼 수 있음

- 다음과 같이 두개의 class로 나누어 만드는 것이 보다 나음

- Filemanager : write, read

- ZipFilemanager : compress, decompress

# file_manager_srp.py

from pathlib import Path
from zipfile import ZipFile

class FileManager:
    def __init__(self, filename):
        self.path = Path(filename)

    def read(self, encoding="utf-8"):
        return self.path.read_text(encoding)

    def write(self, data, encoding="utf-8"):
        self.path.write_text(data, encoding)

class ZipFileManager:
    def __init__(self, filename):
        self.path = Path(filename)

    def compress(self):
        with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:
            archive.write(self.path)

    def decompress(self):
        with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:
            archive.extractall()

Open-closed principle (개방 폐쇄 원칙)

- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

- 메서드를 추가할 수는 있으나, 수정하는 일은 없어야 함

- 객체를 상속받아 여러 class를 만드는 것은 가능하지만, class자체의 변경이 일어나는 것은 지양

- 아래와 같이 도형 모양을 만드는 class가 있을때, 사각형이던 원이던 만들수가 있긴 함

- 그러나 직사각형, 사다리꼴을 만들고 싶다면?! → 내부 method를 모두 수정해야함…elif 같은 조건을 써서…

# shapes_ocp.py

from math import pi

class Shape:
    def __init__(self, shape_type, **kwargs):
        self.shape_type = shape_type
        if self.shape_type == "rectangle":
            self.width = kwargs["width"]
            self.height = kwargs["height"]
        elif self.shape_type == "circle":
            self.radius = kwargs["radius"]

    def calculate_area(self):
        if self.shape_type == "rectangle":
            return self.width * self.height
        elif self.shape_type == "circle":
            return pi * self.radius**2

- 그런식으로 class내 불필요한 기능을 확장할 필요는 없어

- 차라리 Shape라는 기본 껍데기 class를 상속받아, 원, 사각형, 사다리꼴 등 class를 확장하는 것이 나음

# shapes_ocp.py

from abc import ABC, abstractmethod
from math import pi

class Shape(ABC):
    def __init__(self, shape_type):
        self.shape_type = shape_type

    @abstractmethod
    def calculate_area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        super().__init__("circle")
        self.radius = radius

    def calculate_area(self):
        return pi * self.radius**2

class Rectangle(Shape):
    def __init__(self, width, height):
        super().__init__("rectangle")
        self.width = width
        self.height = height

    def calculate_area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def calculate_area(self):
        return self.side**2
반응형

Liskov subsituition principle (리스코프 치환 원칙)

- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀수 있어야한다.

- 상속받은 객체인 자식 객체는 부모 객체를 완전히 대체해도 아무런 문제가 없어야한다.

- 직사각형이라는 class가 있고, 정사각형이라는 class를 직사각형 부모class로 상속받아 만든다면

# shapes_lsp.py

# ...

class Square(Rectangle):
    def __init__(self, side):
        super().__init__(side, side)

    def __setattr__(self, key, value):
        super().__setattr__(key, value)
        if key in ("width", "height"):
            self.__dict__["width"] = value
            self.__dict__["height"] = value

>>> from shapes_lsp import Square

>>> square = Square(5)
>>> vars(square)
{'width': 5, 'height': 5}

>>> square.width = 7
>>> vars(square)
{'width': 7, 'height': 7}

>>> square.height = 9
>>> vars(square)
{'width': 9, 'height': 9}

- 정사각형의 넓이를 구하는 area 메소드를 overiding하였다.

- 정사각형의 넓이는 제대로 연산되나, 정사각형 class에서 다시 선언한 method인 area 를 이용해 직사각형을 다시 구하려면?! 제대로된 값이 구해지지 않을 것

- 리스코프 치환 원칙을 고려해서 class를 구성하려면 아래와 같이 구성하면 됨

- 부모 객체의 메서드를 그 의도에 맞지 않게 오버라이딩하는 것을 주의하자!

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def calculate_area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def calculate_area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def calculate_area(self):
        return self.side ** 2

Interface segregation principle (인터페이스 분리 원칙)

- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

- 구현할 객체가 무의미한 메소드 구현을 방지하기 위해 각 class는 필요한 메서드만 가지고 있게 해야함 → 규모를 작게.

- 만약 Printer라는 class를 만들었는데, 그안에 많은 method를 가지고 있음

- 오래된 프린터는 fax, scan과 같은 기능이 필요없음에도, 추상클래스로 선언되어 가지고 있기때문에 불필요하게 오버라이딩으로 처리해야하는 문제가 있음

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print(self, document):
        pass

    @abstractmethod
    def fax(self, document):
        pass

    @abstractmethod
    def scan(self, document):
        pass

class OldPrinter(Printer):
    def print(self, document):
        print(f"Printing {document} in black and white...")

    def fax(self, document):
        raise NotImplementedError("Fax functionality not supported")

    def scan(self, document):
        raise NotImplementedError("Scan functionality not supported")

class ModernPrinter(Printer):
    def print(self, document):
        print(f"Printing {document} in color...")

    def fax(self, document):
        print(f"Faxing {document}...")

    def scan(self, document):
        print(f"Scanning {document}...")

- 이러한 문제를 해결하기 위해 class를 여러개로 분리

- 기능별 print, fax, scan을 class로 분리해 놓으면 필요한 것만 상속받아 가져다 쓰면 됨

# printers_isp.py

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print(self, document):
        pass

class Fax(ABC):
    @abstractmethod
    def fax(self, document):
        pass

class Scanner(ABC):
    @abstractmethod
    def scan(self, document):
        pass

class OldPrinter(Printer):
    def print(self, document):
        print(f"Printing {document} in black and white...")

class NewPrinter(Printer, Fax, Scanner):
    def print(self, document):
        print(f"Printing {document} in color...")

    def fax(self, document):
        print(f"Faxing {document}...")

    def scan(self, document):
        print(f"Scanning {document}...")

Dependency inversion principe (의존관계 역전 원칙)

- 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

- 아래와 같은 데이터를 화면에 뿌려주는 frontend를 구성했다고 하면

# app_dip.py

class FrontEnd:
    def __init__(self, back_end):
        self.back_end = back_end

    def display_data(self):
        data = self.back_end.get_data_from_database()
        print("Display data:", data)

class BackEnd:
    def get_data_from_database(self):
        return "Data from the database"

그런데, 데이터를 API로 받고 싶어…

FrontEnd class의 back_end에 대한 수정이 필요해짐

하지만 이는 개방-폐쇄원칙에 위배됨

- 다른 방법

data source라는 호환이 가능한 class를 여러개 만들어 두고 상속받아 사용!!!

# app_dip.py

from abc import ABC, abstractmethod

class FrontEnd:
    def __init__(self, data_source):
        self.data_source = data_source

    def display_data(self):
        data = self.data_source.get_data()
        print("Display data:", data)

class DataSource(ABC):
    @abstractmethod
    def get_data(self):
        pass

class Database(DataSource):
    def get_data(self):
        return "Data from the database"

class API(DataSource):
    def get_data(self):
        return "Data from the API"
>>> from app_dip import API, Database, FrontEnd

>>> db_front_end = FrontEnd(Database())
>>> db_front_end.display_data()
Display data: Data from the database

>>> api_front_end = FrontEnd(API())
>>> api_front_end.display_data()
Display data: Data from the API

참조

https://ko.wikipedia.org/wiki/SOLID_(객체_지향_설계)

https://realpython.com/solid-principles-python/

728x90
반응형
LIST
Comments