-
디자인패턴 (3) - 데코레이터패턴언어/디자인패턴 2023. 9. 8. 16:42
1. 데코레이터 패턴이란?
- 데코레이터 패턴은 대상 객체에 대한 기능 확장이나 변경이 필요할 때
- 상속이나 구현이 아닌, 객체의 결합을 통해 서브 클래싱을 대신 쓸 수 있는 구조 패턴이다.
- 데코레이터 패턴을 이용하면 필요한 추가 기능의 조합을 런타임에서 동적으로 생성할 수 있다.
- Component : 원본 객체와 장식된 객체 모두 묶는 역할 ( 모두를 하나의 타입으로 묶음 [공통 책임 수행])
- ConcreteComponent: 원본 객체 (추후에 데코레이팅 될 객체임)
- Decorator: 추상화된 장식자 클래스 -> 원본 객체를 구성(의존)한 필드와 인터페이스의 구현 메서드 가짐
- ConcreteDecorator : 구체 장식자 클래스
* 변화할 수 있는 행동을 인터페이스화 -> 이를 구현한 원본과 원본을 구성하는 데코레이터 추상클래스로 분리
-> 데코레이터 추상클래스 상속하는 데코레이터 클래스로 분리
interface Component{ void operation(); // 공통 책임 } class ConcreteComponent implements Component{ public void operation(){ //원본의 행위 } } ///공통 책임과 원본 생성 abstract class Decorator implements Component{ //구체 장식 책임 중 origin의 operation 호출까지만 // 장식 클래스는 변화 가능, 구체 클래스에 느슨한 의존관계를 위해.. Component origin; Decorator(Component origin){ this.origin = origin; } public void operation(){ origin.operation; } public void extraop(); } class ComponentDecorator1 extends Decorator{ ComponentDecorator1(Component co){ super(co); //부모생성자 호출로 origin초기화 } public void operation() { super.operation(); extraop(); } public void extraop(){ //장식할 행위 } }
* 결국 Component 타입 변수에 담아서 사용한다는 점! -> operation()호출 가능해야함
* 인터페이스 구현 클래스 상속받으면 간접 구현에 해당!
1.2 패턴 주요 사용 시기
- 상황에 따라 동적으로 다양한 기능이 빈번하게 추가/삭제 되는 경우
(책임 자체의 변화보단, 추가 책임 느낌으로)
- 객체를 사용하는 코드 손상시키지 않고, 런타임에 객체에 추가 동작 할당
1.3 패턴 장점
- 서브 클래스 만들때보다 훨씬 더 유연하게 기능 확장 가능
- 객체를 여러 데코레이터로 래핑하여 여러 동작 결합 가능 (연속 래핑)
- 각 장식자는 고유의 책임 수행 SRP 준수
- 클라이언트 코드 수정없이 기능 확장이 필요하면, 장식자 추가 OCP준수
- 구현체가 아닌 인터페이스 바로봄으로써 DIP 준수
1.4 패턴 단점
- 장식자 일부만 제거하기 어려움
- 데코레이터 조합 코드가 보기에 구리다.
- 어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정됨
->순서에 의존하지 않는 방식으로 데코레이터 구현하기 어려움
*OCP -> 모든 부분에서 준수할 수는 없음 -> 바뀔 가능성이 높은 부분을 중점적으로 OCP적용
2. 커피 프렌차이즈 예제
public class Beverage{ public int cost(){ } } public calss Espresso extends Beverage{ public int cost(){ //가격 } } public calss Decaf extends Beverage{ public int cost(){ //가격 } } public calss DarkRoast extends Beverage{ public int cost(){ //가격 } }
- 커피마다 다른 가격을 계산하는 cost()메서드가 있다.
- 이를 계산하기 위해 새로운 종류의 커피가 등장할 때마다 새로운 클래스를 작성하고,
Beverage를 상속받았다.
- 이러한 설계의 문제점은 새로운 종류의 토핑을 얹은 음료가 등장할 때마다 클래스를 추가하고,
cost()를 다시 계산해야한다.
-> 이를 조금 계선해보자
public class Beverage{ private String desciption; private boolean mlik; private boolean soy; private boolean mocha; private boolean whip; public String getDescription(){ //.. } public int cost(){ //각 재료가 첨가되었는지를 검사해서 가격 계산 if has... } private boolean hasMilk(){ } public void setMilk(){ } private boolean haSoy(){ } public void setSoy(){ } }
-> 위와 같이 처리하면, 꼭 서브클래스 만들지 않더라고, Description을 파싱하거나 get을 따로 호출하거나
무튼 하나의 클래스로 첨가물이 포함되어 있는지를 계산해서 가격을 계산할 수도 있다.
-> 혹은 슈퍼클래스에서는 첨가물이 포함되어 있을 때 가격을 계속 더하도록 구성하고,
서브 클래스는 첨가물 가격 set과 has를 조작하여 최종가격을 얻어내는 식으로 구성할 수도 있다.
-> 이 방법은 첨가물이 늘어날 수록 기본 클래스에 너무 많은 필드와 인스턴스가 추가될 것이다.
-> 혹은 첨가물이 필요없는 서브클래스까지 첨가물관련 메서드를 포함하게 된다.
*상속은 코드 중복을 줄이는 좋은 방법이긴하지만, 최선의 방법은 아님
-> 구성(의존)위임을 통해 상속과 비슷한 효과를 낼 수도 있다 (+ 느슨한 의존관계)
2.1 데코레이터 패턴 적용하기
- 기본 음료에 토핑을 장식하는 패턴을 사용해보자
- DarkRoast객체 -> Mocha장식->Whip장식->cost()호출
*이때 첨가물 가격 계산하는 일은 해당 객체에 (토핑객체) 위임
* 데코레이터는 자신이 장식하는 객체에 행동 위임+ 추가 행동 수행
- 기본구성여소와 이를 상속한 원본 콘크리트 클래스
public abstract class Beverage{ protected String description; public Beverage(){ this.description = "기본음료"; } public void getDescription(){ return this.description; } public abstract int cost(); } public HouseBlend() extends Beverage{ public HouseBlend(){ this.description ="하우스 블랜드 커피"; } public int cost(){ //기본가격 } } public DarkRoast() extends Beverage{ public DarkRoast(){ this.description ="에스프레소"; } public int cost(){ //기본가격 } }
- 기존 beverage를 구성하고, 추가 기능이 있는 추상클래스 정의와 이를 구현한 콘크리트 클래스들
public abstract class CondimentDecorator extends Beverage{ Beverage beverage; public abstract String getDescription(); } public class Mocha extends CondimentDecorator{ public Mocha(Beverage br){ this.beverage = beverage; } public String getDescription(){ return beverage.getDescription() +",모카"; } public double cost(){ return beverage.cost()+20; } } public class Soy extends CondimentDecorator{ public Soy(Beverage br){ this.beverage = beverage; } public String getDescription(){ return beverage.getDescription() +",두유"; } public double cost(){ return beverage.cost()+10; } }
*데코레이터 추상클래스가 Beverage를 상속받은 이유는 행동을 상속x, 형식맞추기o
- 커피숍 테스트
public class SratCoffee { public static void main(String[] args) { Beverage bver = new Espresso(); System.out.println(bver.getDescription()+"$"+bver.cost()); Beverage br2 = new HouseBlend(); br2 = new Mocha(br2); br2 = new Mocha(br2); // 모카 다시 추가 System.out.println(br2.getDescription()+br2.cost()); br2 = new Soy(br2); System.out.println(br2.getDescription()+br2.cost()); } }
- 사이즈 추가
public enum Size { VENTI,GRANDE,TALL; public boolean isVENTI() { return this==Size.VENTI; } public boolean isGrande() { return this==Size.GRANDE; } public boolean isTALL() { return this==Size.TALL; } public abstract class Beverage { //.. protected Size s; public Size getSize() { return s; } } public abstract class CondimentDecorator extends Beverage{ protected int decoratorCostPerSize; protected Beverage br; public abstract String getDescription(); //getDescription()을 강제하기 위해 abstract로 추가 public Size getSize() { return br.getSize(); } } public class Mocha extends CondimentDecorator{ @Override public int cost() { if(br.getSize().isGrande()) { this.decoratorCostPerSize = 2; } if(br.getSize().isTALL()) { this.decoratorCostPerSize =1; } if(br.getSize().isVENTI()) { this.decoratorCostPerSize =3; } return br.cost()+this.decoratorCostPerSize; } }
* 데코레이터 추상클래스에 getSize()를 정의하지 않으면,
-> 기존 베버리지.getSize() -> 초기 설정 사이즈
-> 모카 추가 베버리지에 getSize() -> 아무곳에서도 getSize오버라이딩 한 적 x -> 최상위 Beverage의 getSize()호출
* 데코레이터 추상클래스에서 getSize()정의시
-> 모카 추가 베버리지에 getSize()호출시 -> 추가된 구성요소에서 getSize()[에스프레소,하우스블랜드]호출 -> s계산
3. Java에서 데코레이터 패턴 적용
- io클래스가 대표적인 데코레이터 패턴 적용한 API이다.
'언어 > 디자인패턴' 카테고리의 다른 글
디자인 패턴 (7) - 커맨드 패턴 (0) 2023.09.12 디자인 패턴 (6) - 싱글톤 패턴 (0) 2023.09.11 디자인 패턴 (5) - 추상 팩토리 패턴 (0) 2023.09.11 디자인 패턴 (4) - 팩토리 패턴 (0) 2023.09.11 디자인 패턴 (2) - 옵저버 패턴 (0) 2023.09.08