본문으로 건너뛰기

디자인 패턴 - 데코레이터 패턴 (Decorator Pattern)

무료2015-03-06#Design_Pattern#装饰者模式#Decorator Pattern

데코레이터 패턴은 프레임워크에서 널리 적용되는 디자인 패턴입니다. Java API 의 파일 스트림 작업에서도 이러한 패턴이 적용되었습니다. 예를 들어 InputStream in = new BufferedInputStream(new FileInputStream(file)); 에서 BufferedInputStream 이 FileInputStream 을 데코레이트하여 기능 확장을 실현합니다. 데코레이터 패턴은 "수정에 대해서는 닫히고 확장에 대해서는 열린다"는 OO 설계 원칙을 만족하는 훌륭한 디자인 패턴입니다.

no_mkd

일. 데코레이터 패턴이란?

데코레이터 패턴은 "수정에 대해서는 닫히고 확장에 대해서는 열린다"는 원칙을 완벽하게 실현할 수 있습니다. 즉, 피데코레이터를 수정하지 않고도 피데코레이터의 기능을 확장할 수 있습니다. 파일 조작 코드를 다시 살펴보겠습니다:

InputStream in = new BufferedInputStream(new FileInputStream(file));

가장 안쪽에 "래핑"된 InputStream 객체는 new FileInputStream(file) 로, 기본적인 파일 입력 스트림입니다. BufferedInputStream 객체로 그 기능을 확장하며, 심지어 이렇게 할 수도 있습니다:

P.S. 위에서 사용한 동사——"래핑"에 주목하세요. 이것�� 데코레이터 패턴의 핵심입니다

InputStream in = new LineNumberInputStream(new BufferedInputStream(new FileInputStream(file)));

새로운 데코레이터 LineNumberInputStream 을 추가하여 행 번호를 가져오는 기능을 확장합니다. 물론 새로운 데코레이터를 계속 추가하는 한 더 많은 기능을 확장할 수 있습니다. 다시 생각해보면, FileInputStream 에 여러 층의 데코레이터를 추가하여 하나씩 기능을 획득했습니다. 이 과정에서 기능의 동적 확장을 실현했지만, 피데코레이터인 FileInputStream 의 아무것도 수정하지 않았습니다. 이것이 소위 "수정에 대해서는 닫히고 확장에 대해서는 열린다"는 원칙입니다.

이. 예시

Milk 를 판매하는 가게를 연다고 가정해 봅시다. 선택 가능한 토핑에는 모카 Mocha(초콜릿 맛), 커피 Coffee, 얼음물 IceWater 가 있습니다. 물론 판매가 잘된다면 새로운 음료 (Orange, Yoghurt 등) 와 새로운 토핑 (Salt..농담입니다) 을 도입할 계획입니다. Milk 자체에는 가격이 있으며 휴일에는 할인을 합니다. 다양한 토핑의 가격도 다릅니다. 물론 IceWater 를 두 배 추가한 Milk 를 주문할 수도 있습니다..가장 쉽게 생각할 수 있는 해결책은:

Milk 클래스를 정의하고 hasMocha, hasCoffee, hasIceWater(추가된 토핑을 나타냄) 와 같은 많은 속성을 포함합니다. 또한 discount 속성 (할인 정보용), cost 속성 (가격용) 도 필요합니다.これで完了でしょうか?아닙니다. 그 외에도 MochaNum, CoffeeNum, IceWaterNum(토핑의 수량을 나타냄. 진한 맛을 좋아하는 고객은 두 배 또는 그 이상의 토핑이 필요합니다..) 도 필요합니다

문제는 해결되었지만, 이렇게 하는 것이 정말 좋을까요?

다음 상황을 고려해 보세요: 1. 새로운 음료 Orange 를 도입 (Orange 를 정의해야 합니다. 재사용 가능한 부분이 거의 없고 제로부터 시작합니다.. 또는 Beverage 베이스 클래스를 정의하여 음료의 공통 부분을 넣을 수 있습니다); 2. 새로운 토핑 Salt 를 도입 (Milk 클래스를 수정하여 hasSalt, SaltNum 속성을 추가하여 소금 추가 Milk 요구를 충족해야 합니다..)

