바뀌는 부분은 캡슐화 한다.


코드에 새로운 요구사항이 있을 때마다 바뀌는 부분이 있다면, 바뀌는 부분은 바뀌지 않는 다른 부분으로부터 골라내서 분리해야한다. 그래야 바뀌지 않는 부분에 영향을 미치지 않은 채로 그 부분만 고치거나 확장 할 수 있기 때문이다.


예시

 간단한 예시로, 날아다니는 기능과 꽉꽉 소리를 내는 기능을, 그리고 수영하는 기능을 가진 오리(Duck) 클래스를 구현하는 경우를 들어 보겠다.

오리에도 여러 종류가 있는데, 청둥오리(MallardDuck), 붉은머리오리(RedheadDuck), 고무오리(RubberDuck), 나무오리(DecoyDuck)가 그들이다.

각각의 오리는 꽉꽉 소리를 내는 방법도,  날아다니는 방법도 서로 다르다.


여기까지만 생각하면 상속을 이용해서 다음과 같이 디자인 할 수 있었을 것이다.


이 구조로 구현하면 MallardDuc, RedheadDuck, RubberDuck 클래스들이 제각각 quack(), fly(), swim() 메소드를 알아서 구현 하면 된다.

하지만 여기엔 몇 가지 문제점이 있다. 

RubberDuck은 고무오리이기 때문에 날 수 없고, DecoyDuck은 나무오리이기 때문에 날 수도 없고, 꽉꽉소리도 낼 수 없다. 즉, 없는 기능을 메소드로 가지는 것이다.

그리고 꽉꽉 소리를 내는것에 대해서, MallardDuck과 RedheadDuck은 꽉꽉 하고 소리를 내지만, RubberDuck은 삑삑 하고 소리를 낸다.

요약하면 quack(), fly() 메소드는 서브클래스마다 바뀌는 부분인 것이다.


물론 서브클래스들에서 두 메소드를 적절히 정의하면 가능하긴 하다. 그러면 다음과 같이 구현 될 것이다.


하지만 이 방법들로는 코드 중복이 발생하기 때문에 코드의 보수성이 떨어진다. (MallardDuck과 RedheadDuck의 quack() 메소드가 동일. RubberDuck과 DecoyDuck의 fly() 메소드가 동일)


이 예시에서 quack(), fly() 메소드는 바뀌는 부분에 속하고, swim() 메소드는 바뀌지 않는 부분에 속한다.(물론 swim() 메소드도 구현하기에 따라서 바뀌는 부분이 될 수도 있지만, 여기서는 모두 동일하게 구현된다고 하자)

이렇게 바뀌는 부분은 다른 클래스(혹은 인터페이스)로 빼내어 구현해야한다.


바뀌는 부분을 따로 빼내어 다음과 같이 구현할 수 있다.


이와 같이 구현하면 코드 중복문제도 해결되고, 새로운 기능의 추가도 한결 쉬워진다.

가령 Duck의 서브클래스로 RocketDuck이 추가되고, 이 RocektDuck은 로켓을 타고 날아다닌다고 하면, FlyBehavior 인터페이스를 구현하는 FlyByRocekt 구상 클래스를 추가하면 끝이다.

또, MallardDuck 객체중 10% 확률로 날 수 없는 객체가 생성된다고 할 때, 그 MallardDuck 객체에게 FlyBehavior로 FlyNoWay 구상 클래스를 추가해 주면 그만이다.

'IT 관련 > 객체지향 설계' 카테고리의 다른 글

헐리우드 원칙(Hollywood Principle)  (0) 2019.10.01
블로그 이미지

서기리보이

,

1. Abstract Factory 패턴이란

1.1 Abstract Factory 패턴의 정의

인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있게 하는 패턴


정의만읽어서는 이게 무슨 내용인지 알기가 어렵다. UML과 함께 보도록 하자.


1.2 UML




AbstractFactoty - 각각의 Product 객체를 생산하는 메소드를 지닌 추상 팩토리 인터페이스

ConcreteFactory - AbstractFactory 인터페이스를 구현한 구상 팩토리 클래스

Client - 팩토리에게 객체의 생성을 요청하는 클래스

AbstractProductA,B - 팩토리에 의해 생성되는 클래스들에 대한 인터페이스

ProductA_1,A_2,B_1,B_2 - 실제 생성될 Product 구상 클래스





2. 적용 예제


Head First Desgin Pattern에 나오는 예제를 수정한 것이다.

앞부분 내용은 Factory Method 패턴 때 썼던 글과 똑같으니 대충 읽어봐도 괜찮을 듯 하다.


