디자인 패턴

Bridge 패턴

Keisa 2024. 7. 3. 16:17

 의도

브리지는 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조​(추상화 및 구현)​로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 디자인 패턴입니다.

 문제

? ? 어렵게 들리시나요? 진정하세요. 그리고 간단한 예시를 한번 살펴봅시다.

Circle​(원) 및 Square​(직사각형)​라는 한 쌍의 자식 클래스들이 있는 기하학적 Shape​(모양) 클래스가 있다고 가정해 봅시다. 이 클래스 계층 구조를 확장하여 색상을 도입하기 위해 Red​(빨간색) 및 Blue​(파란색) 모양들의 자식 클래스들을 만들 계획입니다. 그러나 이미 두 개의 자식 클래스가 있으므로 Blue­Circle​(파란색 원) 및 Red­Square​(빨간색 직사각형)​와 같은 네 가지의 클래스 조합을 만들어야 합니다.

클래스 조합들의 수는 기하급수적으로 증가합니다.

새로운 모양 유형들과 색상 유형들을 추가할 때마다 계층 구조는 기하급수적으로 성장합니다. 예를 들어, 삼각형 모양을 추가하려면 각 색상별로 하나씩 두 개의 자식 클래스들을 도입해야 합니다. 그리고 그 후에 또 새 색상을 추가하려면 각 모양 유형별로 하나씩 세 개의 자식 클래스를 만들어야 합니다. 유형들이 많아지면 많아질수록 코드는 점점 복잡해집니다.

 해결책

이 문제는 모양과 색상의 두 가지 독립적인 차원에서 모양 클래스들을 확장하려고 하기 때문에 발생합니다. 이것은 클래스 상속과 관련된 매우 일반적인 문제입니다.

브리지 패턴은 상속에서 객체 합성으로 전환하여 이 문제를 해결하려고 시도합니다. 이것이 의미하는 바는 차원 중 하나를 별도의 클래스 계층구조로 추출하여 원래 클래스들이 한 클래스 내에서 모든 상태와 행동들을 갖는 대신 새 계층구조의 객체를 참조하도록 한다는 것입니다.

클래스 계층구조의 기하급수적인 성장을 방지하기 위하여 그것을 여러 관련 계층구조들로 변환할 수 있습니다.

이 접근 방식을 따르면, 색상 관련 코드를 Red  Blue라는 두 개의 자식 클래스들이 있는 자체 클래스로 추출할 수 있습니다. 그런 다음 Shape 클래스는 색상 객체들 중 하나를 가리키는 참조 필드를 받습니다. 이제 모양은 연결된 색상 객체에 모든 색상 관련 작업을 위임할 수 있습니다. 이 참조는 Shape  Color 클래스들 사이의 브리지​(다리) 역할을 할 것입니다. 이제부터 새 색상들을 추가할 때 모양 계층구조를 변경할 필요가 없으며 그 반대의 경우도 마찬가지입니다.

추상화와 구현

GoF의 디자인 패턴 은 브리지 패턴 정의의 일부로   이라는 용어들을 소개합니다. 저는 위 용어들이 너무 학문적이라 그로 인해 패턴이 실제보다 더 복잡하게 들린다고 생각합니다. 그러면 GoF의 책의 난해한 용어들 뒤에 숨겨진 의미를 모양과 색상이 있는 간단한 예를 통해 해독해 보겠습니다.

(라고도 함)​는 일부 개체​(entity)​에 대한 상위 수준의 제어 레이어입니다. 이 레이어는 자체적으로 실제 작업을 수행해서는 안 되며, 작업들을  레이어​(이라고도 함)​에 위임해야 합니다.

참고로 지금 우리가 이야기하는 것은 당신이 선호하는 프로그래밍 언어의 이나  이 아닙니다. 이것들은 같은 것이 아닙니다.