지금 보면 우리의 해결책은 매우 나쁘며 어떤 변화에도 적응할 수 없습니다. 기능을 확장하려면 기존에 캡슐화된 코드를 수정해야 할 가능성이 있으며, 성능상의 문제도 있습니다: 음료 가격 계산 부분에大量의 if...else... 구조가 필요하여 코드가 비대해지고 재사용이 어렵습니다 (다른 음료는 토핑이 다를 수 있고 가격 계산 방법도 다릅니다). 그렇다면 데코레이터 패턴을 시도할 때입니다

먼저, 피데코레이터와 데코레이터는 반드시 같은 슈퍼클래스를 가져야 하므로(왜인지는 잠시 설명하지 않겠습니다), 다음과 같은 Beverage 베이스 클래스를 정의합니다:

package DecoratorPattern;

/**

  • @author ayqy
  • Beverage 슈퍼클래스 정의. 모든 구체적인 Beverage 와 Ingredient 는 이 클래스를 확장해야 합니다

*/ public abstract class Beverage { String desc = "Unknown Beverage";//음료 관련 설명 정보 정의 float cost;//음료의 가격 정의

public abstract float getCost();//이 음료의 가격을 반환하는 cost 메서드 정의. 서브클래스는 이 메서드를 구현해야 합니다

public String getDesc(){
	return desc;
}

}

Beverage 가 있으면 피데코레이터——Milk 를 정의할 수 있습니다:

package DecoratorPattern;

/**

  • @author ayqy
  • 구체적인 Beverage: Milk 클래스 정의

*/ public class Milk extends Beverage{

float discount = 1;//할인 정의. 휴일에는 Milk 가 할인될 수 있습니다 (기본적으로 할인 없음)

public float getDiscount() {
	return discount;
}

public void setDiscount(float discount) {
	this.discount = discount;
}

public Milk(){
	cost = 4.5f;//Milk 의 가격 초기화
	desc = "Milk";//설명 정보 초기화
}

 @Override
public float getCost(){
	return discount * cost;//할인 후 가격 반환
}

}

다음은 데코레이터입니다. 데코레이터는 Beverage 와 다른 특성을 가지므로 추상화합니다:

package DecoratorPattern;

/**

  • @author ayqy
  • Ingredient 토핑 클래스 정의. Beverage 상속 (데코레이터 패턴에서 데코레이터와 피데코레이터는 같은 슈퍼클래스를 가져야 합니다)

*/ public abstract class Ingredient extends Beverage{

Beverage beverage;//이 토핑을 추가할 음료

 @Override
public String getDesc() {
	return "(" + desc + ")" + beverage.getDesc();//토핑의 설명에는 괄호를付けて 토핑과 음료를 구분합니다
}

 @Override
public float getCost() {
	return cost + beverage.getCost();//토핑에는 할인이 없습니다. 해당 가격 + 음료 가격을 직접 반환합니다
}

//여기서 Ingredient 가 Beverage 와 다른 다른 속성과 동작 추가

}

위의 getDesc 와 getCost 메서드에 주목하세요. 가격 계산과 설명 정보 생성의 책임을 완전히 메서드 호출 메커니즘에 위임했기 때문에 코드가 이렇게 간결합니다..

다음으로 구체적인 토핑——IceWater, Coffee, Mocha 를 정의합니다:

package DecoratorPattern;

/**

  • @author ayqy
  • 토핑 IceWater 얼음물 정의

*/ public class IceWater extends Ingredient{

public IceWater(Beverage bev)
{
	cost = 0.5f;
	desc = "IceWater";
	beverage = bev;
}

}

package DecoratorPattern;

/**
 * @author ayqy
 * 토핑 Coffee 커피 정의
 *
 */
public class Coffee extends Ingredient{
	
	public Coffee(Beverage bev)
	{
		cost = 3;
		desc = "Coffee";
		beverage = bev;
	}
}
package DecoratorPattern;

/**
 * @author ayqy
 * 토핑 Mocha 모카 정의
 *
 */
public class Mocha extends Ingredient{
	
	public Mocha(Beverage bev)
	{
		cost = 2;
		desc = "Mocha";
		beverage = bev;
	}
}

모든 준비가 완료되었습니다. Milk小店를 개업할 수 있습니다..

삼. 효과 예시

먼저 테스트 클래스를 정의합니다:

package DecoratorPattern;

public class Test { public static void main(String[] args){ Beverage bev;