객체마을 피자가게에서는 클래스를 이용하여 판매하는 피자 종류를 관리하고, 피자를 주문받는다.

피자가게 관리를 위해서 다음의 클래스 구조를 이용하고 있었다.





우선 피자 인터페이와 구상 클래스 중 하나인 CheesePizza 클래스는 다음과 같이 정의된다.

<Pizza.h>

#pragma once #include <string> using namespace std; class Pizza { public: virtual void prepare() = 0; //피자의 재료를 준비한다. virtual void bake() = 0; //피자를 굽는다 virtual void cut() = 0; //피자를 자른다. virtual void box() = 0; //피자를 박스에 담는다. virtual string toString() = 0; //피자의 종류를 스트링으로 리턴한다. };

<CheesePizza.h>

#include <iostream> #include "Pizza.h" using namespace std; class CheesePizza : public Pizza { public: CheesePizza(); void prepare() override; void bake() override; void cut() override; void box() override; string toString() override; };


객체마을 피자가게를 나타내는 PizzaStore 클래스에서 피자를 주문하는 orderPizza 메소드는다음과 같이 구현된다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();

else if(type.compare("clam") == 0)

pizza = new ClamPizza();

else if(type.compare("veggi") == 0)

pizza = new VeggiPizza();



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


그런데 객체마을 피자나라의 가게 관리 프로그램이 유명해지면서, 여러 도시의 피자가게들이 이 프로그램을 이용 할 수 있게 해달라고 요청했다.


우리는 이 프로그램의 클래스 구조를 수정하여 여러 도시의 피자가게들이 이 프로그램을 이용 할 수있게 새로 만들어야한다.


문제점은, 각 지역마다 피자를 만드는 방식에 차이가 있다는 것이다.


어떤 도시에서는 피자를 만들때 빵은 얇고, 치즈는 조금 적게 넣는 편이고, 또 다른 도시에서는 빵을 두껍게하고 소스를 듬뿍 뿌려 맛을 낸다.


이를 반영하기 위해 피자 클래스는 다음과 같이 지역별로 나뉘어야 한다.


그런데 변경된 피자 클래스 구조를 반영하여, orderPizza() 메소드를 수정하면 다음과 같이 구현 될 것이다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("ChicagoCheese") == 0)

pizza = new ChicagoCheesePizza();

else if(type.compare("ChicagoPepperoni") == 0)

pizza = new ChicagoPepperoniPizza();

else if(type.compare("ChicagoClam") == 0)

pizza = new ChicagoClamPizza();

else if(type.compare("ChicagoVeggi") == 0)

pizza = new ChicagoVeggiPizza();

else if(type.compare("NYCheese")==0)

pizza = new NYCheesePizz();

else if(type.compare("NYVeggi")==0)

pizza = new NYVeggiPizza();

...



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}



이렇게 되면 이 프로그램을 이용하는 피자가게가 늘어 날 때마다 if-else 문이 점점 늘어나게 된다.


이런 문제를 해결하기 위해서 추상 팩토리 패턴을 적용하여 피자 객체 생성의 역할을 팩토리에게 넘기도록 다음의 구조로 수정한다.

러기 위해서 이제 팩토리 메소드 패턴을 적용하여, 피자가게와 피자 클래스를 다음과 같은 구조로 만든다.


이 다이어그램과 1.2의 UML과 비교하면, 각 요소는 다음과 같이 맵핑된다.


PizzaStore - Client

PizzaFactory - AbstractFactory

NYPizzaFactory, ChicagoFactory - ConcreteFactory1,2

Pizza 인터페이스 - Product 인터페이스

NYCheesePizza,NYVegiPizza,...ChacigoClamPizza,ChacigoPepperoniPizza - ConcreteProduct


그리고 PizzaFactory, NYPizzaFactory는 다음과 같이 정의/구현된다.

<PizzaFactory.h>

#pragma once #include <string> #include "../Pizza/Pizza.h" class PizzaFactory { public: //type을 기준으로 피자를 리턴 //만약 없는 타입이라면 NULL 리턴 virtual Pizza* createPizza(const string& type) = 0; };


<NYPizzaFactory.h>

class NYPizzaFactory : public PizzaFactory { public: NYPizzaFactory(); Pizza* createPizza(const string& type) override; };

<NYPizzaFactory.cpp>

NYPizzaFactory::NYPizzaFactory() { } Pizza* NYPizzaFactory::NYPizzaFactory::createPizza(const string& type) { Pizza * pizza = NULL; if (type.compare("cheese") == 0) pizza = new NYCheesePizza(); else if (type.compare("pepperoni") == 0) pizza = new NYPepperoniPizza(); else if (type.compare("clam") == 0) pizza = new NYClamPizza(); else if (type.compare("veggi") == 0) pizza = new NYVeggiPizza(); return pizza; }


