의도

  • 동일 계열의 알고리즘군을 정의하고, 각 알고리즘을 캡슐화하며, 이들을 상호작용이 가능하도록 만듬. 알고리즘을 사용하는 클라이언트와 상관없이 독립적으로 알고리즘을 다양하게 변경할 수 있게 함.

 

다른 이름

  • 정책(Policy)

 

언제 쓰는가 ?

  • 행동들이 조금씩 다를 뿐 개념적으로 관련된 많은 클래스들일 존재할 때. 전략 패턴은 많은 행동 중 하나를 가진 클래스를 구성할 수 있는 방법을 제공함.
  • 알고리즘의 변형이 필요할 때. 이를테면, 기억 공간과 처리 속도 간의 절충에 따라 서로 다른 알고리즘을 정의할 수 있을 것임. 이러한 변형물들이 알고리즘의 상속 관계로 구현될 때 전략 패턴을 사용할 수 있음.
  • 사용자가 몰라야 하는 데이터를 사용하는 알고리즘일 있을 때. 노출하지 말아야 할 복잡한 자료 구조는 Strategy 클래스에만 두면 되므로 사용자는 몰라도 됨.
  • 하나의 클래스가 많은 행동을 정의하고, 이런 행동들이 그 클래스의 연산 안에서 복잡한 다중 조건문의 모습을 취할 때. 많은 조건문보다는 각각을 Strategy 클래스로 옮겨놓는 것이 좋음.

 

구조

GoF의 디자인 패턴 중 P.409

  • Strategy : 제공하는 모든 알고리즘에 대한 공통의 연산들을 인터페이스로 정의함. Context 클래스는 ConcreteStrategy 클래스에 정의한 인터페이스를 통해서 실제 알고리즘을 사용.
  • ConcreteStrategy : Strategy 인터페이스를 실제 알고리즘으로 구현함.
  • Context : ConcreteStrategy 객체를 통해 구성됨. 즉, Strategy 객체에 대한 참조자를 관리하고, 실제로를 Strategy 서브클래스의 인스턴스를 갖고 있음으로써 구체화함. 또한 Strategy 객체가 자료에 접근해가는 데 필요한 인터페이스를 정의함.

 

협력 방법

  • Strategy 클래스와 Context 클래스는 의사교환을 통해 선택한 알고리즘을 구현함. Context 클래스는 알고리즘에 해당하는 연산이 호출되면, 알고리즘 처리에 필요한 모든 데이터를 Strategy 클래스로 보냄. 이때, Context 객체 자체를 Strategy 연산에다가 인자로 전송할 수도 있음.
  • Context 클래스는 사용자 쪽에서 온 요청을 각 전략 객체로 전달함. 이를 위해서 사용자는 필요한 알고리즘에 해당하는 ConcreteStrategy 객체를 생성하여 이를 Context 클래스에 전송하는데, 이 과정을 거치면 사용자는 Context 객체와 동작할 때 전달한 ConcreteStrategy 객체와 함께 동작함.

 

결과

  1. 동일 계열의 관련 알고리즘이 생김
    1. Strategy 클래스 계층은 동일 계열의 알고리즘군을 정의함. 공통의 기능에 대해 상속을 통해 재사용할 수 있음
  2. 서브클래싱을 사용하지 않는 대안
    1. Context 자체를 서브클래싱(상속)하지 않는 방법. Context와 무관하게 Strategy를 확장하는 방식
  3. 조건문을 없앨 수 있음
  4. 구현의 선택이 가능
    1. 사용자는 여러 Strategy들 중 하나를 선택할 수 있음
  5. 사용자는 서로 다른 전략을 알아야 함
    1. 사용자는 여러 전략 중 각 전략이 어떻게 다른지 알아야 함. 서로 다른 행동에 대해 특징을 알고 있을 때 이 패턴을사용함.
  6. Strategy 객체와 Context 객체 사이에 의사소통 오버헤드가 있음
  7. 객체 수가 증가

 

구현 / 고려 사항

  1. Strategy 및 Context 인터페이스를 정의
    1. Strategy가 Context에게서 정보를 얻어오는 방법으로 1) Context가 Strategy 쪽으로 데이터를 매개변수에 담을 수 있고 2) Context 자체를 매개변수로 넘겨 Strategy가 Context에게 다시 요청을 하도록 할 수 있음
    2. Context 자체를 매개변수로 넘기는 경우 Context에서 더욱 정교한 인터페이스 설계가 요구됨
  2. 전략을 템플릿 매개변수로 사용
    1. Strategy 객체가 컴파일 타임에 결정할 수 있고, Strategy 객체가 런타임에 바꿀 필요가 없을 때 템플릿을 사용할 수 있음 -> template <typename strategy> class Context;
    2. 정적으로 바인딩됨으로서 효율이 증가하는 효과도 있음
  3. Strategy 객체에 선택성을 부여
    1. Context 객체에서 Strategy가 있다면 그것을 사용, 없다면 기본 구현 사용.

 

