어댑터 패턴 (adapter pattern)
한 클래스의 인터페이스를 클라이언트에서 사용하고자하는 다른 인터페이스로 변환한다.
어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.
호환되지 않는 인터페이스를 사용하는 클라이언트를 그대로 활용할수 있다.
이렇게 함으로써 클라이언트와 구현된 인터페이스를 분리시킬수 있으며, 향후 인터페이스가 바뀌더라도 그 변경 내역
은 어댑터에 캡슐화 되기 때문에 클라이언트는 바뀔 필요가 없어진다.
어댑터 패턴 클래스 다이어그램
전기 콘센트를 보면 이해가 쉽다.
한국의 표준 플러그를 일본에 전원 소켓에 바로끼워줄수 없어 동그랑 모양을 일자로 바꿔주는 어댑터를 끼워주어야 한다.
이와같이 어댑터는 소켓의 인터페이스를 플러그에서 필요로 하는 인터페이스로 바꿔준다고 할 수 있다.
객체지향 어댑터는 ???
일상 생활에서와 동일하게 어떤 인터페이스를 클라이언트에서 요구하는 형태의 인터페이스에 적응시켜주는 역할을 한다.
public interface Duck {
public void quack();
public void fly();
}
public class MallardDuck implements Duck {
@Override
public void quack() {
System.out.println("Quack");
}
@Override
public void fly() {
System.out.println("I'm flying");
}
}
public interface Turkey {
public void gobble();
public void fly();
}
public class WildTurkey implements Turkey{
@Override
public void gobble() {
System.out.println("Gobble gobble");
}
@Override
public void fly() {
System.out.println("I'm flying a short distance");
}
}
Duck 객체가 모자라서 Turkey 객체를 대신 사용해양 하는 상황이라고 해보자.
인터페이스가 다르기 때문에 Turkey객체를 바로 사용할 수는 없다.
어댑터를 만들어 보자.
public class TurkeyAdapter implements Duck {
Turkey turkey;
public TurkeyAdapter(Turkey turkey) {
this.turkey = turkey;
}
@Override
public void quack(){
turkey.gobble();
}
@Override
public void fly() {
turkey.fly();
}
}
public class DuckTestDrive {
public static void main(String[] args) {
MallardDuck duck = new MallardDuck();
WildTurkey turkey = new WildTurkey();
Duck turkeyAdapter = new TurkeyAdapter(turkey);
System.out.println("The turkey says...");
turkey.gobble();
turkey.fly();
System.out.println("The Duck says...");
testDuck(duck);
System.out.println("The TurkeyAdapter says...");
testDuck(turkeyAdapter);
}
public static void testDuck(Duck duck){
duck.quack();
duck.fly();
}
}
클라이언트 -> request() -> 어댑터 - translatedRequest() -> 어댑티.
클라이언트는 타겟 인터페이스에 맞게 구현, 어댑터는 타겟 인터페이스를 구현하며, 어댑티 인스턴스가 들어있음.
클라이언트에서 어댑터를 사용하는 방법.
1. 클라이언트에서 타겟 인터페이스를 사용하여 메소드를 호출함으로써 어댑터에 요청을 한다.
2. 어댑터에서는 어댑티 인터페이스를 사용하여 그 요청을 어댑티 에 대한 하나 이상의 메소드를 호출로 변환한다.
3. 클라이언트에서는 호출 결과를 받긴 하지만 중간에 어댑터가 껴 있는지는 전혀 알지 못한다.
어댑터에는 두종류가 있다.
하나는. 객체 어댑터
다른 하나는. 클래스 어댑터
클래스 어댑터 패턴을 쓰려면 다중 상속이 필요한데, 자바에서는 다중 상속이 불가능하다.
두 어댑터의 클래스 다이어 그램을 보면 이해가 수월하다.
클래스 어댑터
객체 어댑터
두 클래스 다이어 그램에서 Target은 오리, Adaptee는 칠면조라 볼 수 있다.
클래스 어댑터에서는 어댑터를 만들 때 타겟과 어댑티 모두의 서브 클래스로 만들고,
객체 어댑터 에서는 구성을 통해서 어댑티에 요청을 전달한다는 점을 제외하면 별다른 차이점이 없다.
구식 Enumeration.
Enumeration을 리턴하는 elements() 메소드가 구현되어 있었던, 초기 컬렉션 형식(Vector, Stack, Hashtable 등)
Enumeration 인터페이스를 이용하면 컬렉션 내에서 각 항목이 관리되는 방식에는 신경 쓸 필요 없이 컬렉션의 모든 항목에 접근이 가능하다.
신형 Iterator.
썬에서 새로운 컬렉션 클래스를 출시하면서, Enumeration과 마찬가지로 컬렉션에 있는 일련의 항목들에 접근할 수 있게 해 주면서 항목을 제거할 수 도 있게 해 주는 iterator라는 인터페이스를 이용하기 시작했다.
개발을 하다보면, Enumerator 인터페이스를 사용하는 구형 코드를 사용해야 하는 경우가 종종 있지만, 새로 만드를 코드에서는 Iterator만 사용할 계획이다.
이런 경우에 어댑터 패턴을 적용하면 좋다.
public class EnumerationIterator implements Iterator{
Enumeration enumeration;
public EnumerationIterator(Enumeration enumeration) {
this.enumeration= enumeration;
}
@Override
public boolean hasNext(){
return enumeration.hasMoreElements();
}
@Override
public Object next() {
return enumeration.nextElement();
}
@Override
public void remove() {
throw new UnsupportedOperationException(); //예외 던짐 UnsupportedOperationException 지원
}
}
Enumeration에서는 remove()에 해당하는 기능을 제공하지 않는다. (읽기 전용 인터페이스)
어댑터 차원에서 완벽하게 작동하는 remove() 메소드를 구현할 수 있는 방법은 없다.
그나마 가장 좋은 방법으로 런타임 예외를 던지는 방법, Iterator 인터페이스를 디자인한 사람들은 이런 필요성을 미리 예견하고
remove() 메소드를 구현할 때 UnsupportedOperationException을 지원하도록 만들었다.
이런경우는 어댑터가 완벽하게 정용될 수 없는 경우라고 할수있다.
하지만 클라이 언트에서 충분히 주의를 기울이고 어댑터 문서를 잘 만들어 두면 상당히 쓸만한 해결책이 될 수 있다.
참고.
Head First Design Pattern.
'JAVA > DesignPattern' 카테고리의 다른 글
디자인패턴 - 템플릿 메소드 패턴 (template method pattern) (7) | 2016.06.16 |
---|---|
디자인패턴 - 퍼사드 패턴 (facade pattern) (3) | 2016.06.13 |
디자인패턴 - 커맨드 패턴 (command pattern) (2) | 2016.05.24 |
디자인패턴 - 싱글턴 패턴 (singleton pattern) (0) | 2016.05.16 |
디자인패턴 - 팩토리 패턴 (factory pattern) (17) | 2016.05.12 |
댓글