지식/Python

[python] Factory Pattern

ZeroAct 2023. 2. 26.
728x90
반응형

개요

메이플스토리에 나오는 주황버섯 클래스를 만들어보자.

class OrangeMushroom:
    def __init__(self):
        self.max_hp: int = 50
        self.hp: int = self.max_hp
    
    def hit(self, damage: int):
        self.hp -= damage
    
    def is_dead(self) -> bool:
        return self.hp <= 0

체력 관련된 method 들만 구현했다.

hit: 맞을 때 체력 깎기

is_dead: 죽었는지 살았는지

 

슬라임도 만들어보자

class Slime:
    def __init__(self):
        self.max_hp: int = 20
        self.hp: int = self.max_hp
    
    def hit(self, damage: int):
        self.hp -= damage
    
    def is_dead(self) -> bool:
        return self.hp <= 0

 

 

모든 몬스터는 hit 과 is_dead 함수가 필요하다.

몬스터라는 클래스를 만들어 상속받자.

class Monster:
    def __init__(self, max_hp: int):
        self.max_hp = max_hp
        self.hp= self.max_hp
    
    def hit(self, damage: int):
        self.hp -= damage
    
    def is_dead(self) -> bool:
        return self.hp <= 0

class OrangeMushroom(Monster):
    def __init__(self):
        super().__init__(50)

class Slime(Monster):
    def __init__(self):
        super().__init__(20)

여기까지는 아주 단순한 상속에 대한 예시이다.

 

이제 주황버섯 5, 슬라임 3마리 정도 생성해보자.

monsters_to_create = {
    "OrangeMushroom": 5,
    "Slime": 3,
}
monsters = []
for monster_name, num in monsters_to_create.items():
    if monster_name == "OrangeMushroom":
        monsters.extend([OrangeMushroom() for _ in range(num)])
    elif monster_name == "Slime":
        monsters.extend([Slime() for _ in range(num)])

뭐 이렇게 짜면 될 것 같다.

 

하지만 이게 메인 로직이면 많이 불편해진다.

몬스터 종류 만큼 메인 코드가 길어질 것 이다.

몬스터 이름을 받고 그에 맞는 몬스터를 생성해주는 함수가 있으면 좋을 것 같다.

class MonsterFactory:
    def create_monster(self, monster_name):
        if monster_name == "OrangeMushroom":
            return OrangeMushroom()
        elif monster_name == "Slime":
            return Slime()

monsters_to_create = {
    "OrangeMushroom": 5,
    "Slime": 3,
}
monsters = []
for monster_name, num in monsters_to_create.items():
    monsters.extend([MonsterFactory.create_monster(monster_name=monster_name) for _ in range(num)])

이것이 Factory 패턴이다.

 

만약,

리본돼지 클래스가 추가된다면 Factory 내의 create_monster 함수만 수정이되고,

메인 로직은 그대로 유지된다.

응용

디자인 패턴은 정답이 없다.

기본적인 철학이 통한다면 어떤 다른 형태여도 상관이 없다.

위의 예시는 가장 기본적인 Factory 구조이다.

 

Bounding Box 클래스를 만들어 보았다.

class BoundingBox:
    def __init__(self, xywh: list[int]):
        """Bounding Box

        Args:
            xywh (list[int]): left, top, width, height
        """
        self.x1, self.y1, self.w, self.h = xywh
        self.x2 = self.x1 + self.w
        self.y2 = self.y1 + self.h
    
    def area(self) -> int:
        return self.w * self.h
    
    def aspect_ratio(self) -> float:
        return self.h / self.w

xywh 를 인자로 받고

area, aspect_ratio 메소드가 구현되어 있다.

 

근데 만약에 내가 가지고 있는 데이터가 cxcywh 라고 해보자.

BoundingBox([cx - w/2, cy - h/2, w, h])

뭐 이런식으로 인자를 넘겨주면 될 것이다.

하지만, 코드가 더러워진다.

x1y1x2y2 로 가지고 있으면 메인 코드에 또 저런 코드를 짜야한다.

 

Factory 철학을 활용해보자.

class BoundingBox:
    def __init__(self, xywh: list[int]):
        """Bounding Box

        Args:
            xywh (list[int]): left, top, width, height
        """
        self.x1, self.y1, self.w, self.h = xywh
        self.x2 = self.x1 + self.w
        self.y2 = self.y1 + self.h

    @classmethod
    def from_xyxy(cls, xyxy: list[int]):
        x1, y1, x2, y2 = xyxy
        return cls([x1, y1, x2-x1, y2-y1])
    
    @classmethod
    def from_cxcywh(cls, cxcywh: list[int]):
        cx, cy, w, h = cxcywh
        return cls([cx - w/2, cy - h/2, w, h])

BoundingBox.from_xyxy(x1y1x2y2)
BoundingBox.from_cxcywh(cxcywh)

이게 가장 좋은 예시인지는 모르겠으나 개념을 알고 있다면 사용할 수 있는 곳이 많다.

Ref.

Refactoring and Design Patterns

 

Refactoring and Design Patterns

Hello, world! Refactoring.Guru makes it easy for you to discover everything you need to know about refactoring, design patterns, SOLID principles, and other smart programming topics. This site shows you the big picture, how all these subjects intersect, wo

refactoring.guru

 

728x90
반응형

'지식 > Python' 카테고리의 다른 글

[Database] Transaction  (0) 2023.08.24
[python] 시간, 타입 체크 데코레이터  (2) 2023.02.26

댓글