디자인 패턴/GoF

[디자인 패턴] 구조 패턴 (1) 적응자 Adapter

로파이 2021. 1. 26. 16:16

적응자 (Adapter) 패턴

 

1. 의도

  • 다른 클래스의 인터페이스를 가져와서 새로 만들거나 기존에 있던 클래스에 추가하는 것

2. 활용

  • 기존 클래스에서 기능을 추가해야하나 상위 추상 클래스를 손보고 싶지 않을 때
  • 다른 클래스에서 기능을 가져오되 다른 클래스 역시 수정하고 싶지 않을 때
  • 한마디로 두 개의 인터페이스를 다중 상속하여 두 기능을 갖고 싶을 때 적용

예시 TextShape와 TextView

 

적응자 예시

  • 추상 클래스 Shape를 상속받는 Line과 TextShape 객체가 있는데 이들은 특정 모양을 나타내고 그래픽 요소로서 Client가 화면에 그리기 위한 기본 객체이다.
  • 객체의 바운더리를 얻는 BoundingBox() 메서드와 이 객체를 다루는 핸들러?를 반환하는 CreateManipulator()가 있다.
  • CreateManipulator() 개념은 팩토리 메서드에서 언급하였는데, 각 객체와 클래스 계통이 연결된 특정 핸들러 객체를 생성한다.
  • 여기서 TextShape 객체를 그리기에 앞서 추가적으로 객체에 포함된 문자열을 처리하는 복잡한 기능이 필요하다고 하자.
  • 우리는 이 기능을 위해 Shape 상위 추상 클래스에 문자열 처리 인터페이스를 추가하기에는 부담스럽다.
  • 또한 프레임워크에서 기존 TextView 객체를 Shape에서 상속시켜 새로운 TextShape를 만들기에도 TextView와 관련된 것이 많아 부담스럽다.
  • 따라서 Shape의 기본 인터페이스와 TextView의 GetExtent() 인터페이스를 적절히 혼합하여 사용하는 방법이 Adapter 패턴이다.

3. 구조

  • Shape와 같이 원래 종속적인 인터페이스를 Target
  • 두 인터페이스를 사용하는 Adapter
  • Adpater의 인터페이스 적응 대상인 TextView를 Adaptee

4. 종류

 

적응자 패턴은 두 가지로 구현할 수 있다.

 

  1) 클래스 적응자

클래스 적응자

  • 두 인퍼테이스를 모두 상속받아 Adapter에서 새롭게 구현하는 방법이다.
  • Target과 Adaptee가 관련이 적을 때, 또 다중 상속으로 복합 기능을 가진 Adapter로 의미가 클 때 사용한다.
  • 객체 적응자보다 Adapter에서 Target과 Adaptee를 합성하는 느낌이 강하다.
  • Adaptee를 상속받는 것이기 때문에 Adaptee의 또다른 서브 클래스의 기능을 사용할 수 는 없다.

  2) 객체 적응자

객체 적응자

  • Adaptee 인스턴스를 포함시켜 기존 Adaptee의 구현된 내용을 사용하는 방법이다.
  • 예시에서 TextShape의 BoundingBox() 메서드 내부에서 TextView 인스턴스 text의 GetExtent()를 사용한다.
  • 클래스 적응자처럼 완전 합체가 불필요하거나 불가능할 때 인스턴스를 통해 기능을 사용한다.
  • Adaptee라는 인스턴스는 Adaptee의 서브 클래스를 내포하고 이들의 기능을 Adapter에서 사용가능하게 된다.

  3) 추가 고려사항

  • Adaptee의 기능을 얼마나 차용할 것인가, Target 인터페이스에 적응시키기위해 들어가는 노력이 적당한가?
  • 대체 가능 적응자: 적응시 과도한 인터페이스 구성을 지양하고 최소한의 인터페이스 구성으로 클래스 재사용성을 높인다.
  • 양방향 적응자: Adaptee를 통해 Target를 사용하거나 Target를 사용해 Adaptee를 사용함 -> 클래스 적응자로 구현

구현

1. 클래스 적응자를 C++로 구현

class Adapter : public Target, private Adaptee{

             // implements Target method
             // implements Adaptee method

}

  • Target을 public, Adpatee를 private로 상속받는다.
  • Target 인터페이스는 Adapter에서 공개되지만 Adaptee 인터페이스는 내부 구현에 필요하기 때문에 보일 필요가 없다.
  • Adapter는 Target의 서브 클래스이고 Adaptee의 서브 클래스는 아니게 된다.

2. 대체 가능 적응자 - TreeDisplay 문제

- 고찰

Target에 트리 순회와 관련된 다양한 연산이 있을 때,

Target으로부터 상속받은 인터페이스 중 어떤 인터페이스를 최소한으로 구현하면서 그래픽 요소를 나타낼 수 있을 것 인가?

 

- 구현해야하는 필요 최소 연산

GetChildren(n): n 노드의 모든 자식을 반환하는 연산

CreateGraphicNode(child): child 노드에 대한 그래픽 노드 객체를 생성하여 반환하는 연산

 

