끝나지 않는 프로그래밍 일기

Abstract factory pattern을 이용하면 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 concrete class를 지정하지 않고도 생성할 수 있다. abstract factory를 바탕으로 똑같은 제품을 다른 방식으로 구현하는 서로 다른 concrete factory를 만들어낼 수 있다.


구조


UML 클래스 다이어그램


  • AbstractFactory: 모든 concrete factory는 이를 구현해야 한다. 제품을 생산하기 위한 일련의 메서드들이 정의되어 있다.
  • ConcreteFactory: 서로 다른 제품군(product family)을 구현한다. client에서 제품이 필요하면 이 factory 가운데 적당한 걸 골라서 쓰면 되기 때문에 제품 객체의 인스턴스를 직접 만들 필요가 없다. 
  • AbstractProduct: 각 concrete factory에서 필요한 제품 객체에 대한 인터페이스를 정의한다. 
  • ConcreteProduct: 구체적으로 factory가 생성할 객체를 정의하고, AbstractProduct가 정의하는 인터페이스를 구현한다. 
  • Client: 이를 만들 때는 abstract factory를 바탕으로 만든다. 실제 factory는 실행시에 결정된다.


장단점

  1. concrete class를 분리한다. 이는 client와 factory에서 생산되는 제품을 분리시킬 수 있다는 말이다. 이는 client가 abstract interface를 통해서만 인스턴스를 조작하며, 실제로 어떤 제품이 생산되는지는 전혀 알 필요가 없다.
  2. 제품군을 쉽게 대체할 수 있도록 만든다. concrete factory 클래스는 application에서 한 번만 나타나기 때문에, application이 사용할 concrete factory를 변경하는 것은 쉽다. 그리고, concrete factory를 변경함으로써 application은 간단하게 다른 제품 구성을 사용할 수 있다.
  3. 제품 사이의 일관성을 증진시킨다. 하나의 군 안에 있는 제품 객체들이 함께 동작하도록 설계되어 있을 때, application은 한 번에 오직 한 군에서 만든 객체를 사용하도록 하는 것으로 일관성을 가지도록 해야한다. abstract factory를 사용하면 이를 아주 쉽게 보장할 수 있다.
  4. 새로운 종류의 제품을 제공하는 것이 어렵다. 제품군에 제품을 추가하거나 하는 식으로 관련된 제품들을 확대해야 하는 경우에는 인터페이스를 변경해야 한다. 이로 인해, 모든 자식 클래스의 인터페이스를 바꿔야 한다는 문제점이 있다.

예시

아래의 MazeGame 클래스는 실제로 미로를 생성하는 클래스로, 아래의 메서드 CreateMaze()는 방 사이에 문이 있는 두 개의 방으로 구성된 미로를 만든다.

Maze* MazeGame::CreateMaze() {
	Maze* aMaze = new Maze;
	Room* r1 = new Room(1);
	Room* r2 = new Room(2);
	Door* theDoor = new Door(r1, r2);

	r1->SetSide(North, new Wall);
	r1->SetSide(East, theDoor);
	r1->SetSide(South, new Wall);
	r1->SetSide(West, new Wall);

	r2->SetSide(North, new Wall);
	r2->SetSide(East, new Wall);
	r2->SetSide(South, new Wall);
	r2->SetSide(West, theDoor);

	return aMaze;
}

위의 CreateMaze() 메서드는 다른 메서드를 호출하여 방들 사이에 문이 있는 방 두 개짜리 미로를 만들어 낸다. 위 메서드는 'Maze', 'Room', 'Door'와 같이 클래스명을 하드코딩 해놨기 때문에 폭탄이 들어있는 방이라던가, 어떤 아이템이 들어있는 방 등을 미로에 추가하기가 힘들다. 

class MazeFactory {
public:
	MazeFactory();

	virtual Maze* MakeMaze() const
	{ return new Maze; }

	virtual Wall* MakeWall() const
	{ return new Wall; }

	virtual Room* MakeRoom(int n) const
	{ return new Room(n); }

	virtual Door* MakeDoor(Room* c1, Room* c2) const
	{ return new Door(c1, c2); }
};

이러한 문제는 아래와 같이 CreateMaze() 메서드가 위 MazeFactory 클래스의 인스턴스를 매개변수로 받도록 하여 해결할 수 있다.

Maze* MazeGame::CreateMaze(MazeFactory& factory) {
	Maze* aMaze = factory.MakeMaze();
	Room* r1 = factory.MakeRoom(1);
	Room* r2 = factory.MakeRoom(2);
	Door* aDoor = factory.MakeDoor(r1, r2);

	aMaze->AddRoom(r1);
	aMaze->AddRoom(r2);

	r1->SetSide(North, factory.MakeWall());
	r1->SetSide(East, aDoor);
	r1->SetSide(South, factory.MakeWall());
	r1->SetSide(West, factory.MakeWall());

	r2->SetSide(North, factory.MakeWall());
	r2->SetSide(East, factory.MakeWall());
	r2->SetSide(South, factory.MakeWall());
	r2->SetSide(West, aDoor);

	return aMaze;
}

여기서 예를 들어 폭탄이 들어있는 방을 만들고 싶다면, MazeFactory를 상속하고 메서드 MakeWall()와 MakeRoom()을 재정의하여 각각 방에 들어갔을 때 폭탄을 처리하는 클래스인 RoomWithABomb와 폭탄이 터진 후 벽에 손상이 갔을 때 벽의 모습을 바뀌도록 하기 위한 BombedWall 클래스의 인스턴스를 반환하게 만든다.

class BombedMazeFactory : public MazeFactory {
public:
	BombedMazeFactory();

	virtual Wall* MakeWall() const 
	{
		return new BombedWall;
	}

	virtual Room* MakeRoom(int n) const
	{
		return new RoomWithABomb(n);
	}
};

여기서 RoomWithABomb 클래스는 BombedWall 멤버에 접근해야 하므로 Wall*에 대한 매개변수를 BombedWall*에 대한 매개변수로 변환해야 한다. 이러한 타입 변환은 모든 벽들이 BombedMazeFactory 내에서만 사용하고 있고, BombedWall은 Wall의 자식 클래스(또는 서브 클래스)이므로 가능하다.

'메모 > Design pattern' 카테고리의 다른 글

메모. Proxy pattern  (0) 2019.11.18
메모. Facade pattern  (0) 2019.11.17
메모. Factory method pattern  (0) 2019.05.19
메모. Builder pattern  (0) 2019.05.18
메모. Concrete class  (1) 2019.05.13
메모. Abstract factory pattern  (0) 2019.05.13