디자인 패턴/GoF

[디자인 패턴] 구조 패턴 (3) 복합체 Composite

로파이 2021. 1. 27. 18:03

복합체 (Composite) 패턴

 

1. 의도

  • 객체 구조가 트리 형태로 나타낼 수 있다. 객체의 부분은 또 다른 객체의 전체를 표현한다.

2. 활용

Grahpic 객체와 Picture 객체

복합체 패턴 예시

  • Graphic 객체는 화면에 표시할 수 있는 모든 객체가 될 수 있다. 한 객체는 또 다른 Graphic 객체들을 포함하고 하나의 그룹처럼 행동할 수 있다.
  • 예를 들면, MS PowerPoint의 도형 객체들을 그룹하하여 하나의 객체처럼 이동하거나 확대, 축소 연산이 가능하다.
  • 이러한 그룹으로 묶어진 객체들을 컨테이너라고 하고 예시에서 Picture 클래스는 컨테이너로서 여러 그림 요소를 포함할 수 있다.
  • 복합체 패턴에서 Graphic 클래스는 컨테이너와 개별 그림 요소의 의미를 모두 포함하도록 추상화하여 정의한다.

따라서 복합 객체, Graphic 클래스는 기본적인 그리기 연산 Draw()와 컨테이너에 그림 요소를 추가 Add(), 삭제 Remove(), 그림 요소 중 하나를 반환 GetChild(int) 등의 연산을 포함한다.

 

복합 객체는 트리 구조를 가지며 트리의 특성에 따라 최상위 계층 부터 자식 노드를 따라가며 또 다른 복합 객체를 재귀적으로 탐색가능하다.

 

다음과 같은 경우 사용한다.

  • 부분-전체 객체 계통을 표현할 때
  • 사용자가 복합 객체와 개별 객체의 차이를 구분하지 않고 똑같이 취급하게 하고 싶을 때

3. 참여자 

  • Component: 모든 객체를 포괄하는 인터페이스를 정의.
  • Leaf: 자식이 없는 개별 객체.
  • Composite: 자식이 있는 구성요소에 대한 행동을 정의. Component 인터페이스에 정의된 자식 관련 연산을 구현.
  • Client: Component 인터페이스를 통해 모든 객체를 조작.

4. 협력 방법

  • 사용자는 복합 구조 내 객체 간의 상호작용을 위해 Component 클래스 인터페이스를 사용.

5. 결과

  • 기본 객체와 복합 객체가 일관성을 가진다.
  • 사용자 코드가 단순해진다.
  • 새로운 종류의 구성요소를 쉽게 추가 가능하다.

6. 구현

복합체 패턴을 구현할 때 고려할 사항

1) 부모 객체에 대한 참조자

  • Component 클래스에 부모 객체에 대한 참조자를 정의하면 상속받는 Composite과 Leaf 모두 부모 객체에 대한 참조자를 가질 수 있다.
  • 부모 참조자로 트리를 거슬러 올라가거나 parent->Remove("name") 등의 연산으로 삭제를 간편하게 할 수 있다.

2) 구성요소 공유

  • 다른 부모에서 동일한 자식을 공유한다면 구성요소를 공유할 수 있다.

3) Component 인터페이스를 최대화

  • 사용자가 어떤 Leaf나 Composite 클래스가 존재하는 지 모르게 하고 Composite과 Leaf에 정의된 모든 연산을 정의하고 있어야 한다.
  • Leaf에 자식 노드 처리와 관련한 Component의 인터페이스를 사용하지 않는 연산이 있는데 이에 대한 함수 구현 내용을 아무것도 반환하지 않도록 한다.

4) 자식을 관리하는 연산 선언

  • 되도록이면 Component에 모든 연산을 정의한다.

그렇다면 Leaf가 자식 노드와 관련한 연산, 의미없는 연산을 호출시 어떻게 처리할 것인가?

Composite* GetCompsite();

연산을 Component 클래스에 선언하고 실 객체가 Composite* 일 때만 Add(), Remove()등의 연산을 수행할 수 있게 한다.

class Compsite;

class Component{
public:
	// ...
    virtual Composite* GetComposite() {return 0;}
};

class Composite : public Component{
public:
	void Add(Component*);
    // ...
    virtual Composite* GetComposite() {return this;}
};

class Leaf : public Component{
	// ...
};

GetComponent() 연산 결과에 따라 Composite 클래스가 아니면 0가 반환되는 것을 이용하여 Composite 클래스 일 때만 자식 노드 처리 연산을 수행할 수 있게 한다.