이렇게 구현하고 나면, 사용 할 때는 PizzaStore를 생성 할 때는 다음과 같이 각 분점에 맞는 PizzaFactory를 넘겨주면 된다.


//뉴욕 피자 팩토리를 넘겨줘서 뉴욕식 피자를 만드는 PizzaStore 생성

PizzaStore nyPizzaStore(&nyPizzaFactory);

//시카고 피자 팩토리를 넘겨줘서 시카고 피자를 만드는 PizzaStore 생성 PizzaStore chicagoPizzaStore(&chicagoPizzaFactory);


구현된 프로젝트는 다음의 깃허브에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/AbstractFactoryPattern/tree/master)

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Abstract Factory 패턴의 장점

- 객체 생성을 캡슐화 할 수 있다.

- 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있다.

- 관련 있는 클래스들을 묶어서 하나의 제품군으로 만들 수 있다.

- 싱글톤 패턴을 적용하기 적합하다.


객체를 생성하는 부분이 팩토리에 캡슐화되므로 코드 중복을 방지 할 수 있다.


구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있기 때문에 특정 구현에 덜 의존적이고, 프로그램을 더 유연하게 만들어 준다.


구상 팩토리가 생산해내는 제품별로 클래스를 분류 할 수 있다. 이 덕분에 제품군을 대체하기 쉽고, 서로 연관된 여러 제품들을 만들어야 할 때 유용하다.


한 제품군을 생산하는 팩토리는 프로그램 내에서 한개로 충분하다. 그래서 Abstract Factory 패턴을 적용할 때는 싱글톤 패턴으로 구현하는 것도 좋은 방법이 될 수 있다.


4. Abstract Factory 사용시 유의사항(단점)

- 새로운 종류의 제품을 제공하기 어렵다.


Abstract Factory 인터페이스가 생성 할 수 없는 새로운 종류의 제품이 생긴다면 그 제품을 반영하기 위해서는 Abstract Factory 와 모든 구상 팩토리들을 수정해야한다.


가령 위의 피자가게 예제에서 피클을 생산하는 기능을 추가하려면, PizzaFactory 인터페이스에 createPickle() 메소드가 추가되어야하고, NYPizzaFactory, ChicagoPizzaFactory 클래스에서 createPickle() 메소드를 구현해야한다.


5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

블로그 이미지

서기리보이

,

1. Factory Method 패턴이란

1.1 Factory Method 패턴의 정의

객체를 생성하기 위한 인터페이스를 정의하고, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 하는 패턴


정의만읽어서는 이게 무슨 내용인지 알기가 어렵다. UML과 함께 보도록 하자.


1.2 UML



Product - 생성될 객체의 인터페이스

ConcreteProduct - Product 인터페이스를 구현한 구상 클래스.

Creator - 객체를 생성하는 팩토리 메소드를 가진 인터페이스

ConcreteCreator - Creator 인터페이스를 구현한 구상 클래스.


여기서 ConcreteProduct와 ConcreteCreator 여러개의 구상 클래스들을 하나로 묶은것으로 봐도 된다.

즉, ConcreteProduct1,2,3... 이런식으로 여러 종류의 클래스가 Product 인터페이스를 구현 할 수 있고, ConcreteCreatort에 대해서도 마찬가지다.


Creator 인터페이스는 factoryMethod()를 통해 Product 인터페이스 객체를 생성한다. 하지만 Creator클래스는 Product 인터페이스의 구상 클래스들 중 어떤 클래스를 생성할 지 알 수 없다.

어떤 구상 클래스를 생성할지는 ConcreteCreator 클래스에서 factoryMethod()를 오버라이드하여 결정한다.


* Product 인터페이스 객체를 생성한다는 말이 조금 헷갈릴 수도 있을 것 같은데, 추상 클래스나 인터페이스는 원래 객체화가 불가능하다. 

객체화를 할 때는 당연히 Product 인터페이스를 구현한 구상 클래스를 객체화해야하고, 이 구상 클래스를 객체화 한다는 의미로 Product 인터페이스 객체를 생성한다는 말을 사용한 것이다.



2. 적용 예제


Head First Desgin Pattern에 나오는 예제이다.

이전에 Simple Factory 글을 쓸 때 나왔던 예제의 확장판이다.


객체마을 피자가게에서는 클래스를 이용하여 판매하는 피자 종류를 관리하고, 피자를 주문받는다.