예제 코드

  • 마트에서 과일을 파는 상황을 예시로 들었고 마트가 각 물건에 대해 가격을 어떻게 책정하는 가를 변경 가능한 '정책'으로 봤습니다.
  • 간단하게 물건 값을 얼마나 할인해줄지를 정하는 PriceStrategy를 인터페이스로 두었습니다. 이 전략은 Market에서 갖게 됩니다.
#include "Strategy1.h"

int main()
{
	Market market;
	Person person;
	
	market.Sell(&person, "apple");
	market.Sell(&person, "banana");

	person.ShowItems();
	person.ShowMoney();

	market.ChangeStrategy(PriceStrategy::manysell_strategy);

	market.Sell(&person, "apple");
	market.Sell(&person, "banana");
	market.Sell(&person, "graph");

	person.ShowItems();
	person.ShowMoney();

	return 0;
}

  • 첫 번째 구매에선 apple을 10달러, banana를 20달러를 주고 사서 70달러가 남았고
  • 두 번째 구매에선 apple을 7달러, banana를 14달러, graph를 21달러에 사서 70 - 42 달러가 된 모습입니다.
  • 첫 번째와 두 번째 구매 사이에 마켓은 판매 정책을 바꿨습니다. 싸게 많이 팔기 위한 정책입니다.
//Strategy1.h

#pragma once
#include <string>
#include <vector>
#include <map>

class Person;
class DefaultStrategy;
class ManySellStrategy;
class LuxuryStrategy;

class PriceStrategy
{
public:
	static DefaultStrategy* default_strategy;
	static ManySellStrategy* manysell_strategy;
	static LuxuryStrategy* luxury_strategy;

public:
	virtual float DisCountPercent() { return 1.0f; };
};

class Market
{
public:
	Market();

	void Sell(Person* person, std::string item_name);
	void ChangeStrategy(PriceStrategy* price_strategy);

private:
	PriceStrategy* price_strategy = nullptr;
	std::map<std::string, size_t> items;
};

class Person
{
public:
	Person();

	void Buy(std::string item_name, size_t price);
	void ShowMoney();
	void ShowItems();

private:
	std::vector<std::string> items;
	size_t money;
};

class DefaultStrategy : public PriceStrategy
{
public:
	virtual float DisCountPercent() { return 1.0f; };
};

class ManySellStrategy : public PriceStrategy
{
public:
	virtual float DisCountPercent() { return 0.7f; };
};

class LuxuryStrategy : public PriceStrategy
{
public:
	virtual float DisCountPercent() { return 1.5f; };
};
  • 전략 패턴을 사용한 PriceStrategy는 단순하게 얼마나 할인해 줄지를 결정하는 DisCountPercent를 인터페이스로 갖습니다. 서브클래싱을 하여 각 전략마다 얼마나 할인해줄지를 오버라이드하여 결정해줍니다.
  • Market(Context)는 PriceStrategy를 교체가능한 형태(포인터)로 갖고 있으며 물건을 팔 때 전략에 영향을 받습니다.
//Strategy1.cpp

#include "Strategy1.h"
#include <iostream>

DefaultStrategy* PriceStrategy::default_strategy = new DefaultStrategy;
ManySellStrategy* PriceStrategy::manysell_strategy = new ManySellStrategy;
LuxuryStrategy* PriceStrategy::luxury_strategy = new LuxuryStrategy;

Market::Market()
{
	price_strategy = PriceStrategy::default_strategy;

	items["apple"] = 10;
	items["banana"] = 20;
	items["graph"] = 30;
}

void Market::Sell(Person* person, std::string item_name)
{
	size_t price = items[item_name] * price_strategy->DisCountPercent();

	person->Buy(item_name, price);
}

void Market::ChangeStrategy(PriceStrategy* price_strategy)
{
	this->price_strategy = price_strategy;
}

Person::Person()
{
	money = 100;
}

void Person::Buy(std::string item_name, size_t price)
{
	if (money >= price)
	{
		money -= price;
		items.push_back(item_name);
	}
	else
	{
		std::cout << "구매 실패! " << std::endl;
	}
}

void Person::ShowMoney()
{
	std::cout << "남은 돈은 " << money << "$ 입니다" << std::endl;
}

void Person::ShowItems()
{
	for (const auto& item : items)
	{
		std::cout << "item : " << item << std::endl;
	}
}
  • Market은 DefaultStrategy를 기본값으로 사용하고, apple, banana, graph가 준비되어 있습니다.
  • Market에서 특정 물건을 Person에게 팔 때 할인 정책을 사용하여 기존 물건 값에서 할인 혹은 값 증가를 시켜주고 그 값을 Person의 Buy를 통해 실제 구매를 진행해줍니다.

'디자인 패턴 > 행위' 카테고리의 다른 글

방문자(Visitor)  (0) 2024.01.15
템플릿 메서드(Template Method)  (2) 2024.01.15
상태(State)  (0) 2024.01.12
감시자(Observer)  (0) 2024.01.12
메멘토(Memento)  (1) 2024.01.11

+ Recent posts