ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 디자인패턴 (3) - 데코레이터패턴
    언어/디자인패턴 2023. 9. 8. 16:42

     

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

     

    - 데코레이터 패턴은 대상 객체에 대한 기능 확장이나 변경이 필요할 때

    - 상속이나 구현이 아닌, 객체의 결합을 통해 서브 클래싱을 대신 쓸 수 있는 구조 패턴이다.

    - 데코레이터 패턴을 이용하면 필요한 추가 기능의 조합을 런타임에서 동적으로 생성할 수 있다.

     

    출처: https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0Decorator-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

    - 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이다. 

Designed by Tistory.