피자가게 관리를 위해서 다음의 클래스 구조를 이용하고 있었다.





우선 피자 인터페이와 구상 클래스 중 하나인 CheesePizza 클래스는 다음과 같이 정의된다.

<Pizza.h>

#pragma once #include <string> using namespace std; class Pizza { public: virtual void prepare() = 0; //피자의 재료를 준비한다. virtual void bake() = 0; //피자를 굽는다 virtual void cut() = 0; //피자를 자른다. virtual void box() = 0; //피자를 박스에 담는다. virtual string toString() = 0; //피자의 종류를 스트링으로 리턴한다. };

<CheesePizza.h>

#include <iostream> #include "Pizza.h" using namespace std; class CheesePizza : public Pizza { public: CheesePizza(); void prepare() override; void bake() override; void cut() override; void box() override; string toString() override; };


객체마을 피자가게를 나타내는 PizzaStore 클래스에서 피자를 주문하는 orderPizza 메소드는다음과 같이 구현된다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();

else if(type.compare("clam") == 0)

pizza = new ClamPizza();

else if(type.compare("veggi") == 0)

pizza = new VeggiPizza();



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


그런데 객체마을 피자나라의 가게 관리 프로그램이 유명해지면서, 여러 도시의 피자가게들이 이 프로그램을 이용 할 수 있게 해달라고 요청했다.


우리는 이 프로그램의 클래스 구조를 수정하여 여러 도시의 피자가게들이 이 프로그램을 이용 할 수있게 새로 만들어야한다.


문제점은, 각 지역마다 피자를 만드는 방식에 차이가 있다는 것이다.


어떤 도시에서는 피자를 만들때 빵은 얇고, 치즈는 조금 적게 넣는 편이고, 또 다른 도시에서는 빵을 두껍게하고 소스를 듬뿍 뿌려 맛을 낸다.


이를 반영하기 위해 피자 클래스는 다음과 같이 지역별로 나뉘어야 한다.


그런데 변경된 피자 클래스 구조를 반영하여, orderPizza() 메소드를 수정하면 다음과 같이 구현 될 것이다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("ChicagoCheese") == 0)

pizza = new ChicagoCheesePizza();

else if(type.compare("ChicagoPepperoni") == 0)

pizza = new ChicagoPepperoniPizza();

else if(type.compare("ChicagoClam") == 0)

pizza = new ChicagoClamPizza();

else if(type.compare("ChicagoVeggi") == 0)

pizza = new ChicagoVeggiPizza();

else if(type.compare("NYCheese")==0)

pizza = new NYCheesePizz();

else if(type.compare("NYVeggi")==0)

pizza = new NYVeggiPizza();

...



//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}



이렇게 되면 이 프로그램을 이용하는 피자가게가 늘어 날 때마다 if-else 문이 점점 늘어나게 된다.


이런 문제를 해결하기 위해서는 PizzaStore 클래스가 각 지역의 가게들 별로 나누어질 필요가 있다.

그러기 위해서 이제 팩토리 메소드 패턴을 적용하여, 피자가게와 피자 클래스를 다음과 같은 구조로 만든다.



이 다이어그램과 1.2의 UML과 비교하면, 각 요소는 다음과 같이 맵핑된다.


PizzaStore 인터페이스 - Creator 인터페이스

NYPizzaStore, ChicagoPizzaStore - ConcreteCreator

Pizza 인터페이스 - Product 인터페이스

NYCheesePizza,NYVegiPizza,...ChacigoClamPizza,ChacigoPepperoniPizza - ConcreteProduct



그리고 PizzaStore, NYPizzaStore, ChicagoPizzaStore는 각각 다음과 같이 정의/구현된다.

<PizzaStore.h>

#pragma once #include <string> #include "../Pizza/Pizza.h" using namespace std; //피자 가게 추상 클래스 class PizzaStore { public:

    //피자를 주문. Pizza* orderPizza(const string& type);  protected:

// 피자를 생성. 없는 타입이면 NULL 리턴. virtual Pizza* createPizza(const string& type) = 0; };

<PizzaStore.cpp>

#include "PizzaStore.h" PizzaStore::PizzaStore() { } Pizza* PizzaStore::orderPizza(const string& type) { Pizza * pizza; pizza = createPizza(type); pizza->prepare(); pizza->bake(); pizza->cut(); pizza->box(); return pizza; }

<NYPizzaStore.cpp>

