이 글은 헤드퍼스트 디자인 패턴을 읽고 필자의 방식으로 이해하고 정리한 내용입니다.
개요
Strategy Pattern은 Design Pattern 중 행위(Behavior)에 속하는 패턴이며 정의하자면 다음과 같다.
알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘 군을 수정해서 쓸 수 있게 해 줍니다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다. (헤드퍼스트 디자인 패턴, 64p)
상속은 무엇이 문제일까?
"알고리즘을 군을 정의하고 캡슐화해서 각각의 알고리즘 군을 수정해서 쓴다"라는 건 Super Class에 Method를 정의하고 이를 상속받는 Sub Class가 이를 Overrding으로 재사용한다는 측면에서 다를 바 없어 보인다.
만약 Sub Class가 상속은 받되 Overrding을 하지 않는다고 한다면?
설계의 관점에서 보자면 Sub Class는 잠재적으로 Super Class의 기능을 포함하고 있는 모양새이기 때문에 불필요한 동작이 추가된 Class일 뿐이다.
변경이 잦은 사항에 상속을 활용한다면 변경이 일어날 때마다 메서드를 일일이 살펴보고 상황에 따라 Override 해서 사용해하는 상황이 발생한다.
Strategy Pattern 어떻게 접근할까?
앞서 상속을 통해 Overrding을 이용하게 되면 변경이 일어날 때마다 상황에 따라 사용해야 한다고 언급했다. 그렇다면 이를 해결하기 위해서는 변경이 일어날만한 부분과 변경이 일어나지 않을 만한 부분을 분리하는 것이다.
Strategy Pattern은 행위(Behavior)에 관련된 Pattern이라고 언급했다. 즉 어떤 행위들을 가장 높은 레벨로 추상화하고 이를 구현할 때는 이 형식에 맞춰서 프로그래밍하면 된다.
오리를 예제로 이해해 보기
오리는 다음과 같이 두 가지 행동을 할 수 있다고 해보자.
- fly: 오리는 '날 수' 있음
- quack : 오리는 '꽥' 소리를 낼 수 있음
이를 파이썬 개념상 인터페이스로 나타내자면 다음과 같다.
class Duck(metaclass=ABCMeta):
def __init__(self, fly_behavior, quack_behavior):
self.fly_behavior: FlyBehavior = fly_behavior
self.quack_behavior: QuackBehavior = quack_behavior
def set_quack_behavior(self, quack_behavior):
self.quack_behavior = quack_behavior
def set_fly_behavior(self, fly_behavior):
self.fly_behavior = fly_behavior
앞으로 구현될 오리들은 위 인터페이스를 상속받아 사용하게 될 것이다. 아래와 같이 말이다.
class MallardDuck(Duck): ...
""" 청둥오리 """
class RubberDuck(Duck): ...
""" 고무오리 """
Behavior 구현하기
이제 오리가 할 수 있는 행위를 구현해 보자. '오리는 날 수 있음'을 나타내보자. 이를 추상화해 보자면 오리는 '날 수 있는 행위'에 해당하는 개념을 따로 구현하기 위해 '날 수 있는' 행위를 표현하는 인터페이스를 만들자.
class FlyBehavior(metaclass=ABCMeta):
@abstractmethod
def fly(self): pass
FlyBehavior를 통해 오리가 날 수 있다는 개념을 표현했다. 이를 구현하는 클래스들은 어떤 모양새일까?
class FlyWithWings(FlyBehavior):
""" 오리는 날개를 이용해 날 수 있다 """
def fly(self):
print("I'm flying!!")
class FlyNoWay(FlyBehavior):
""" 고무오리는 날 수 없다 """
def fly(self):
print("I can't fly")
FlyBehavior를 상속받아 클래스 자체로서 오리의 행동을 나타내는 모양새를 갖추게 되었다. 만약 날개 없이 비행이 가능한 오리가 생겼다면 어떻게 할까? 가능한 일인지 여부에 상관없이 개념을 표현하도록 하자.
class FlyWithNpWings(FlyBehavior):
""" 날개 없이도 날 수 있는 오리가 있다 """
def fly(self):
print("I'm flying!!")
quack에 해당하는 행위도 방법은 다르지 않다.
class QuackBehavior(metaclass=ABCMeta):
@abstractmethod
def quack(self):
pass
class NormalQuack(QuackBehavior):
def quack(self):
print("Noraml Quack")
어떤 식으로 사용하는 걸까?
앞서 정리한 내용은 Strategy Pattern을 사용하기 위한 예시로 오리에 관련된 행위를 구현했는데 이를 코드상에서 instance화 시키는 과정을 보도록 해보자. instance화는 Duck을 상속받는 class 중 하나를 선택해서 사용한다.
rubber_duck = RubberDuck()
지금까지 구현된 예제에서는 Duck을 instance 화 할 때는 flybehavior와 quackbehavior를 지정해줘야 한다.
rubber_duck = RubberDuck(
fly_behavior=FlyWithWings()
quack_behavior=NormalQuack()
)
Class를 초기화하면서 Behavior를 받는 것이 이상할 수 있는데 예시를 설명하기 위함이므로 넘어가자. 핵심은 이렇게 특정 행위(behavior)를 넘겨줌으로써 알고리즘 군을 교체한다라는 목적을 달성했다는 것이다. 만약 어떤 조건에 따라서 fly_behavior를 변경하고자 한다면 set_fly_behaivor method를 통해 교체할 수 있다.
rubber_duck = RubberDuck(
fly_behavior=FlyWithWings()
quack_behavior=NormalQuack()
)
rubber_duck.set_fly_behavior(FlyWithNoWings())
'Abstract' 카테고리의 다른 글
[Design Pattern] Observer Pattern (0) | 2023.01.18 |
---|