// Composite 인스턴스와 Leaf 인스턴스 생성
Composite* aComposite = new Composite;
Leaf* aLeaf = new Leaf;
// Component 참조자
Component* aComponent;
Composite* test;
// 실제 객체는 Composite
aComponent = aComposite;
if (test == aComponent->GetComposite())
{
	// 실행 OK
	test->Add(new Leaf);
}
// 실제 객체는 Leaf
aComponent = aLeaf;
if (test = aComponent->GetComposite())
{
	// 실행 X
	test->Add(new Leaf);
}

혹은 Leaf가 해당 자식 노드 관련 연산 호출시 오류가 나도록 처리할 수 있다.

 

5) 자식 노드를 관리하는 List 정의 부분

  • Component에 List를 정의할 시 모든 Leaf가 자식 노드가 없음에도 List를 위한 메모리가 할당되므로 Composite에 자식들에 대한 List를 관리하도록한다.

6) 자식들의 순서를 고려

  • 어떤 순서로 자식들을 접근할 것인가
  • 정렬된 순? 정렬되지 않아도 상관없는 객체들인가

7) 성능 개선을 위한 캐싱

  • 트리 구조에서 어떤 자식 노드를 찾아가기 위한 경로에 대한 정보를 Composite 클래스에 담는 방법.
  • 자식 노드가 바뀔때 경로 캐시를 업데이트 해야한다.

8) 구성요소 삭제 책임

  • 가비지 컬렉션을 제공하지 않는 언어에서 Composite 클래스가 자식의 삭제를 한다.
  • 자식노드가 공유 객체인지도 고려.

9) 구성요소를 저장하기 위해 적절한 데이터 구조

  • 연결리스트, 배열, 트리, 해시 테이블 등을 이용하여 자식들을 저장할 수 있다.

예제 코드

컴퓨터 부품을 나타내는 객체를 복합체 패턴으로 구성하자.

 

Equipment.h

  • Composite과 Leaf를 포괄할 수 있는 Component 클래스이다.
  • 반복자를 통해 자신에게 포함된 모든 노드를 재귀적으로 탐색하여 NetPrice()와 DiscountPrice()를 구할 수 있다.
  • 또한 자식 노드 처리에 관한 인터페이스가 정의되어 있다.
  • Component 클래스에 List가 정의되어 있지 않다.
class Equipment
{
public:
	virtual ~Equipment();
	const char* Name() { return _name; }

	virtual Watt Power();
	virtual Currency NetPrice();
	virtual Currency DiscountPrice();

	virtual void Add(Equipment*);
	virtual void Remove(Equipment*);
	virtual Iterator<Equipment*>* CreateIterator();
protected:
	Equipment(const char*);
private:
	const char * _name;
};

FloppyDisk.h

Equipment를 상속하는 단순 Leaf 클래스 (FloppyDisk) 예시

class FloppyDisk : public Equipment
{
public:
	FloppyDisk(const char*);
	virtual  ~FloppyDisk();
	virtual Watt Power();
	virtual Currency NetPrice();
	virtual Currency DiscountPrice();
};

CompositeEquipment.h

자식 노드를 관리하는 List 자료구조가 있으며 자신에게 자식 노드를 추가하거나 삭제하는 등 연산을 구현한다.

반복자를 이용한 NetPrice()와 Discount()를 구하는 실제 연산을 구현한다.

class CompositeEquipment : public Equipment {
public:
	virtual  ~CompositeEquipment();
	virtual Watt Power();
	virtual Currency NetPrice();
	virtual Currency DiscountPrice();

	virtual void Add(Equipment*);
	virtual void Remove(Equipment*);
	virtual Iterator<Equipment*>* CreateIterator();
protected:
	CompositeEquipment(const char*);
private:
	List<Equipment*> _equipment;
};

CompositeEquipment를 상속하는 여러 Composite 클래스

또 다른 구성요소를 포함하고 있을 수 있다.

ex) Chassis.h, Bus.h, Cabinet.h

class Chassis : public CompositeEquipment {
public:
	Chassis(const char*);
	virtual ~Chassis();
	virtual Watt Power();
	virtual Currency NetPrice();
	virtual Currency DiscountPrice();
};

모든 구성요소를 조립하면 트리 관계를 만들 수 있다. 

A -> B : A 부모, B 자식 관계

Cabinet -> Chassis

              Chassis ->  Bus -> Card (Leaf)

              Chassis ->  FloppyDisk (Leaf)

	Cabinet* cabinet = new Cabinet("PC Cabinet");
	Chassis* chassis = new Chassis("PC Chassis");

	cabinet->Add(chassis);

	Bus* bus = new Bus("MCA Bus");
	bus->Add(new Card("16Mbs Token Ring"));
	chassis->Add(bus);
	chassis->Add(new FloppyDisk("3.5bin Floppy"));

	cout << "The net price is " << chassis->NetPrice() << endl;