디자인 패턴

Composite 패턴

Keisa 2024. 7. 11. 10:21

의도

복합체 패턴은 객체들을 트리 구조들로 구성한 후, 이러한 구조들과 개별 객체들처럼 작업할 수 있도록 하는 구조 패턴입니다.

 문제

복합체 패턴은 앱의 핵심 모델이 트리로 표현될 수 있을 때만 사용하세요.

예를 들어 제품들과 상자들이라는 두 가지 유형의 객체들이 있다고 가정해 봅시다. 상자에는 여러 개의 제품들과 여러 개의 작은 상자들이 포함될 수 있습니다. 이 작은 상자들은 또한 일부 제품들 또는 더 작은 상자들등을 담을 수 있습니다.

이러한 클래스들을 사용하는 주문 시스템을 만들기로 했다고 가정해 보겠습니다. 주문들에는 포장이 없는 단순한 제품들과 제품들로 채워진 상자들 및 다른 상자들이 포함될 수 있습니다. 그러면 그러한 주문의 총가격을 어떻게 계산하시겠습니까?

하나의 주문은 여러 제품으로 구성될 수 있는데, 이 제품들은 상자에 포장될 수 있으며, 다시 그 상자들이 더 큰 상자에 포장되는 식으로 이어질 수 있습니다. 그러면 그 전체 구조는 거꾸로 된 나무와 같은 모양으로 형성될 것입니다.

당신은 이 문제에 직접적으로 접근해 볼 수 있습니다: 모든 상자를 푼 후 내부의 모든 제품을 살펴본 다음 가격의 합계를 계산하는 것입니다. 현실 세계에서는 이러한 접근 방법은 가능하나, 프로그램에서 이 작업은 덧셈 루프를 실행하는 것만큼 간단하지 않은데, 그 이유는 덧셈 루프를 실행하기 위해 진행 중인 제품들  상자들의 클래스들, 상자의 중첩 수준 및 기타 복잡한 세부 사항들을 미리 알고 있어야 하기 때문입니다. 이 모든 것이 상자 및 내부 상자들을 열어 포함된 모든 제품 가격의 합계를 계산하는 직접적인 접근 방식을 어렵게 만듭니다.

 해결책

복합체 패턴은 총가격을 계산하는 메서드를 선언하는 공통 인터페이스를 통해 제품들  상자들 클래스들과 작업할 것을 제안합니다.

그러면 이 메서드는 어떻게 작동할까요? 제품의 경우 이 메서드는 단순히 제품 가격을 반환합니다. 상자의 경우, 이 메서드는 상자에 포함된 각 항목을 살펴보고 가격을 확인한 뒤 해당 상자의 총 가격을 반환합니다. 만약 이 항목들 중 하나가 더 작은 상자라면, 메서드는 해당 상자의 모든 내부 구성 요소의 가격이 계산될 때까지 내용물 등을 살펴봅니다. 메서드는 상자를 다룰 때 최종 가격에 포장 비용 같은 약간의 추가 비용도 추가할 수도 있습니다.

복합체 패턴은 객체 트리의 모든 컴포넌트들에 대해 재귀적으로 행동을 실행할 수 있도록 합니다.

이 접근 방식의 가장 큰 이점은 더 이상 트리를 구성하는 객체들의 구상 클래스들에 대해 신경 쓸 필요도, 또 물건이 단순한 제품인지 내용물이 있는 상자인지 알 필요도 없다는 점입니다. 단순히 공통 인터페이스를 통해 모두 같은 방식으로 처리하시면 됩니다. 당신이 메서드를 호출하면 객체들 자체가 요청을 트리 아래로 전달합니다.

 실제상황 적용

군대 구조의 예시.

대부분의 국가에서 군대는 계층구조로 구성되어 있습니다. 군대는 여러 사단으로 구성되며, 사단은 여단의 집합이고, 여단은 소대의 집합이며, 소대는 또 분대로 나누어질 수 있습니다. 마지막으로 분대는 실제 군인들의 작은 집합입니다. 명령들은 계층구조의 최상위에서 내려와 모든 병사가 자신이 수행해야 할 작업을 알게 될 때까지 계층구조의 각 하위 계층으로

구조

 

의사코드

이 예시에서는 복합체 패턴이 어떻게 그래픽 편집기에서 기하학적 모양 쌓기를 구현할 수 있도록 하는지를 살펴보겠습니다.

기하학적 모양 편집기 예시.

Compound­Graphic 클래스는 다른 복합 모양들을 포함한 모든 하위 모양으로 구성될 수 있는 컨테이너입니다. 복합 모양은 단순 모양과 같은 메서드들을 보유하나, 자체적으로 무언가를 수행하는 대신 복합 모양은 모든 자녀에게 재귀적으로 요청을 전달하고 이의 결과를 '요약'합니다.

