프로그래밍/CS

[디자인패턴] Strategy Pattern (전략 패턴)

choar 2022. 7. 29. 15:38
반응형

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를 구현하는 일 또한 담당하고 있기 때문이다.

if-else문

 

고전적인 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

반응형