ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (3) Decorator Pattern
    프로그래밍/Design Pattern 2023. 3. 13. 00:42

    여러 종류의 음료 가격을 계산하는 부분을 구현하려고 합니다.

    음료별 종류뿐만 아니라 커피처럼 토핑 옵션이 있을 때, 음료 메뉴들의 부모클래스를 선언하고 각 메뉴별로 상속하여 옵션별로 가격을 반환하는 구조로 만들 수 있습니다.

     

    class Beverage {
    	func cost() -> Int { return 2500 }
    }
    
    class Espresso: Beverage {
    	override func cost() -> Int {
        	return super.cost()
        }
    }
    
    class TwoShotEspresso: Beverage {
    	override func cost() -> Int {
        	return super.cost() + 1000
        }
    }
    
    class ThreeShotEspresso: Beverage {
    	override func cost() -> Int {
        	return super.cost() + 1000 * 2
        }
    }

     

    위 코드는 극단적으로 모든 옵션별로 클래스를 나눈 경우입니다. 클래스를 어떤 카테고리로 나누는가에 따라서 위처럼만큼은 나누지 않을 수 있겠지만, 결국 음료별로 클래스를 나누게 될 경우 옵션 가격을 각 서브클래스에서 직접 계산하여 구현하는 방향으로 구현하게 됩니다. 계산해서 구현하는 방법이 꼭 나쁜 것은 아니지만 경우를 애매하게 나누기 어려운 경우가 발생하게 되었을 때 일관성 없게 코드를 작성하게 될 수도 있습니다. 위 경우 커피 음료에 대해서 기본 가격인 2500원을 적용하려고 기본 Cost를 적용했지만, 일반 물 메뉴인 경우 위 기본 가격을 같이 적용하기는 어렵습니다.

     

    두번째 방법으로는 슈퍼 클래스에서 가능한 옵션들에 대해서 미리 만들어두고, 서브 클래스에서 이를 조합하여 사용하는 방법이 있습니다.

     

    class Beverage {
        func cost() -> Int
        
        func setCoffee()
        func setMilk()
        func setWhip()
        func setMocha()
        ...
    }

    이 경우 각 개별 음료 클래스에서 슈퍼 클래스의 함수를 조합하여 가격을 구성할 수 있습니다.

    이 경우의 문제는 무엇일까요?

     

    신메뉴가 추가 될 때 신메뉴 서브클래스가 추가됨과 동시에 슈퍼클래스인 Beverage도 같이 수정이 일어난다는 것 입니다.

    만약 위 코드에서 녹차메뉴가 추가되었다면 setTea() 와 같은 함수가 추가되어야 서브클래스에서 사용 할 수 있게 됩니다.

     

     

    Decorator 패턴은 객체에 추가 요소를 동적으로 추가(장식)할 수 있게 하는 패턴입니다.

    위에서 설명한 예시에서 2개의 샷이 들어간 에스프레소의 경우,

    ((((음료) + 커피 )+ 샷) + 샷) 으로 표현하여 해결한다는 구조를 갖습니다.

    코드로 설명하면 아래와 같습니다.

     

    class Beverage {
    	func cost() -> Int {
        	return 500
        }
    }
    
    class BeverageDecorator: Beverage {
    
    }
    
    class Shot: BeverageDecorator {
    	var beverage: Beverage
        
        init(beverage: Beverage) {
        	self.beverage = beverage
        }
        
    	override func cost() -> Int {
        	return 1000 + self.beverage.cost()
        }
    }
    
    class Whip: BeverageDecorator {
    	var beverage: Beverage
        
        init(beverage: Beverage) {
        	self.beverage = beverage
        }
        
    	override func cost() -> Int {
        	return 500 + self.beverage.cost()
        }
    }
    
    class Tea: BeverageDecorator {
    	var beverage: Beverage
        
        init(beverage: Beverage) {
        	self.beverage = beverage
        }
        
    	override func cost() -> Int {
        	return 1000 + self.beverage.cost()
        } 
    }
    
    var beverage = Beverage()
    var whipBeverage = Whip(beverage)
    var shotAndWhipBeverage = Shot(whipBeverage) // Shot(Whip(beverage))

     

    위 코드에서는 음료를 꾸미는 Decorator 클래스가 있습니다. 이 클래스는 Beverage 클래스(Component)에 대한 참조를 가지고 있고, 이 인스턴스의 인터페이스 cost를 호출하는 구조를 가지고 있습니다.

     

    만약 새로운 메뉴가 출시가 된다면 기존 Beverage 클래스를 수정할 필요없이 새로운 Decorator를 선언하여 사용하면 됩니다.

    확장에 대해서 열려있고 변경에 대해서 닫혀있는 OCP(Open-Closed Principle)원칙을 준수하고 있음을 확인할 수 있습니다.

     

    반응형

    '프로그래밍 > Design Pattern' 카테고리의 다른 글

    (2) Observer Pattern  (0) 2023.03.05
    (1) Strategy Pattern  (0) 2023.03.05

    댓글

Designed by Tistory.