Strategy Pattern
전략(strategy)은 특정한 목표를 수행하기 위한 행동 계획이다.
전략 패턴(strategy pattern)은 디자인 패턴 중 하나로, 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고 사용하며, 동적으로 전략 수정이 가능한 패턴을 일컫는다.
코드에서 뭔가를 하는 데 있어서 여러가지 방법이 존재할 때가 있다. 또한 상황에 따라 그 방법을 변경하고 싶을 수 있다.
예를 들어 고객 지원 소프트웨어를 사용할 때, 사용자가 얼마나 붐비는지 등에 따라 지원 고객 처리 순서를 다르게 처리하고 싶을 수 있다.
또는 VR 애플리케이션을 만들 때, VR 장비에 따라 다른 렌더링 알고리즘을 적용하도록 개발하고 싶을 수 있다.
이럴 때 이 '전략 패턴'을 사용할 수 있다.
Example
# BEFORE
import string
import random
from typing import List
def generate_id(length=8):
# helper function for generating an id
return ''.join(random.choices(string.ascii_uppercase, k=length))
class SupportTicket:
def __init__(self, customer, issue):
self.id = generate_id()
self.customer = customer
self.issue = issue
class CustomerSupport:
def __init__(self, processing_strategy: str = "fifo"):
self.tickets = []
self.processing_strategy = processing_strategy
def create_ticket(self, customer, issue):
self.tickets.append(SupportTicket(customer, issue))
def process_tickets(self):
# if it's empty, don't do anything
if len(self.tickets) == 0:
print("There are no tickets to process. Well done!")
return
if self.processing_strategy == "fifo":
for ticket in self.tickets:
self.process_ticket(ticket)
elif self.processing_strategy == "filo":
for ticket in reversed(self.tickets):
self.process_ticket(ticket)
elif self.processing_strategy == "random":
list_copy = self.tickets.copy()
random.shuffle(list_copy)
for ticket in list_copy:
self.process_ticket(ticket)
def process_ticket(self, ticket: SupportTicket):
print("==================================")
print(f"Processing ticket id: {ticket.id}")
print(f"Customer: {ticket.customer}")
print(f"Issue: {ticket.issue}")
print("==================================")
# create the application
app = CustomerSupport("filo")
# register a few tickets
app.create_ticket("John Smith", "My computer makes strange sounds!")
app.create_ticket("Linus Sebastian", "I can't upload any videos, please help.")
app.create_ticket("Arjan Egges", "VSCode doesn't automatically solve my bugs.")
# process the tickets
app.process_tickets()
이 코드에서 문제는 CustomerSupport의 process_tickets 메서드이다.
if-else문으로 적혀있어 코드가 길고 strategy를 추가하려면 이 코드를 더 늘려야 한다.
또한 해당 메서드로 인해 CustomerSupport 클래스의 cohesion이 낮아진다. 각 티켓을 출력하는 일 뿐만 아니라 각각의 ordering strategy를 구현하는 일 또한 담당하고 있기 때문이다.
고전적인 startegy pattern에서는 각 ordering strategy를 위한 class를 만든다.
그 class에는 실제 ordering 작업을 하는 메서드가 있고, 이는 process_tickets에서 호출된다.
abstract class로 interface를 만들고, 이를 implement해 각 ordering strategy 클래스를 만든다.
class TicketOrderingStrategy(ABC):
@abstractmethod
def create_ordering(self, list: List[SupportTicket]) -> List[SupportTicket]:
pass
class FIFOOrderingStrategy(TicketOrderingStrategy):
def create_ordering(self, list: List[SupportTicket]) -> List[SupportTicket]:
return list.copy()
class FILOOrderingStrategy(TicketOrderingStrategy):
def create_ordering(self, list: List[SupportTicket]) -> List[SupportTicket]:
list_copy = list.copy()
list_copy.reverse()
return list_copy
class RandomOrderingStrategy(TicketOrderingStrategy):
def create_ordering(self, list: List[SupportTicket]) -> List[SupportTicket]:
list_copy = list.copy()
random.shuffle(list_copy)
return list_copy
그러면 process_tickets 메소드를 다음과 같이 변경할 수 있다.
def process_tickets(self):
# create the ordered list
ticket_list = self.processing_strategy.create_ordering(self.tickets)
# if it's empty, don't do anything
if len(ticket_list) == 0:
print("There are no tickets to process. Well done!")
return
# go through the tickets in the list
for ticket in ticket_list:
self.process_ticket(ticket)
새로운 process_tickets는 티켓 순서와 관련된 코드를 포함하지 않는다.
→ cohesion이 높아졌다. 새로운 strategy가 추가돼도 process_tickets의 코드를 수정할 필요가 없다.
black hole strategy라는 ordering strategy를 추가하려는 경우를 예로 들면,
TicketOrderingStrategy를 implement하는 클래스를 하나 더 만들면 된다.
class BlackHoleStrategy(TicketOrderingStrategy):
def create_ordering(self, list: List[SupportTicket]) -> List[SupportTicket]:
return []
다른 방법도 있다.
interface를 만들고 이를 implement하는 class를 만드는 것 대신 ordering strategy를 함수로 만들 수 있다.
def fifoOrdering(list: List[SupportTicket]) -> List[SupportTicket]:
return list.copy()
def filoOrdering(list: List[SupportTicket]) -> List[SupportTicket]:
list_copy = list.copy()
list_copy.reverse()
return list_copy
def randomOrdering(list: List[SupportTicket]) -> List[SupportTicket]:
list_copy = list.copy()
random.shuffle(list_copy)
return list_copy
def blackHoleOrdering(list: List[SupportTicket]) -> List[SupportTicket]:
return []
이렇게 할 경우 코드가 더 줄어든다.
process_tickets는 다음과 같이 수정하면 된다.
(파이썬에서 함수의 type hint는 다음과 같이 처리한다 → Callable[[Arg1Type, Arg2Type], ReturnType])
전체 코드는 레퍼런스에 있는 주소에서 확인할 수 있다.
def process_tickets(self, ordering: Callable[[List[SupportTicket]], List[SupportTicket]]):
# create the ordered list
ticket_list = ordering(self.tickets)
# if it's empty, don't do anything
if len(ticket_list) == 0:
print("There are no tickets to process. Well done!")
return
# go through the tickets in the list
for ticket in ticket_list:
self.process_ticket(ticket)
🌟 내용에 오류가 있다면 댓글 달아주시면 감사하겠습니다.
References
https://www.youtube.com/watch?v=WQ8bNdxREHU
https://www.youtube.com/watch?v=vNsZXC3VgUA
https://github.com/ArjanCodes/betterpython/tree/main/3%20-%20strategy%20pattern
'프로그래밍 > CS' 카테고리의 다른 글
[디자인패턴] Template Method Pattern (템플릿 메소드 패턴) (0) | 2022.08.08 |
---|---|
[디자인패턴] Observer Pattern (옵저버 패턴) (0) | 2022.08.05 |
[디자인패턴] Dependency Inversion (의존성 역전) (0) | 2022.07.27 |
[디자인패턴] Cohesion (응집도), Coupling (결합도) (0) | 2022.07.26 |
[네트워크] OSI 7계층 모델 (0) | 2022.07.25 |