	System.out.println("모카와 커피를 추가한 Milk 를 만듭니다..");
	bev = new Milk();//먼저 Milk 를 만듭니다
	bev = new Mocha(bev);//Mocha 추가
	bev = new Coffee(bev);//Coffee 추가
	System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
	
	System.out.println("얼음물 두 배와 모카 두 배를 추가한 커피 Milk 를 만듭니다..");
	bev = new Milk();//Milk 를 다시 만듭니다
	bev = new IceWater(bev);//얼음물 추가
	bev = new IceWater(bev);//얼음물 추가
	bev = new Mocha(bev);//Mocha 추가
	bev = new Mocha(bev);//Mocha 추가
	bev = new Coffee(bev);//Coffee 추가
	System.out.println(bev.getDesc() + " " + bev.getCost() + "¥");
	//물론 이렇게 쓸 수도 있습니다: Beverage bev = new Coffee(new Mocha(new Milk()));
	//우리가 익숙한 메서드 체인과 비교: InputStream in = new BufferedInputStream(new FileInputStream(file));
}

}

결과 예시:

효과가 좋죠. 이전의 확장 문제를 다시 고려해 보세요:

1. 새로운 음료 Orange 를 도입 (Beverage 베이스 클래스를 상속하는 Orange 클래스를 정의해야 합니다. Beverage 베이스 클래스에서 기존 부분을 재사용할 수 있습니다. 그래도 만족하지 못한다면 물론 "구체적인 음료 클래스 ConcreteBeverage"를 추상화하여 Milk 등 다른 음료를在此基础上에서 확장할 수 있습니다); 2. 새로운 토핑 Salt 를 도입 (Milk 클래스를 수정할 필요가 없습니다. Salt 토핑을 구현하여 Ingredient 클래스를 상속하기만 하면 됩니다)

데코레이터 패턴의 장점을 발견했나요? 그렇다면 찬물을 끼얹을 때입니다..

사. 데코레이터 패턴의 장단점

단점은 명확합니다——이렇게 긴 코드를 본 적이 있나요?

XObject o = new XDecorator(new XXDecorator(new XXXDecorator(new XXXXDecorator())));

네, 이것은 피데코레이트 객체의 기능을 세 번 확장했을 뿐입니다. 물론 더 많이 할 수 있습니다..즉, 더 길어질 수 있다는 뜻입니다. 또한 사용할 때 많은 작은 객체를 생성합니다. 예를 들어:

bev = new Milk();//Milk 를 다시 만듭니다
bev = new IceWater(bev);//얼음물 추가
bev = new IceWater(bev);//얼음물 추가
bev = new Mocha(bev);//Mocha 추가
bev = new Mocha(bev);//Mocha 추가
bev = new Coffee(bev);//Coffee 추가

데코레이터 패턴에 익숙하지 않은 사람이 위의 코드를 읽는다면 빠르게 이해할 수 있을까요?

주의하세요. 위의 코드는 시작에서 언급한 동사——"래핑"을 설명합니다. 맞죠?

장점:

위에서 언급한 동적 확장 장점 외에도, 더 중요한 장점이 있습니다. 바로 앞에서 언급한 getDesc 와 getCost 메서드입니다

맞습니다. 이 호출 메커니즘을 이용하여 작업을 완료할 수 있습니다 (데코레이트 동작 전 또는 후에 커스텀 작업을 추가하기만 하면 됩니다. 예제에서는 실제로 데코레이트 동작 후에 작업을 추가합니다). 재귀와 유사한 효과를 쉽게 달성할 수 있습니다

이것이 "왜 데코레이터와 피데코레이터는 같은 슈퍼클래스를 가져야 하는가?"를 설명합니다.조금 더 설명이 필요합니다:

"상속보다 컴포지션을 사용하라"는 설계 원칙이 있습니다. 여기서 이 원칙을 위반하는 것처럼 보입니다

실제로는 원칙을 위반하지 않았습니다. 데코레이터 패턴에서의 상속은 타입 매칭을 얻기 위한 것이며, 상속을 이용하여 클래스의 동작을 확장하기 위한 것이 아닙니다. "상속보다 컴포지션을 사용하라" 원칙의 전제 조건 "(클래스의 동작을 확장해야 할 때)"이 생략된 것입니다

댓글

아직 댓글이 없습니다

댓글 작성