#7. [디자인 패턴-구조 패턴] Bridge
Bridge는 추상과 구현을 분리하여, 종속적인 부분을 느슨하게 만들어 주거나, 구현을 다형적으로 만들 수 있게 합니다.
설명
추상화는 공통적인 일반 개념을 만드는 작업입니다. 이를 잘 설계하면, 재활용성이 높아지고, 특정 문제들을 해결하는데 있어서 공통된 접근을 하기 때문에 고민할 것들이 줄어듭니다.
Bridge는 추상화와 이의 구체화를 분리하는 것인데요, 코드간 종속성을 해결하고, 구체화를 다형적으로 만들 수 있어 확장성을 제공해 줍니다. 대부분의 디자인 패턴에서 근간이 되는 구조라고도 할 수 있겠습니다.(Abstract Factory, Builder, Adapter, Decorator, Proxy, State, Strategy 등)
간단하게는 PImpl 이디엄도 Bridge라 할 수 있습니다. 추상과 구현을 분리해서 코드간의 종속성이나 컴파일 종속성을 최소화 해줍니다.
다음 그림에서 Client
는 MyClass
에만 종속되어 있기 때문에, 구현부인 MyClassImpl
이 수정되더라도 새롭게 컴파일할 필요가 없어집니다.
복잡하게는 구현부를 추상 클래스로 만들어 다형적으로 동작하게 할 수 있습니다. 이때에는 m_Impl
을 런타임에 교체할 수도 있어 확장성이 좋아집니다.
다음 그림에서 Client
는 추상부인 Abstraction
만 이용합니다. Client
코드는 Abstraction
의 Implementor
가 내부적으로 ConcreteImplementorA
로 변경되던 ConcreteImplementorB
로 변경되던 아무런 영향을 받지 않으며, 설정된 Implementor
에 따라 다형적으로 동작하게 됩니다.
항목 | 내용 |
---|---|
Abstraction |
추상부 입니다. |
Implementor |
구현부 입니다. 구현부를 추상 클래스로 만들어 다형적으로 확장할 수 있습니다. |
ConcreteImplementorA, ConcreteImplementorB |
Implementor 를 구체화한 개체입니다. |
Client |
Abstraction 을 사용하는 개체입니다. Implementor 는 Client 로부터 완전히 은닉되어 코드간의 종속성이 없습니다. |
특징
구현부를 은닉하여 코드간의 종속성이나 컴파일 종속성을 최소화 할 수 있으며, 구현부를 추상 클래스로 만들어 런타임 다형성을 구현할 수 있습니다.
예제
렌더링 엔진은 운영체제에 따라 다를 수 있고, 선호하는 방식에 따라 다를 수도 있습니다.(예를 들어 Windows 에서도 GDI, GDI+, WPF가 번갈아 사용될 수도 있습니다.)
이런 경우 다음처럼 렌더링을 할때마다 조건 검사를 하면,
1
2
3
4
5
6
7
8
void Draw() {
if (IsGDI()) {
//...
}
else {
// ...
}
}
조건 검사 코드가 여기저기 흩어져 많이 지저분해질 수 있고, 본연의 출력 로직 개발에 방해가 될 수 있습니다. 코드 냄새도 나고요.
이런 경우 Bridge를 이용하여 추상부인 Render
와 구현부인 RenderImpl
을 나누면 좀더 간결하게 문제를 해결할 수 있습니다.
전체적인 구조는 다음과 같습니다.
Rectangle
은 추상화된Render
만 사용하며,RenderImpl
에 독립적입니다.RenderImpl
은 GDI 버전과 WPF 버전이 있으며,Render::SetImpl()
함수로 런타임에 설정할 수 있습니다.- 외부에서 GDI와 WPF를 결정해서 전달해 주므로,
Rectangle
은 GDI인지, WPF인지 고민없이 본연의 출력 로직에만 집중하여 작성하면 됩니다.
다음은 Rectangle
을 Draw()
하는 예입니다. GDIRenderImpl
을 사용하던, WPFRenderImpl
을 사용하던, Rentangle
의 Draw()
에서는 아무런 조건 검사가 필요 없습니다.
- #1 :
Render
추상부 입니다. 출력에 필요한 기본적인 함수들을 추상화합니다. - #2 :
RenderImpl
과의 컴파일 종속성을 없애기 위해Render
의 선언과 정의를 분리했습니다. - #3 :
RenderImpl
은Render
에서 필요로 하는 인터페이스 함수들을 제공합니다. - #4 :
RenderImpl
을 GDI 또는 WPF로 구체화합니다. - #5 :
SetImpl()
함수로 구현부를 런타임에 변경할 수 있습니다. - #6 :
Rectangle
(Client)에서는 어떤 렌더링 엔진을 사용하는지 고민하지 않고 본연의 드로잉 코드만 작성하면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class RenderImpl; // #2
// ----
// #1. 추상부 입니다. m_Impl을 이용합니다.
// ----
class Render {
std::unique_ptr<RenderImpl> m_Impl;
public:
explicit Render(std::unique_ptr<RenderImpl> impl) : m_Impl{std::move(impl)} {
assert(m_Impl);
}
private:
Render(const Render&) = delete;
Render(Render&&) = delete;
Render& operator =(const Render&) = delete;
Render& operator =(Render&&) = delete;
public:
// #5. 구현부를 변경합니다.
void SetImpl(std::unique_ptr<RenderImpl> impl) {
assert(impl);
m_Impl = std::move(impl);
}
// #2. RenderImpl에 컴파일 종속성이 있어 선언과 정의를 분리했습니다.
void DrawLine(int x1, int y1, int x2, int y2);
void DrawImage(int l, int t, const char* filename);
};
// ----
// Client 입니다. 추상부인 Render만 사용하며, RenderImpl에 독립적입니다.
// ----
class Rectangle {
int m_Left;
int m_Top;
int m_Width;
int m_Height;
public:
Rectangle(int l, int t, int w, int h) : m_Left{l}, m_Top{t}, m_Width{w}, m_Height{h} {}
// #6. 어떤 렌더링 엔진을 사용하는지 고민하지 않고 본연의 드로잉 코드만 작성합니다.
void Draw(Render& render) const {
int right{m_Left + m_Width};
int bottom{m_Top + m_Height};
render.DrawLine(m_Left, m_Top, right, m_Top);
render.DrawLine(right, m_Top, right, bottom);
render.DrawLine(right, bottom, m_Left, bottom);
render.DrawLine(m_Left, bottom, m_Left, m_Top);
}
};
// ----
// #3. 구현부의 추상 클래스입니다.
// ----
class RenderImpl {
protected:
RenderImpl() = default; // 다형 소멸을 제공하는 추상 클래스. 상속해서만 사용하도록 protected
public:
virtual ~RenderImpl() = default; // 다형 소멸 하도록 public virtual
private:
RenderImpl(const RenderImpl&) = delete;
RenderImpl(RenderImpl&&) = delete;
RenderImpl& operator =(const RenderImpl&) = delete;
RenderImpl& operator =(RenderImpl&&) = delete;
public:
virtual void DrawLineImpl(int x1, int y1, int x2, int y2) = 0;
virtual void DrawImageImpl(int l, int t, const char* filename) = 0;
};
// ----
// #2. Render 정의부입니다. RenderImpl에 컴파일 종속성이 있어 선언과 정의를 분리했습니다.
// ----
void Render::DrawLine(int x1, int y1, int x2, int y2) {
assert(m_Impl);
m_Impl->DrawLineImpl(x1, y1, x2, y2);
}
void Render::DrawImage(int l, int t, const char* filename) {
assert(m_Impl);
m_Impl->DrawImageImpl(l, t, filename);
}
// ----
// #4. RenderImpl의 실제 구현부 입니다.
// ----
class GDIRenderImpl : public RenderImpl {
public:
virtual void DrawLineImpl(int x1, int y1, int x2, int y2) override {
std::cout << "GDIRenderImpl::DrawLineImpl()" << std::endl;
}
virtual void DrawImageImpl(int l, int t, const char* filename) override {
std::cout << "GDIRenderImpl::DrawImageImpl()" << std::endl;
}
};
class WPFRenderImpl : public RenderImpl {
public:
virtual void DrawLineImpl(int x1, int y1, int x2, int y2) override {
std::cout << "WPFRenderImpl::DrawLineImpl()" << std::endl;
}
virtual void DrawImageImpl(int l, int t, const char* filename) override {
std::cout << "WPFRenderImpl::DrawImageImpl()" << std::endl;
}
};
// ----
// 테스트 코드
// ----
Render render{std::unique_ptr<RenderImpl>{new GDIRenderImpl{}}};
Rectangle rect{0, 0, 10, 20};
// GDIRenderImpl로 실행합니다.
rect.Draw(render);
// #5. 런타임에 구현부를 WPFRenderImpl로 변경하여 실행할 수 있습니다.
render.SetImpl(std::unique_ptr<RenderImpl>{new WPFRenderImpl{}});
rect.Draw(render);
댓글남기기