실제 앱을 예로 들면 추상화는 그래픽 사용자 인터페이스이며 구현은 그래픽 사용자 인터페이스 레이어가 사용자와 상호작용하여 그 결과로 호출하는 배경 운영 체제 코드​(API)​입니다.

일반적으로 이러한 앱은 두 가지 독립적인 방향으로 확장할 수 있습니다.

  • 다른 여러 가지의 그래픽 사용자 인터페이스를 가진다 (예: 일반 고객 또는 관리자용으로 맞춘 인터페이스들).
  • 여러 다른 API들을 지원한다 (예: 맥, 리눅스 및 윈도우에서 앱을 실행할 수 있는 API들).

최악의 경우 이 앱은 수백 개의 조건문들이 코드 전체에 다양한 API와 다양한 유형의 그래픽 사용자 인터페이스들을 연결한 거대한 스파게티 코드 그릇처럼 형성됩니다.

를 잘 이해해야 하므로 모놀리식 코드베이스는 간단한 변경을 하는 것조차 매우 어렵습니다. 반면에 잘 정의된 작은 모듈들을 변경하는 것이 훨씬 쉽습니다.

당신은 특정 인터페이스-플랫폼 조합들과 관련된 코드를 별도의 클래스들로 추출하여 이 복잡함에 질서를 부여할 수 있으나, 곧 이러한 클래스들이  있다는 것을 알게 될 것입니다. 새로운 그래픽 사용자 인터페이스를 추가하거나 다른 API를 지원하려면 점점 더 많은 클래스를 생성해야 하므로 클래스 계층구조는 기하급수적으로 성장할 것입니다.

브리지 패턴으로 이 문제를 해결해 봅시다. 브리지 패턴은 클래스들을 두 개의 계층구조로 분리하라고 제안합니다:

  • 추상화: 앱의 그래픽 사용자 인터페이스 레이어.
  • 구현: 운영 체제의 API.

크로스 플랫폼 앱을 구성하는 방법 중 하나.

추상화 객체는 앱의 드러나는 모습을 제어하고 연결된 구현 객체에 실제 작업들을 위임합니다. 서로 다른 구현들은 공통 인터페이스를 따르는 한 상호 호환이 가능하며, 이에 따라 같은 그래픽 사용자 인터페이스는 리눅스와 윈도우에 동시에 작동할 수 있습니다.

따라서 당신은 API 관련 클래스들을 건드리지 않고 그래픽 사용자 인터페이스 클래스들을 변경할 수 있습니다. 그리고 다른 운영 체제에 대한 지원을 추가하려면 구현 계층구조 내에 자식 클래스를 생성하기만 하면 됩니다.

 

 구조

  1. 추상화는 상위 수준의 제어 논리를 제공하며, 구현 객체에 의존해 실제 하위 수준 작업들을 수행합니다.
  2. 구현은 모든 구상 구현들에 공통적인 인터페이스를 선언하며, 추상화는 여기에 선언된 메서드들을 통해서만 구현 객체와 소통할 수 있습니다.
  3. 추상화는 구현과 같은 메서드들을 나열할 수 있지만 보통은 구현이 선언한 다양한 원시 작업들에 의존하는 몇 가지 복잡한 행동들을 선언합니다.
  4. 구상 구현들에는 플랫폼별 맞춤형 코드가 포함됩니다.
  5. 정제된 추상화들은 제어 논리의 변형들을 제공합니다. 그들은 그들의 부모처럼 일반 구현 인터페이스를 통해 다른 구현들과 작업합니다.
  6. 일반적으로 클라이언트는 추상화와 작업하는데만 관심이 있습니다. 그러나 추상화 객체를 구현 객체들 중 하나와 연결하는 것도 클라이언트의 역할입니다.

 의사코드

이 예시는 브리지 패턴이 기기와 리모컨을 관리하는 앱의 모놀리식 코드를 나누는 데 어떻게 도움이 되는지 보여줍니다. Device 클래스들은 구현의 역할을 하는 반면, Remote클래스들은 추상화 역할을 합니다.

