헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
Chapter 3. 객체 꾸미기 - 데코레이터 패턴
OCP 살펴보기
- OCP(Open-Closed Principle)
디자인 원칙 5
- 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
- 우리의 목표는 기존 코드를 건드리지 않고 확장으로 새로운 기능을 추가하는 것이다.
- 이러한 경우 유연하고 튼튼한 디자인을 만들 수 있다.
커피 전문점 프로젝트
- 커피 주문 시스템에는 기본 커피 메뉴 가격에 고객들이 추가로 첨가하는 첨가물들의 가격도 각각 더해야 한다는 점을 고려해야 한다.
데코레이터 패턴 살펴보기
- 특정 음료에서 첨가물로 그 음료를 장식(decorate)한다고 생각해보면…
- 예시로 모카와 휘핑 크림을 추가한 다크 로스트 커피를 주문한다고 가정한다.
DarkRoast
객체를 가져온다.Mocha
객체로 장식한다.Whip
객체로 장식한다.cost()
메서드를 호출한다. 단, 이때 첨가물의 가격을 개산하는 일은 해당 객체에게 위임한다.
- 위 예시에서 객체를 어떻게 장식할지, 계산하는 일을 어떻게 위임할지가 바로 데코레이터 패턴의 방식이다.
- 예시로 모카와 휘핑 크림을 추가한 다크 로스트 커피를 주문한다고 가정한다.
주문 시스템에 데코레이터 패턴 적용하기
DarkRoast
객체를 가져온다.Mocha
객체를 만들고 그 객체로DarkRoast
를 감싼다.Whip
객체를 만들어Mocha
를 감싼다.- 가격을 계산한다. 가격을 계산할 때는 가장 바깥쪽에 있는
Whip
객체의cost()
를 호출한다. Whip
은 그 객체가 장식하고 있는 객체에게 가격 계산을 위임한다. 가격이 구해지면, 계산된 가격에 휘핑크림 가격을 더해 그 결과값을 반환한다.- 가장 바깥쪽에 있는 데코레이터인
Whip
의cost()
를 호출 Whip
은Mocha
의cost()
를 호출Mocha
는DarkRoast
의cost()
를 호출DarkRoast
는 다크 로스트 커피 가격을 반환Mocha
는DarkRoast
로부터 리턴받은 가격에 모카 가격을 더해서 반환Whip
은Mocha
로부터 받은 가격에 휘핑크림 가격을 더해서 최종 가격 반환
- 가장 바깥쪽에 있는 데코레이터인
데코레이터 패턴의 정의
- 데코레이터 패턴으로 객체에 추가 요소를 동적으로 더할 수 있다.
- 데코레이터 패턴을 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.
데코레이터 패턴 클래스 다이어그램
- 각 구성 요소는 직접 쓰일 수도 있고 데코레이터에 감싸여 쓰일 수도 있다.
| Component | | --------------- | | methodA() | | methodB() | | // 기타 메서드 |
- ConcreteComponent에 새로운 행동을 동적으로 추가한다.
| ConcreteComponent | | ----------------------- | | methodA() | | methodB() | | // 기타 메서드 |
- Decorator에는 장식할 구성 요소와 같은 인터페이스 또는 추상 클래스를 구현한다.
| Decorator | | ----------------------- | | Component wrappedObj | | ----------------------- | | methodA() | | methodB() | | // 기타 메서드 |
- ConcreteDecorator에는 데코레이터가 감싸고 있는 Component 객체용 인스턴스 변수가 존재한다.
| ConcreteDecoratorA | | ------------------- | | methodA() | | methodB() | | newBehavior() | | // 기타 메서드 |
- Decorator는 Component의 상태를 확장할 수 있다.
| ConcreteDecoratorB | | ------------------- | | Object newState | | ------------------- | | methodA() | | methodB() | | // 기타 메서드 |
커피 전문점 프로젝트에 적용하기
- Component 추상 클래스
| Beverage | | ---------------- | | description | | ---------------- | | getDescription() | | cost() |
- 커피 종류마다 구성 요소를 나타내는 구상 클래스
| DarkRoast | | --------- | | cost() |
| Espresso | | -------- | | cost() |
| Decaf | | ------ | | cost() |
- Decorator 클래스
| CondimentDecorator | | ------------------ | | getDescription() |
- 각각의 첨가물을 나타내는 데코레이터
cost()
와getDescription()
을 구현해야 한다.| Milk | | ----------------- | | Beverage beverage | | ----------------- | | cost() | | getDescription() |
| Mocha | | ----------------- | | Beverage beverage | | ----------------- | | cost() | | getDescription() |
| Whip | | ----------------- | | Beverage beverage | | ----------------- | | cost() | | getDescription() |
커피 주문 시스템 코드 작성
- Beverage 클래스
public abstract class Beverage { String description = "제목 없음"; public String getDescription() { return description; } // getDescription()은 이미 구현되어 있지만 cost()는 서브클래스에서 구현해야 한다. public abstract double cost(); }
- Decorator 클래스
// Beverage 객체가 들어갈 자리에 들어갈 수 있어야 하므로 Beverage를 상속받는다. public abstract class CondimentDecorator extends Beverage { // 각 데코레이터가 감쌀 음료를 나타내는 Beverage 객체 Beverage beverage; // 모든 첨가물 데코레이터에서 getDescription()을 새로 구현하도록 만들 계획이다. public abstract String getDescription(); }
음료 코드 구현
- 다크 로스트 커피 구현하기
public class DarkRoast extends Beverage { public DarkRoast() { description = "다크 로스트 커피"; } public double cost() { return 0.99; } }
- 에스프레소 음료 구현하기
public class Espresso extends Beverage { public Espresso() { description = "에스프레소"; } public double cost() { return 1.99; } }
첨가물 코드 구현
- 모카 첨가물 구현하기
public class Mocha extends CondimentDecorator { public Mocha(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + ", 모카"; } public double cost() { return beverage.cost() + 0.20; } }
- 휘핑크림 첨가물 구현하기
public class Whip extends CondimentDecorator { public Whip(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + ", 휘핑크림"; } public double cost() { return beverage.cost() + 0.10; } }
커피 주문 시스템 코드 테스트
- 주문용 테스트 코드
public class CoffeeOrder { public static void main(String args[]) { Beverage beverage = new Espresso(); System.out.println(beverage.getDescription() + " $" + beverage.cost()); Beverage beverage2 = new DarkRoast(); beverage2 = new Mocha(beverage2); beverage2 = new Whip(beverage2); System.out.println(beverage2.getDescription() + " $" + beverage2.cost()); } }
데코레이터가 적용된 예: 자바 I/O
FileInputStream
- 데코레이터로 장식될 예정인 클래스
BufferedInputStream
- 구상 데코레이터로,
FileInputStream
에 입력을 미리 읽어서 더 빠르게 처리할 수 있게 해주는 버퍼링 기능을 더해주는 역할
- 구상 데코레이터로,
ZipInputStream
- 또한 구상 데코레이터로, zip 파일에서 데이터를 읽어올 때 안에 들어있는 항목을 읽는 기능을 더해주는 역할
데코레이터 패턴의 문제점
- 데코레이터 패턴을 사용해 디자인하다 보면 잡다한 클래스가 많아진다.
- 데코레이터 패턴을 도입하면 구성 요소를 초기화하는 데 필요한 코드가 훨씬 복잡해진다.
- 빌더 패턴이나 팩토리 패턴으로 개선 가능