- 적용 예시

BuildTree(n): 최상위 노드 n으로 부터 재귀적으로 순회하며 그래픽 요소를 나타내는 연산

--- BuildTree(n) 연산

GetChildren(n)

for each child{

    AddGraphicNode(CreateGraphicNode(child))

    BuildTree(child)

}

 

2.1 추상 연산

TreeDisplay에 추상 연산 형태로 인터페이스를 제공하고 Adapter가 최소 연산을 구현하는 방식

 

추상 연산

디렉토리 트리를 순환하며 표시하는 DirectoryTreeDisplay에서 GetChildren(n), CreateGraphicNode(child)을 구현하여 FileSystemEntity를 접근하여 자식노드를 반환할 수 있게 한다.

 

2.2 위임 객체를 사용

TreeDisplay가 두 연산이 포함된 인터페이스가 포함된 대리 객체, TreeAccessorDelegate에 위임하는 방식

 

 

위임 객체

DirectoryBrowser는 TreeAccessorDelegate를 상속받아 Adapter로서 두 연산을 구현한다.


예제 코드

TextShape와 TextView 예시에서 실제 Adapter TextShape를 구현하는 예시 코드

Shape.h

#include "Point.h"
#include "Manipulator.h"
class Shape
{
public:
	Shape();
	// 왼쪽 아래와 오른쪽 위 좌표를 이용하여 사각형 경계를 구한다.
	virtual void BoundingBox(Point& bottomleft, Point& topRight) const;
	// Shape를 조작하는 Manipulator를 반환하는 팩토리 메서드
	virtual Manipulator* CreateManipulator() const;
};

TextView.h

#include "Point.h"
class TextView
{
public:
	TextView();
	// 텍스트 상자의 중심점
	void GetOrigin(Coord& x, Coord& y) const;
	// 텍스트 상자의 가로 세로 길이를 이용하여 사각형 경계를 구한다.
	void GetExtent(Coord& width, Coord& height) const;
	virtual bool IsEmpty() const;
};

1. 클래스 적응자로 구현

C++에서 public 접근자를 통해 인터페이스 상속을 하여 override를 하여 새롭게 구현이 가능하고 private 접근자를 통해

구현을 상속받아 새로 정의하지 않고 부모의 메서드를 자식 메서드에서 그대로 사용할 수 있다.

 

TextShape.h

#include "Shape.h"
#include "TextView.h"
class TextShape : public Shape, private TextView
{
public:
	TextShape();

	virtual void BoundingBox(Point& bottomLeft, Point& topRight) const;
	virtual bool IsEmpty() const;
	virtual Manipulator* CreateManipulator() const;
 };

실제 구현을 보면 BoundingBox()와 IsEmpty() 메서드는 내부에서 TextView 구현을 상속받아 부모 메서드를 그대로 사용하고 있다.

추가적으로 Adapter는 Shape에 적응하기 위해 CreateManipulator를 구현해야한다.

// TextView의 GetOrigin, GetExtent 메서드를 이용하여 BoundingBox 메서드를 구현
void TextShape::BoundingBox(Point& bottomLeft, Point& topRight) const
{
    Coord bottom, left, width, height;

    GetOrigin(bottom, left);
    GetExtent(width, height);

    bottomLeft = Point(bottom, left);
    topRight = Point(bottom + height, left + width);
}
// TextView 구현을 그대로 이용
bool TextShape::IsEmpty() const
{
    return TextView::IsEmpty();
}
// Shape로 상속받은 TextView에는 없는 인터페이스
Manipulator* TextShape::CreateManipulator() const
{
    return new TextManipulator(this);
}

2. 객체 적응자로 구현

객체 적응자에서 TextView를 상속받지 않고 내부에서 인스턴스를 사용하기 위해 포인터를 관리한다.

#include "Shape.h"
#include "TextView.h"
class TextShape : public Shape
{
public:
	TextShape(TextView* text);

	virtual void BoundingBox(Point& bottomLeft, Point& topRight) const;
	virtual bool IsEmpty() const;
	virtual Manipulator* CreateManipulator() const;

private:
	// TextView 인스턴스를 참조하기위한 포인터
	TextView* _text;
 };

생성자를 통해 TextView 인스턴스를 할당하고 내부 메서드에서 참조를 통해 TextView 메서드를 사용한다.

// 내부 메서드에서 TextView 인스턴스를 이용하여 연산을 호출
TextShape::TextShape(TextView* text)
{
	_text = text;
}

void TextShape::BoundingBox(Point& bottomLeft, Point& topRight) const
{
	Coord bottom, left, width, height;

	_text->GetOrigin(bottom, left);
	_text->GetExtent(width, height);

	bottomLeft = Point(bottom, left);
	topRight = Point(bottom + height, left + width);
}

bool TextShape::IsEmpty() const
{
	return _text->IsEmpty();
}
// 클래스 적응자와 동일하게 구현한다.
Manipulator* TextShape::CreateManipulator() const
{
	return new TextManipulator(this);
}

참고 자료: GoF 디자인 패턴