원조 클래스의 계층구조는 두 부분으로 나뉩니다: 장치들과 리모컨들.

기초 리모컨 클래스는 이 클래스를 장치 객체와 연결하는 참조 필드를 선언합니다. 모든 리모컨은 일반 장치 인터페이스를 통해 장치들과 작동하므로 같은 리모컨이 여러 장치 유형을 지원할 수 있습니다.

장치 클래스들과 독립적으로 리모컨 클래스들을 개발할 수 있으며, 필요한 것은 새로운 리모컨 자식 클래스를 만드는 것뿐입니다. 예를 들어 기초 리모컨에는 버튼이 두 개뿐일 수 있지만, 추가 터치스크린과 추가 배터리 같은 기능들도 가지도록 확장할 수 있습니다.

클라이언트 코드는 Remote의 생성자를 통해 원하는 유형의 리모컨을 특정 장치 객체와 연결합니다.

 

/**
 * The Implementation defines the interface for all implementation classes. It
 * doesn't have to match the Abstraction's interface. In fact, the two
 * interfaces can be entirely different. Typically the Implementation interface
 * provides only primitive operations, while the Abstraction defines higher-
 * level operations based on those primitives.
 */

class Implementation {
 public:
  virtual ~Implementation() {}
  virtual std::string OperationImplementation() const = 0;
};

/**
 * Each Concrete Implementation corresponds to a specific platform and
 * implements the Implementation interface using that platform's API.
 */
class ConcreteImplementationA : public Implementation {
 public:
  std::string OperationImplementation() const override {
    return "ConcreteImplementationA: Here's the result on the platform A.\n";
  }
};
class ConcreteImplementationB : public Implementation {
 public:
  std::string OperationImplementation() const override {
    return "ConcreteImplementationB: Here's the result on the platform B.\n";
  }
};

/**
 * The Abstraction defines the interface for the "control" part of the two class
 * hierarchies. It maintains a reference to an object of the Implementation
 * hierarchy and delegates all of the real work to this object.
 */

class Abstraction {
  /**
   * @var Implementation
   */
 protected:
  Implementation* implementation_;

 public:
  Abstraction(Implementation* implementation) : implementation_(implementation) {
  }

  virtual ~Abstraction() {
  }

  virtual std::string Operation() const {
    return "Abstraction: Base operation with:\n" +
           this->implementation_->OperationImplementation();
  }
};
/**
 * You can extend the Abstraction without changing the Implementation classes.
 */
class ExtendedAbstraction : public Abstraction {
 public:
  ExtendedAbstraction(Implementation* implementation) : Abstraction(implementation) {
  }
  std::string Operation() const override {
    return "ExtendedAbstraction: Extended operation with:\n" +
           this->implementation_->OperationImplementation();
  }
};

/**
 * Except for the initialization phase, where an Abstraction object gets linked
 * with a specific Implementation object, the client code should only depend on
 * the Abstraction class. This way the client code can support any abstraction-
 * implementation combination.
 */
void ClientCode(const Abstraction& abstraction) {
  // ...
  std::cout << abstraction.Operation();
  // ...
}
/**
 * The client code should be able to work with any pre-configured abstraction-
 * implementation combination.
 */

int main() {
  Implementation* implementation = new ConcreteImplementationA;
  Abstraction* abstraction = new Abstraction(implementation);
  ClientCode(*abstraction);
  std::cout << std::endl;
  delete implementation;
  delete abstraction;

  implementation = new ConcreteImplementationB;
  abstraction = new ExtendedAbstraction(implementation);
  ClientCode(*abstraction);

  delete implementation;
  delete abstraction;

  return 0;
}

 

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

Decorator 패턴  (0) 2024.07.11
Composite 패턴  (0) 2024.07.11
Adapter 패턴  (0) 2024.06.29
Singleton 패턴  (0) 2024.06.26
Prototype 패턴  (0) 2024.06.26