클라이언트 코드는 모든 모양 클래스들에 공통인 단일 인터페이스를 통해 모든 모양과 함께 작동합니다. 따라서 클라이언트는 자신이 단순 모양과 작업하는지 복합 모양과 작업하는지를 알지 못합니다. 또 클라이언트는 매우 복잡한 객체 구조들을 형성하는 구상 클래스들에 결합하지 않고도 해당 구조들과 작업할 수 있습니다.

 

#include <algorithm>
#include <iostream>
#include <list>
#include <string>
/**
 * The base Component class declares common operations for both simple and
 * complex objects of a composition.
 */
class Component 
{
  /**
   * @var Component
   */
 protected:
  Component *parent_;
  /**
   * Optionally, the base Component can declare an interface for setting and
   * accessing a parent of the component in a tree structure. It can also
   * provide some default implementation for these methods.
   */
 public:
  virtual ~Component() {}
  void SetParent(Component *parent) 
  {
    this->parent_ = parent;
  }
  Component *GetParent() const 
  {
    return this->parent_;
  }
  /**
   * In some cases, it would be beneficial to define the child-management
   * operations right in the base Component class. This way, you won't need to
   * expose any concrete component classes to the client code, even during the
   * object tree assembly. The downside is that these methods will be empty for
   * the leaf-level components.
   */
  virtual void Add(Component *component) {}
  virtual void Remove(Component *component) {}
  /**
   * You can provide a method that lets the client code figure out whether a
   * component can bear children.
   */
  virtual bool IsComposite() const 
  {
    return false;
  }
  /**
   * The base Component may implement some default behavior or leave it to
   * concrete classes (by declaring the method containing the behavior as
   * "abstract").
   */
  virtual std::string Operation() const = 0;
};
/**
 * The Leaf class represents the end objects of a composition. A leaf can't have
 * any children.
 *
 * Usually, it's the Leaf objects that do the actual work, whereas Composite
 * objects only delegate to their sub-components.
 */
class Leaf : public Component 
{
 public:
  std::string Operation() const override 
  {
    return "Leaf";
  }
};
/**
 * The Composite class represents the complex components that may have children.
 * Usually, the Composite objects delegate the actual work to their children and
 * then "sum-up" the result.
 */
class Composite : public Component 
{
  /**
   * @var \SplObjectStorage
   */
 protected:
  std::list<Component *> children_;

 public:
  /**
   * A composite object can add or remove other components (both simple or
   * complex) to or from its child list.
   */
  void Add(Component *component) override 
  {
    this->children_.push_back(component);
    component->SetParent(this);
  }
  /**
   * Have in mind that this method removes the pointer to the list but doesn't
   * frees the
   *     memory, you should do it manually or better use smart pointers.
   */
  void Remove(Component *component) override 
  {
    children_.remove(component);
    component->SetParent(nullptr);
  }
  bool IsComposite() const override 
  {
    return true;
  }
  /**
   * The Composite executes its primary logic in a particular way. It traverses
   * recursively through all its children, collecting and summing their results.
   * Since the composite's children pass these calls to their children and so
   * forth, the whole object tree is traversed as a result.
   */
  std::string Operation() const override 
  {
    std::string result;
    for (const Component *c : children_) 
    {
      if (c == children_.back()) 
      {
        result += c->Operation();
      } 
      else 
      {
        result += c->Operation() + "+";
      }
    }
    return "Branch(" + result + ")";
  }
};
/**
 * The client code works with all of the components via the base interface.
 */
void ClientCode(Component *component) 
{
  // ...
  std::cout << "RESULT: " << component->Operation();
  // ...
}

/**
 * Thanks to the fact that the child-management operations are declared in the
 * base Component class, the client code can work with any component, simple or
 * complex, without depending on their concrete classes.
 */
void ClientCode2(Component *component1, Component *component2) 
{
  // ...
  if (component1->IsComposite()) 
  {
    component1->Add(component2);
  }
  std::cout << "RESULT: " << component1->Operation();
  // ...
}

/**
 * This way the client code can support the simple leaf components...
 */

int main() 
{
  Component *simple = new Leaf;
  std::cout << "Client: I've got a simple component:\n";
  ClientCode(simple);
  std::cout << "\n\n";
  /**
   * ...as well as the complex composites.
   */

  Component *tree = new Composite;
  Component *branch1 = new Composite;

  Component *leaf_1 = new Leaf;
  Component *leaf_2 = new Leaf;
  Component *leaf_3 = new Leaf;
  branch1->Add(leaf_1);
  branch1->Add(leaf_2);
  Component *branch2 = new Composite;
  branch2->Add(leaf_3);
  tree->Add(branch1);
  tree->Add(branch2);
  std::cout << "Client: Now I've got a composite tree:\n";
  ClientCode(tree);
  std::cout << "\n\n";

  std::cout << "Client: I don't need to check the components classes even when managing the tree:\n";
  ClientCode2(tree, simple);
  std::cout << "\n";

  delete simple;
  delete tree;
  delete branch1;
  delete branch2;
  delete leaf_1;
  delete leaf_2;
  delete leaf_3;

  return 0;
}