#include "NYPizzaStore.h" NYPizzaStore::NYPizzaStore() { } Pizza * NYPizzaStore::createPizza(const string& type) { if (type.compare("cheese")) return new NYCheesePizza(); else if (type.compare("veggi")) return new NYVeggiPizza(); else if (type.compare("clam")) return new NYClamPizza(); else if (type.compare("pepperoni")) return new NYPepperoniPizza(); return NULL; }

<ChicagoPizzaStore.cpp>

#include "ChicagoPizzaStore.h" ChicagoPizzaStore::ChicagoPizzaStore() { } Pizza * ChicagoPizzaStore::createPizza(const string& type) { if (type.compare("cheese")) return new ChicagoCheesePizza(); else if (type.compare("veggi")) return new ChicagoVeggiPizza(); else if (type.compare("clam")) return new ChicagoClamPizza(); else if (type.compare("pepperoni")) return new ChicagoPepperoniPizza(); return NULL; }

구현된 프로젝트는 다음의 깃허브에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/FactoryMethodPattern)

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Factory Method 패턴의 장점

- 의존성 뒤집기 원칙이 지켜진다.

- 객체의 생성을 서브클래스가 담당한다

- 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있음


Factory Method 패턴에서는 객체생성시 저수준 구성요소인 ConcreteProduct 대신 고수준 구성요소인 Product에 의존 하게된다.

피자가게 예제에서, PizzaStore 인터페이스는 피자 객체를 생성할 때, 저수준 구성요소인 NYCheesePizza, ChicagoClamPizza 등에 의존하지 않고, 고수준 구성요소인 Pizza에 의존한다.


객체의 생성을 서브클래스가 담당하기 때문에 생성할 객체 타입을 예측 할 수 없을 때 유용하고, 변화에 더 유연하게 대처 할 수 있다.

가령 위의 피자가게 예제에서, 캘리포니아(Califonia)의 피자가게에서도 이 프로그램을 이용 할 수 있게 해달라고 한다면 CalifoniaCheesePizza 등의 피자 클래스와 CalifoniaPizzaStore 클래스를 추가 해주면 되고, 뉴욕 피자가게에서 더이상 ClamPizza를 판매하지 않는다고 하면 NYClamPizza 클래스를 삭제하고 NYPizzaStore 클래스의 createPizza() 메소드에서 ClamPizza를 주문하는 부분을 삭제하면 된다.


그리고 구상 클래스가 아닌 추상 클래스/인터페이스에 맞춰 구현 할 수 있다는 점도 프로그램의 유연성을 늘려준다.





4. Factory Method 패턴 이용 시 유의 할 점(단점)

- 상속을 이용하기 때문에 서브클래싱이 안되는 경우를 고려해야한다.


C#를 예로 들면 C#에서는 인터페이스를 제외한 추상 클래스, 일반 클래스를 다중 상속 할 수 없다. 그래서 Creator가 모든 메소드를 가상 메소드로 선언한 인터페이스이거나, ConcreteCreator의 유일한 부모 클래스가 되어야 한다.

그리고 그 외의 대부분의 언어에서도 다중 상속을 사용하면 모호성이 발생할 수 있기 때문에 다중상속은 이용하지 않는 것을 권장한다.



5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

- 위키피디아 : Factory Method Pattern

