C++ - 브리지 패턴 (Bridge Pattern)
Architecture & Design Pattern 전체 링크
참고
브리지 패턴 (Bridge Pattern) - 구조 패턴
- 클래스의 집합을 두 개의 추상화와 구현으로 분리하는 패턴
- 인터페이스와 구현을 별도의 클래스로 분리하여 각각 독립적으로 변경할 수 있다.
구현
- Abstraction : 기능적 요구사항을 정의하는 인터페이스를 제공한다.
- Implementation : Abstraction이 정의한 인터페이스를 실제로 구현한다.
- RefinedAbstraction : Abstraction을 확장한 클래스, 추가적인 기능을 정의한다.
- ConcreteImplementation : Implementation의 실제 구현이 된다.
장점
- SRP(단일 책임 원칙), 추상화와 구현의 각각 세부 정보에 집중할 수 있다.
- OCP(개방-폐쇄 원칙), 기존의 코드를 수정하지 않고 새로운 추상화들과 구현들을 추가할 수 있다.
- 추상화된 부분을 구현한 클래스를 변경해도 클라이언트에 영향을 미치지 않는다.
단점
- 결합도가 높은 클래스일수록 브리지 패턴을 적용하면 더 복잡할 수 있다.
- 추상화와 구현을 분리하기 때문에 약간의 오버헤드가 발생할 수 있다.
리모컨과 TV
리모컨은 다음과 같은 기능이 필수로 들어간다.
class RemoteControl
{
public:
virtual void on() = 0;
virtual void off() = 0;
virtual void changeChannel(int c) = 0;
// ...
protected:
int channel;
string tvType;
};
그런데 리모컨은 필수 동작이 TV마다 다르게 코드를 구현해야 한다.
그리고 각 리모컨마다 추가 동작이 있을 수 있다. (nextChannel)
class ConcreteRemoteA : public RemoteControl
{
public:
ConcreteRemoteA() = default;
void on()
{
if (tvType == "Samsung") cout << "Samsung TV On" << endl;
else if (tvType == "Sony") cout << "Sony TV On" << endl;
// or LG, Panasonic, ...
}
// ...
void nextChannel() { cout << "A : Next Channel" << endl; }
// ...
};
리모컨 하나가 여러 TV에 동작을 해야하기 때문에 코드가 복잡해지고, 리모컨과 TV의 결합도가 매우 높아진다.
브리지 패턴 적용
브리지 패턴을 이용해 TV와 리모컨을 분리할 수 있다.
리모컨과 TV를 구성 관계로 만들어서 두 개의 계층 구조를 만들면 된다.
추상화된 부분에 있는 메서드는 구현 클래스의 메서드를 통해 실제로 구현된다. (implementation->method())
먼저 TV가 제공해야 하는 기능을 인터페이스로 정의하자.
class TV
{
public:
virtual void on() = 0;
virtual void off() = 0;
virtual void setChannel(int c) = 0;
virtual void showChannel() = 0;
protected:
int channel;
};
예를 들어 Samsung TV는 다음과 같이 구현할 수 있다.
class SamsungTV : public TV
{
public:
void on() { cout << "Samsung TV On" << endl; }
void off() { cout << "Samsung TV Off" << endl; }
void setChannel(int c) { channel = c; }
void showChannel() { cout << "Samsung TV Channel : " << channel << endl; }
};
리모컨은 TV를 구성 요소로 가지게 되고, 각 메서드는 TV의 구현 클래스에 있는 메서드에 위임한다.
class RemoteControl
{
public:
virtual ~RemoteControl() = default;
virtual void on() { tvInterface->on(); }
virtual void off() { tvInterface->off(); }
virtual void changeChannel(int c) { tvInterface->setChannel(c); }
virtual void showChannel() { tvInterface->showChannel(); }
protected:
TV* tvInterface;
};
리모컨 A는 TV를 인자로 받고, 해당 리모컨에 대해 추가로 필요한 메서드를 구현하면 된다.
class ConcreteRemoteA : public RemoteControl
{
public:
ConcreteRemoteA(TV* tv) { tvInterface = tv; }
void nextChannel() { cout << "A : Next Channel" << endl; }
void prevChannel() { cout << "A : Prev Channel" << endl; }
};
전체 코드는 다음과 같다.
#include <iostream>
using namespace std;
// Interface
class TV
{
public:
virtual void on() = 0;
virtual void off() = 0;
virtual void setChannel(int c) = 0;
virtual void showChannel() = 0;
protected:
int channel;
};
class SamsungTV : public TV
{
public:
void on() { cout << "Samsung TV On" << endl; }
void off() { cout << "Samsung TV Off" << endl; }
void setChannel(int c) { channel = c; }
void showChannel() { cout << "Samsung TV Channel : " << channel << endl; }
};
class SonyTV : public TV
{
public:
void on() { cout << "Sony TV On" << endl; }
void off() { cout << "Sony TV Off" << endl; }
void setChannel(int c) { channel = c; }
void showChannel() { cout << "Sony TV Channel : " << channel << endl; }
};
/* ------------------------------------------------------------------- */
class RemoteControl
{
public:
virtual ~RemoteControl() = default;
virtual void on() { tvInterface->on(); }
virtual void off() { tvInterface->off(); }
virtual void changeChannel(int c) { tvInterface->setChannel(c); }
virtual void showChannel() { tvInterface->showChannel(); }
protected:
TV* tvInterface;
};
class ConcreteRemoteA : public RemoteControl
{
public:
ConcreteRemoteA(TV* tv) { tvInterface = tv; }
void nextChannel() { cout << "A : Next Channel" << endl; }
void prevChannel() { cout << "A : Prev Channel" << endl; }
};
class ConcreteRemoteB : public RemoteControl
{
public:
ConcreteRemoteB(TV* tv) { tvInterface = tv; }
void nextChannel() { cout << "B : Next Channel" << endl; }
void prevChannel() { cout << "B : Prev Channel" << endl; }
};
int main()
{
TV* samsung = new SamsungTV();
TV* sony = new SonyTV();
ConcreteRemoteA* rcA = new ConcreteRemoteA(samsung);
ConcreteRemoteB* rcB = new ConcreteRemoteB(sony);
rcA->on();
rcA->off();
rcA->changeChannel(10);
rcA->showChannel();
rcA->nextChannel();
rcA->prevChannel();
cout << endl;
rcB->on();
rcB->off();
rcB->changeChannel(20);
rcB->showChannel();
rcB->nextChannel();
rcB->prevChannel();
return 0;
}
Pimpl (Pointer to Implementation)
Pimpl은 C++에서 구현 세부 사항을 캡슐화하고, 추상 인터페이스만 노출시키는 디자인 패턴이다.
즉, 브릿지 패턴의 좋은 예시로 볼 수 있다.
Product 클래스는 Product.h에 정의된다.
그리고 Product 클래스의 메서드 setString과 getString은 선언만 한다.
// Product.h
#ifndef __PRODUCT__
#define __PRODUCT__
#include <memory>
class ProductImpl;
class Product
{
public:
Product();
~Product();
void setString(const string& str);
string getString();
private:
unique_ptr<ProductImpl> pImpl;
};
#endif // __PRODUCT__
구체적인 구현은 Product.cpp 파일에서 Product의 모든 메서드(행동)를 ProductImpl 클래스에 위임한다.
// Product.cpp
// #include "Product.h"
#include <string>
// 구현 세부 사항
class ProductImpl
{
public:
void setString(const string& str) { data = str; }
string getString() { return data; }
private:
string data;
};
Product::Product() : pImpl(make_unique<ProductImpl>()) {}
Product::~Product() = default;
void Product::setString(const string& str) { pImpl->setString(str); }
string Product::getString() { return pImpl->getString(); }
위와 같은 방식을 적용하면 Product의 클래스의 구현 내용을 감출 수 있다.
특히 Product에 private, protected 멤버가 많은 경우, 헤더 파일을 통해 클라이언트에 멤버가 노출된다.
하지만 위의 방식은 ProductImpl로 필요한 public 인터페이스만 노출된다.
그리고 구현부에서 실제 필요하지 않은 헤더를 include할 필요가 없어진다.
예를 들어 <memory> 헤더는 Product.h에만 필요하고, <string> 헤더는 Product.cpp에만 선언하면 된다.
마지막으로, 내부 구현이 캡슐화되어 있고 외부 인터페이스가 유지되기 때문에,
구현이 변경되더라도 외부 코드는 수정 없이 이전 보전과 호환될 수 있다. (바이너리 호환성 보증이 쉬워진다.)
전체 코드는 다음과 같다.
#include <stdio.h>
#include <iostream>
using namespace std;
// Product.h
#ifndef __PRODUCT__
#define __PRODUCT__
#include <memory>
class ProductImpl;
class Product
{
public:
Product();
~Product();
void setString(const string& str);
string getString();
private:
unique_ptr<ProductImpl> pImpl;
};
#endif // __PRODUCT__
// Product.cpp
// #include "Product.h"
#include <string>
// 구현 세부 사항
class ProductImpl
{
public:
void setString(const string& str) { data = str; }
string getString() { return data; }
private:
string data;
};
Product::Product() : pImpl(make_unique<ProductImpl>()) {}
Product::~Product() = default;
void Product::setString(const string& str) { pImpl->setString(str); }
string Product::getString() { return pImpl->getString(); }
//
int main(void)
{
Product* product = new Product();
product->setString("Computer");
cout << "String from Product : " << product->getString() << endl;
return 0;
}
ClassDiagram.md
```mermaid
classDiagram
class Abstraction {
implementation
operation()
}
class RefinedAbstractionA {
}
class RefinedAbstractionB {
}
class Implementation {
}
class ConcreteImplementationA {
}
class ConcreteImplementationB {
}
<<abstract>> Abstraction
<<interface>> Implementation
Abstraction <|-- RefinedAbstractionA
Abstraction <|-- RefinedAbstractionB
Implementation <|.. ConcreteImplementationA
Implementation <|.. ConcreteImplementationB
```