1.1. 의도
- 인터페이스를 클라이언트가 기대하는 형태의 인터페이스로 변환
1.2. 구현 방법
- Class Adapter를 활용한 구현
- 다중 상속을 활용하여 CoolText의 show()함수를 draw()함수로 탈바꿈
- Object Adapter를 사용하는 경우에 비하여 가상함수를 통해 재정의 할 가능성이 남아 있음.
class ClsAdapter : public CoolText, public Shape { public: ClsAdapter(const std::string& text) : CoolText(text) {} void draw() override { CoolText::show();} };
- Object Adapter를 활용한 구현
- 이미 생성되어 있는 객체를 포인터 혹은 Reference 타입으로 불러와서 해당 객체의 함수를 호출
class ObjAdapter : public Shape // 객체 어댑터 { CoolText* ct; // 객체를 받아올 떄는 보통 이런 형식을 많이 사용한다. public: ObjAdapter(CoolText* ct) : ct(ct) {} void draw() override { ct->show();} };
1.3. STL 에서의 활용
- stack을 구현하고자 할때 이미 다른 클래스에서 잘 만들어진 메모리 관리, 혹은 기본 기능들을 굳이 재정의하여 코드 메모리를 늘리고 싶지 않음
- object adapter를 활용하여 stack을 만들고, policy based design을 통해 사용자에게 선택권을 제공
template<typename T, typename C = std::deque<T> > class stack { C c; // C* c; // object adapter public: constexpr void push(const T& a) { c.push_back(a); } constexpr void pop() { c.pop_back(); } constexpr T& top() { return c.back(); } }; #include <stack> int main() { stack<int, std::vector<int>> s1; // cash 사용에 따라 유리할 수 있음 stack<int, std::list<int>> s2; stack<int> s; s.push(10); // 인라인화가 이루어진다면 s.c.push_back(a) 가 되어 마치 stack은 없는 것처럼 작동하게 된다. s.push(20); // 기계어 코드에는 stack과 관련된 코드들은 없는 것 처럼 받아들여진다. }
1.4. private을 이용한 상속
- 구현은 물려받았지만 인터페이스는 물려받지 않았다
- 상속을 모두 private 영역 안에 받게 하여, 외부에 노출하고 싶지 않은 부모 클래스의 기능을 숨길 수 있음. (java에서의 stack이 vector로부터 일반 상속을 받아 구현되어 push_front와 같은 함수가 ide에 잡히는 문제 발생)
2.1. 의도
- 다른 객체에 접근하기 위한 중간 대리 역할을 하는 객체를 생성, 대리 역할하는 객체에서 다양한 문제를 해결(인증, 보안, 원격지 서버 대행)
2.2 예제
- Image 객체를 호출하면 Image가 로드됨.
- 하지만 주로 하고 싶은 작업은 이미지의 크기를 불러오는 것 >> 불필요하게 메모리가 소모됨.
- ImageProxy 클래스를 생성하여 문제 해결(헤더파일만을 읽는 방식으로 진행)
- 이미지를 그려야 할때 Image 객체를 생성해서 그림을 그림 ( 지연된 생성 )
class Image : public IImage { std::string name; public: Image(const std::string& name) : name(name) { std::cout << "open " << name << '\n'; } void draw() { std::cout << "draw " << name << '\n'; } int width() const { return 100;} int height() const { return 100;} }; class ImageProxy : public IImage { std::string name; Image* img = nullptr; public: ImageProxy(const std::string& name) : name(name) {} int width() const { return 100;} // 파일헤더에서 정보 획득 int height() const { return 100;} // 파일헤더에서 정보 획득 void draw() { if ( img == nullptr ) img = new Image(name); // 꼭 그림을 그려 주어야 할때는 img 객체를 생성해서 진행 // 지연된 생성이라 부름 img->draw(); } };
2.3. 차별점
- 브릿지와 유사하지만, 브릿지는 상호간의 업데이트를 위한 패턴
- 데코레이터와 유사하게 기능을 추가하는 것 같지만, 데코레이터는 중첩의 형태로 진행되며, 인터페이스가 동일함 / 반면에 프록시는 중첩이 안되며 지연된 생성을 활용한다는 점이 다름
3.1. 의도
- 서브 시스템을 합성하는 다수의 객체들의 인터페이스 집합에 대해 일관된 하나의 인터페이스를 제공할 수 있도록 한다.
- 서브 시스템을 사용하기 쉽게 하기 위한 포괄적인 개념의 인터페이스를 정의
3.2. 예제
- 내부적인 코드를 모두 감싸 TCP server에 대해서 몰라도 쉽고 간편하게 사용할 수 있도록 Facade를 생성
class TCPServer // 반복되는것, 내부동작을 알 필요가 없는 동작은 다음과 같이 싸주는 것이 좋다. { NetworkInit init; Socket sock{SOCK_STREAM}; public: void Start(const char* ip, short port) { IPAddress addr(ip, port); sock.Bind(&addr); sock.Listen(); sock.Accept(); } }; int main() { TCPServer server; // 파사드. 이련의 절차에 대한 포괄적인 개념의 인터페이스를 제공 >> 사용자가 사용이 쉬워짐 server.Start("127.0.0.1", 4000); }
4.1 의도
- 구현과 추상화 개념을 분리하여 각각 독립적으로 변형할 수 있게 한다.
- 클라이언트에서 어떤 추가적인 기능을 원할때 인터페이스가 바뀌면 모든 구현 클래스들이 변경되어야 함.
- Abstraction 이라는 가상의 클래스를 통해 간단한 기능은 해당 클래스에서 구현할 수 있고, 구현계층이 변경되더라도 사용자와는 아무 상관이 없다.
4.2. 예제
- 업데이트를 편안하게 하고 싶다면 중간에 계층을 늘려 상호 독립적으로 업데이트! (단 성능 저하가 발생할 수 있음)
class MP3 { IMP3* impl; //P - IMPL point to implementation으로 불리기도 함 . public: MP3(IMP3* p = nullptr) : impl(p) { if (impl == nullptr) impl = new IPod; } void play() { impl->play(); } void stop() { impl->stop(); } void play_one_minute() { impl->play(); // 1분 후에.. impl->stop(); } };
4.3. PIMPL
- Pointer to Implementation 으로 cpp 에서는 유용하나 다른 언어에서는 적합하지 않을 수 있다.
- Point.h가 간접 계층이 되어준다면 PointImpl이 변경되어도 main은 Point.h만을 사용하기 때문에 변경될 것이 없다 (== maind을 전부 재 컴파일 할 필요가 없다.)
- Pointer를 선언할때 include 없이 전방선언으로도 가능하기 때문에 가능한 기법이다.
- 컴파일러 방화벽이자 완벽한 정보 은닉이 가능해진다.(실제 구현 파트는 DLL로 제공)
//#include "PointImpl.h" class PointImpl; // 핵심.. 포인터를 쓸때는 전방선언으로도 충분하다. class Point { PointImpl* impl; public: Point(int x, int y); void print() const; };