(https://en.wikipedia.org/wiki/Factory_method_pattern)

- 윤성우저 열혈C++프로그래밍(Orange 

블로그 이미지

서기리보이

,

Simple Factory는 사실 디자인 패턴에 속하지 않는다. 하지만 프로그래밍을 하는데 있어서는 자주 사용되기도 하고, Factory Method 패턴이나 Abstract Factory 패턴과 많이 유사하기 때문에, '팩토리'의 대략적인 개념을 잡기위해 디자인 패턴 카테고리에 포함시켰다.


1. Simple Factory이란


Simple Factory는 객체를 생성하는 역할을 팩토리 클래스가 전담하게 하는 방법이다.


UML과 함께 보도록 하자.





Client - 객체의 생성을 요청하는 클래스

SimpleFactory - 객체의 생성을 담당하는 클래스

Product - 생성될 객체에 대한 인터페이스

ConcreteProduct1,2,3 - Product 인터페이스를 구현한 구상인터페이스


여기서 Client는 createProduct() 메소드를 통해 SimpleFactory에게 객체 생성을 전담하고 있다.

이렇게 하면 ConcreteProduct 객체들을 필요로하는 클래스가 여러개 있을 때, SimpleFactory 클래스를 통해 객체를 생성하기 때문에 ConcreteProduct 객체를 생성하는 부분에 대한 코드 중복을 방지 할 수 있다.



2. 적용 예제


Head First Desgin Pattern에 나오는 예제이다.


객체마을 피자가게에서는 클래스를 이용하여 판매하는 피자 종류를 관리하고, 피자를 주문받는다.

피자가게 관리를 위해서 다음의 클래스 구조를 이용하고 있었다.





우선 피자 인터페이와 구상 클래스 중 하나인 CheesePizza 클래스는 다음과 같이 정의된다.

<Pizza.h>

#pragma once #include <string> using namespace std; class Pizza { public: virtual void prepare() = 0; //피자의 재료를 준비한다. virtual void bake() = 0; //피자를 굽는다 virtual void cut() = 0; //피자를 자른다. virtual void box() = 0; //피자를 박스에 담는다. virtual string toString() = 0; //피자의 종류를 스트링으로 리턴한다. };

<CheesePizza.h>

#include <iostream> #include "Pizza.h" using namespace std; class CheesePizza : public Pizza { public: CheesePizza(); void prepare() override; void bake() override; void cut() override; void box() override; string toString() override; };


객체마을 피자가게를 나타내는 PizzaStore 클래스에서 피자를 주문하는 orderPizza 메소드는다음과 같이 구현된다.

Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("greek") == 0)

pizza = new GreekPizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();


//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


여기서 조개피자(Clam Pizza)와 야채피자(Veggi Pizza)가 

추가되고, 잘 팔리지 않는 그리스식 피자(Greek Pizza)가 제외된다고 하자 그러면 코드는 다음과 같이 수정된다.


Pizza* PizzaStore::orderPizza(string type){

Pizza pizza;


//피자 객체 생성

//바뀌는 부분

if(type.compare("cheese") == 0)

pizza = new CheesePizza();

else if(type.compare("greek") == 0)

pizza = new GreekPizza();

else if(type.compare("pepperoni") == 0)

pizza = new PepperoniPizza();

else if(type.compare("clam") == 0)

pizza = new ClamPizza();

else if(type.compare("veggi") == 0)

pizza = new VeggiPizza();


//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비.

//바뀌지 않는 부분

pizza.perpare();

pizza.bake();

pizza.cut();

pizza.box();

return pizza;

}


orderPizza() 메소드에서 가장 문제가 되는 점은 판매하는 피자의 종류가 바뀔 때 마다 코드를 수정해야 한다는 점이다.

즉, 피자 객체를 생성하는 부분은 바뀌는 부분이다.


이제 바뀌는 부분을 알아냈으니 이 부분을 캡슐화 하면 된다.


바뀌는 부분인 객체를 생성하는 부분을 캡슐화 하기 위해서 팩토리 패턴을 적용하여 다음과 같은 구조로 구현한다.




여기서 SimplePizzaFactory가 피자의 생성을 담당한다.


SimplePizzaFactory의 createPizza() 메소드는 다음과 같이 구현된다.

<SimplePizzaFactory.cpp>


Pizza* SimplePizzaFactory::createPizza(const string& type) { Pizza * pizza = NULL; if (type.compare("cheese") == 0) pizza = new CheesePizza(); else if (type.compare("pepperoni") == 0) pizza = new PepperoniPizza(); else if (type.compare("clam") == 0) pizza = new ClamPizza(); else if (type.compare("veggi") == 0) pizza = new VeggiPizza(); return pizza; }


이제 객체를 생성하는 역할을 SimplePizzaFactory에게 전담시켰으니 orderPizza() 메소드를 다음과 같이 수정한다.

<PizzaStore.cpp>


Pizza* PizzaStore::orderPizza(const string& type) {

//팩토리를 통해 피자 객체 생성

SimplePizzaFactory factory; Pizza* pizza = factory.createPizza(type);

//피자 재료를 준비하고, 굽고, 자르고, 박스에 담아서 피자를 준비. pizza->prepare(); pizza->bake(); pizza->cut(); pizza->box(); return pizza; }

이제 피자 객체의 생성을 SimplePizzaFactory 클래스가 전담하기 때문에, 또 다른 클래스가 피자 객체를 생성해야 하는 상황이 생긴다면 이 SimplePizzaFactory를 이용하면 된다.


구현된 프로젝트는 다음의 깃허브에서 확인 할 수 있다.

(https://github.com/InvincibleTyphoon/SimpleFactoryPattern)

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Simple Factory의 장점

- 객체를 생성하는 부분을 한 클래스에 캡슐화함


Simple Factory의 장점은 이 한마디로 정리 될 수 있다.

객체를 생성하는 부분을 한 클래스에 캡슐화하면, 여러 클래스에서 어떤 클래스의 객체를 생성할 때, 그 클래스를 생성하는 코드 중복을 방지 할 수 있다.

가령 위의 예제에서 PizzaStore 클래스 뿐만 아니라, 피자에 대한 설명을 찾아서 활용하는 PizzaShopMenu 클래스, 피자를 준비 할 때 피자를 박스에 포장(box() 메소드 호출)한 후에 또 봉지에 싸서 포장해야하는 HomeDelivery 클래스가 추가되고, 피자의 메뉴에 변경이 발생하면 PizzaStore,PizzaShopMenu,HomeDelivery 세 클래스에서 피자 객체를 생성하는 부분을 수정해야하는 어려움이 있다.

하지만 객체 생성을 SimplePizzaFactory 클래스가 전담하면 SimplePizzaFactory 클래스만 수정하면 된다.


4. 레퍼런스

- Head First Design Pattern(O'REILLY media)

블로그 이미지

서기리보이

,

1. Decorator 패턴이란

1.1 Decorator 패턴의 정의

주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴


정의만읽어서는 이게 무슨 내용인지 알기가 어렵다. UML과 함께 보도록 하자.


1.2 UML



Component - 최상위 클래스, 혹은 인터페이스, 혹은 추상 클래스.

ConcreteComponent - Component 인터페이스를 구현한 구상(Concrete) 클래스.

Decorator - Component를 상속한 클래스들을 장식 하는 기능에 대한 인터페이스.

ConcreteDecoratorA,ConcreteDecoratorB - Decorator 인터페이스를 구현한 구상 클래스


ConcreteDecoratorA, ConcreteDecoratorB는 Component를 상속받는 모든 종류의 클래스를 장식하기 위해 사용된다. 그래서 ConcreteComponent 뿐만 아니라 Decorator 클래스들도 장식 할 수 있다.



2. 적용 예제


Head First Desgin Pattern에 나오는 예제이다.


스타버즈 커피샵은 클래스를 이용하여 판매하는 음료들을 관리한다. 스타버즈 커피에서 판매하는 음료들은 다음과 같은 클래스 구조로 구성되었다.




Beverage라는 인터페이스는 가격을 계산하기 위한 cost 값과, 음료의 설명을 위한 description 값을 가진다. 자세한 가격 계산 및 description에 관한 것은 Beverage 인터페이스를 구현한 구상 클래스들의 몫이다.


그런데 여기서 커피를 주문 할 때 스팀 우유, 두유, 모카(초콜릿), 휘핑 크림 등의 첨가물을 추가했을때, 첨가물을 포함한 음료의 총 가격을 계산할 수 있는 기능이 필요해졌다.

이에따라 우리는 이 클래스 구조를 4가지 종류의 첨가물(스팀우유, 두유, 모카, 휘핑크림)을 포함한 음료의 총 가격을 계산 할 수 있도록 수정해야한다.

첨가물을 이 시스템에 추가할 방법을 찾아야 하는데, 어떤 방법이 있을까?


여기에 Decorator 패턴을 적용해 보자.



CondimentDecorator는 음료에 추가 될 수 있는 첨가물 데코레이터를 나타내는 인터페이스이며, SteamdMilk, Mocha 등의 클래스는 이 첨가물 인터페이스를 구현한다.

여기서 눈여겨 볼 점은 CondimentDecorator가 Beverage를 구성요소로 가진다는 점이다. CondimentDecorator가 구성요소로 가진 Beverage가 첨가물이 더해질 음료가 된다.

가령, Mocha 객체가 beverage로 HouseBlended를 가지면 이 음료는 모카 하우스 블렌디드 커피가 된다.

그리고 첨가물은 첨가물이 더해진 음료도 장식 할 수 있다. 그래서 다음과 같은 구조도 가능하다.



이 음료의 이름은 라떼 더블모카 하우스 블렌디드 커피가 될 것이다.

그리고 우리의 목표였던 가격 계산은 다음과 같이 수행 할 수 있다.



다음은 최상위 인터페이스인 Beverage의 구현 코드이다.

#pragma once #include using namespace std; //The base class for beverages class Beverage { public: virtual float getCost() = 0; virtual const string& getDescription() = 0; protected: float cost; //$ notation string description; };

Beverage 인터페이스는 음료의 가격을 계산하는 getCost() 메소드와 getDescription() 메소드를 갖는다.


다음은 Beverage 인터페이스를 구현한 HouseBlended 클래스다.

<HouserBlended.h>

#pragma once #include "Beverage.h" //House Blended Coffee class HouseBlended : public Beverage { public: HouseBlended(); const string& getDescription() override; float getCost() override; };

<HouserBlended.cpp> #include "HouseBlended.h" HouseBlended::HouseBlended() { this->cost = 0.89; this->description = "House Blended Coffee"; } const string& HouseBlended::getDescription() { return description; } float HouseBlended::getCost() { return cost; }

음료의 getCost(), getDescription 메소드의 구현은 단순히 클래스 내에 cost값과 description 값을 리턴하도록 구현한다.


다음은 Decorator 패턴을 위해 구현한 CondimentDecorator 클래스다.

#pragma once #include "../Beverage.h" //Decorator interface for Beverage class CondimentDecorator : public Beverage { protected: Beverage * beverage; string cattedString; //string for returning the concatenated string. //it's not a good way to return new object in c++, because it's not deleted automatically. };

장식할 음료에 접근하기 위해 Beverage* beverage; 필드를 갖는다.

cattedString은 getDescription() 메소드 구현을 위해서, 두개의 string 객체를 합쳐서 리턴해야하는데, 리턴할 스트링 값을 담기 위해 사용된다.

return string1 + string2; 와같이 리턴하면, 합쳐진 결과 string 객체는 지역변수처럼 함수가 종료되면서 메모리 공간이 해제되어 사라져버린다. 그래서 필드 값으로 리턴 할 값을 담을 공간을 만들어 주었다.


다음은 Decorator 인터페이스를 구현한 구상 클래스인 Mocha 클래스를 보자. 

<Mocha .h>

#include "CondimentDecorator.h" //Mocha condiment class Mocha : public CondimentDecorator { public: Mocha(Beverage* const beverage); float getCost() override; const string& getDescription() override; };


<Mocha .cpp>

#include "Mocha.h" Mocha::Mocha(Beverage* const beverage) { this->beverage = beverage; this->cost = 0.2; this->description = "Mocha, "; } float Mocha::getCost() { return cost + beverage->getCost(); } const string& Mocha::getDescription() { cattedString = description + beverage->getDescription(); return cattedString; }

Mocha 클래스는 가격을 계산할 때, 자신의 가격 + beverage의 가격을 더해서 리턴한다.

getDescription() 메소드도 유사하게 구현되었고, CondimentDecorator를 구현한 모든 구상 클래스가 동일한 구조다.



구현한 프로젝트는 다음 깃허브 페이지에서 확인 할 수 있다. https://github.com/InvincibleTyphoon/DecoratorPattern

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Decorator 패턴의 장점

- 구성(Composition)을 통해 객체의 행동을 확장하기 때문에 실행중에 동적으로 행동을 설정 할 수 있다. (객체지향 설계 원칙 - 상속보다는 구성을 이용. Composition over inheritance.)


- 기존 코드를 건드리지 않고, 확장을 통해 새로운 행동을 추가 할 수 있다.(OCP(Open-Closed Principle) 원칙을 준수)


둘 다 객체지향 설계 원칙과 관련되는 이야기다.


상속이 객체지향 설계에서 강력한 힘을 발휘하긴 하지만, 일반적으로 상속보다 구성을 우선시 해야한다. 두 클래스의 관계에 있어서 상속 관계보다는 구성 관계가 더 느슨하기 때문에, 구성이 상속에 비해 변화에 더 유연하게 대처할 수 있다.


기존 코드를 수정하면 아무래도 새로운 클래스를 작성하는 것 보다 오류를 범할 위험성이 있다. 수정해야 할 코드에 의존하는 클래스가 많다면 그 코드를 수정하는 것이 각각의 클래스에 어떤 영향을 미칠지 다 고려해야한다. 

하지만 새로운 클래스를 만드는 것은 새로 만든 클래스에 오류가 없는지만 고려하면 된다.


4. 주의 사항

- 데코레이터 패턴은 구성요소의 형식을 알아내어 그 형식을 바탕으로 처리하는 작업에는 부적합하다.

가령, 위의 예제에서 하우스 블렌디드 커피만 특별 할인을 해주는 상황은 처리 하기는 어렵다.


- 데코레이터 패턴을 적용하면 관리해야할 객체가 늘어난다.

데코레이터 패턴을 적용하면, 데코레이터 클래스들이 새로 생겨나기 마련이다. 그에 따라 코딩하는 과정에서 실수가 늘어날 가능성이 있다.


5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- 위키피디아 데코레이터 패턴 : https://en.wikipedia.org/wiki/Decorator_pattern

- 위키피디아 Composition over inheritance : https://en.wikipedia.org/wiki/Composition_over_inheritance#Benefits

- 위키피디아 개방-폐쇄원칙 : https://ko.wikipedia.org/wiki/개방-폐쇄_원칙


블로그 이미지

서기리보이

,