개발/Architecture & Design Pattern

C++ - 브리지 패턴 (Bridge Pattern)

피로물든딸기 2024. 3. 1. 13:01
반응형

C, C++ 전체 링크

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;
};

 

리모컨 ATV를 인자로 받고, 해당 리모컨에 대해 추가로 필요한 메서드를 구현하면 된다.

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 클래스의 메서드 setStringgetString은 선언만 한다.

// 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의 클래스의 구현 내용을 감출 수 있다.

특히 Productprivate, 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     
```
반응형