[Design Pattern] Decorator Pattern

윰진·2023년 3월 26일
0

면접

목록 보기
7/11

Reference

📢 공부한 내용을 바탕으로 작성하였습니다. 내용에 문제가 있다면 댓글 또는 메일 jnw__@naver.com 주시면 확인 후 빠른 수정하겠습니다. !

데코레이터 패턴(Decorator Pattern) :
객체에 추가 요소를 동적으로 더해, 서브 클래스를 작성하는 것 보다 더 유연하게 기능 확장이 가능
{\rightarrow} 데코레이터 형식이 그 데코레이터로 감싸는 객체의 형식과 같다는 점에 주목해보자!

주의할 점
데코레이터 패턴변경이 잦은 내용을 찾아 Open-Closed Principle 을 적용
{\rightarrow} OCP를 남용하면 복잡한 코드가 되고, 시간 낭비한 셈이 될 수 있음

객체지향 기본 원칙

  • 변경되는 부분은 캡슐화한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
  • 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀있어야 한다.

01. 개요

데코레이터 패턴은 구성 요소의 구조를 상속받아 형식을 같게 한 클래스와 구성 요소를 나타내는 구상 클래스로 구현할 수 있다.

아래 사진에서 음료를 나타내는 Beverage 추상 클래스에 대한 구상 클래스 HouseBlend, DarkRoast 등은 커피 종류를 나타낸다.

Beverage를 상속하여 같은 형식을 가지는 Decorator는 하위에 Milk, Mocha 같은 커피 첨가물을 나타낸다. 이들은 구성 요소로 Beverage를 가진다.

{\Rightarrow} 데코레이터 객체가 자신이 감싸고 있는 객체와 같은 인터페이스를 가지고 있기 때문에 확장이 가능하다.

02. 데코레이터 패턴

1 ) Beverage 추상 클래스

상황에 따라 추상 클래스나 인터페이스 일 수 있다.

public abstract class Beverage {
    String description = "제목없음";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

2 ) Condiment를 나타내는 추상 클래스 (데코레이터 클래스)

각 첨가물이 더해질 음료를 나타내는 객체는 슈퍼 클래스인 Beverage로 나타낸다.

public abstract class CondimentDecorator extends Beverage{
    // 각 데코레이터가 감쌀 음료를 나타내는 객체 지정
    // 데코레이터에서 어떤 음료든 감쌀 수 있도록 Beverage 슈퍼 클래스 사용
    Beverage beverage;
    // 모든 첨가물에서 getDescription() 메소드를 새로 구현하도록 할 것 이므로 추상 메소드로 선언
    public abstract String getDescription();
}

3 ) 음료 코드 구현하기

다른 음료들도 아래와 같이 구현할 수 있음

public class Espresso extends Beverage{

    public Espresso(){
        description = "에스프레소";
    }

    public double cost(){
        return 1.99;
    }
}

4 ) 첨가물 코드 구현하기

전달된 음료를 감싸서 데코

public class Mocha extends CondimentDecorator{

    public Mocha(Beverage beverage){
        this.beverage = beverage;
    }
    @Override
    public double cost() {
        return beverage.cost() + .20;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }
}

5 ) 테스트 코드

에스프레소, 모카 $2.19

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        beverage = new Mocha(beverage);
        System.out.println(beverage.getDescription() + " $" + beverage.cost());
    }
}

🐾 데코레이터 패턴을 사용하면 구상 구성 요소로 어떤 작업을 처리하는 것에는 적합하지 않음

  • 구상 구성 요소로 돌아가는 코드를 만든다면 데코레이터 패턴 사용

03. 연습하기

1 ) Decorator Pattern HW 1

// 아래 코드를 읽고 분석해보자.
interface Fighter {
  public void attack ();
}

class XWingFighter implements Fighter {
  @Override
  public void attack () {
    System.out.println("탄환 발사");
  }
}

abstract class FighterDecorator implements Fighter {

  private Fighter decoratedFighter;
  public FighterDecorator(Fighter _decoratedFighter) {
    decoratedFighter = _decoratedFighter;
  }

  @Override
  public void attack () {
    decoratedFighter.attack();
  }
}

class LaserDecorator extends FighterDecorator {
  public LaserDecorator (Fighter _decoratedFighter) {
    super(_decoratedFighter);
  }
  @Override
  public void attack () {
    super.attack();
    System.out.println("레이저 발사");
  }
}

class MissileDecorator extends FighterDecorator {
  public MissileDecorator (Fighter _decoratedFighter) {
    super(_decoratedFighter);
  }
  @Override
  public void attack () {
    super.attack();
    System.out.println("미사일 발사");
  }
}

class PlasmaDecorator extends FighterDecorator {
  public PlasmaDecorator (Fighter _decoratedFighter) {
    super(_decoratedFighter);
  }
  @Override
  public void attack () {
    super.attack();
    System.out.println("플라즈마 발사");
  }
}

(1) 위 코드를 class diagram 으로 그려 보세요.

인터페이스 Fighter의 구상 클래스 XWingFighter, Fighter를 감싸줄 데코레이터로 구성

(2) 위 코드를 기반으로 다음 예제를 만족하는 client 코드를 구현해 보세요.

  • 탄환 발사
  • 탄환 발사
    레이저 발사
  • 탄환 발사
    레이저 발사
    미사일 발사
    플라즈마 발사
// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
    public static void main(String[] args) {
        Fighter fighter = new XWingFighter();
        System.out.println("Case 1 : " );
        fighter.attack();

        System.out.println("Case 2 : " );
        fighter = new LaserDecorator(fighter);
        fighter.attack();

        System.out.println("Case 3 : " );
        fighter = new MissileDecorator(fighter);
        fighter = new PlasmaDecorator(fighter);
        fighter.attack();

    }
}

2 ) Decorator Pattern HW 2

유경이는 사칙연산 수식을 만들어내는 코드를 만들어 Decorator 패턴으로 보려고 한다.

정수와 사칙 연산자(operator) - 덧셈(Plus), 뺄셈(Minus), 곱셈(Mult), 나눗셈(Div) -으로 구성된
임의의 수식(Expression)을 decorator 패턴을 이용하여 표현하고자 한다.

단, 연산의 결과는 항상 정수라고 가정하며, 모든 사칙 연산자의 피연산자(operand)의 갯수는 두 개로 고정한다.

즉, 단항(unary) 연산자는 지원할 필요가 없다.

(1) decorator pattern을 사용한 class diagram을 개략적으로 그려 보세요.

-> 이미지 삽입

(2) decorator pattern을 사용하여 설계한 코드를 자바 또는 C++ 으로 구현해 보세요.

-> 코드

(3) 해당 설계를 기반으로 다음 예제의 수식들을 만들어내는 client 코드를 구현해 보세요.

  • 3
  • 2 + 5
  • ( ( 4 + 2 ) * ( 8 - 1 ) ) / 2

3 ) Decorator Pattern HW 3

"제환 호스팅" 회사는 웹 호스팅 업체이며
개인(personal), 교육(educational), 프리미엄(premium) 세 종류의 서버 플랜을 제공하고 있다.

각 플랜에는 암호화전송(SSL), 클라우드 호스팅(Cloud Hosting), 연중무중단(C365_24), 백업(Backup) 등의
옵션 및 조합이 자유롭게 추가될 수 있다.

(1) decorator pattern을 사용한 class diagram을 개략적으로 그려 보세요.

-> 이미지 삽입

(2) decorator pattern을 사용하여 설계한 코드를 자바 또는 C++ 으로 구현해 보세요.

public abstract class Plan {
    String description;
    public String getDescription() {
        return description;
    }

}

public class Educational extends Plan{

    public Educational(){
        this.description = "Educational";
    }
    public String getDescription() {
        return description;
    }

}

public class Personal extends Plan{

    public Personal(){
        this.description = "Personal";
    }
    public String getDescription() {
        return description;
    }

}

public class Premium extends Plan{

    public Premium(){
        this.description = "Premium";
    }
    public String getDescription() {
        return description;
    }

}

public abstract class Option extends Plan {
    Plan plan ;

    public Option(Plan _plan){
        this.plan = _plan;
    }
    public String getDescription() {
        return plan.getDescription();
    }

}

public class Encrypt extends Option {
    public Encrypt(Plan _plan){
        super(_plan);
    }
    public String getDescription() {
        return "SSL " + plan.getDescription();
    }

}

public class NonBlocking extends Option {
    public NonBlocking(Plan _plan){
        super(_plan);
    }
    public String getDescription() {
        return "C365_24 " + plan.getDescription();
    }

}

public class Cloud extends Option {
    public Cloud(Plan _plan){
        super(_plan);
    }
    public String getDescription() {
        return "Cloud " + plan.getDescription();
    }

}

public class Backup extends Option {
    public Backup(Plan _plan){
        super(_plan);
    }
    public String getDescription() {
        return "Backup " + plan.getDescription();
    }

}

(3) 해당 설계를 기반으로 다음 예제의 수식들을 만들어내는 client 코드를 구현해 보세요.

  • Educational server + C365_24
  • Personal server + SSL + CloudHosting + Backup
public class Main {
    public static void main(String[] args) {
        Plan server = new Educational();
        server = new NonBlocking(server);
        System.out.println(server.getDescription());

        Plan personalServer = new Personal();
        personalServer = new Encrypt(personalServer);
        personalServer = new Cloud(personalServer);
        personalServer = new Backup(personalServer);
        System.out.println(personalServer.getDescription());
    }
}

4 ) Decorator Pattern HW 4

유진이는 서브웨이를 창업하려고 한다. 개발자 출신인 유진이는 시스템을 직접 만들어 보고 싶다.

샌드위치의 베이스가 되는 빵(Bread)에는 Wheat, FlatBread, HoneyOat 세가지가 있다.
샌드위치에 추가될 수 있는 야채(Vegetable)와 소스(Sauce)에는 각각 Lettuce, Tomato 그리고 Ranch, HoneyMustard가 있다.

샌드위치를 만들기 위해서는 하나의 빵 종류를 고르고 야채와 소스를 임의로 추가할 수 있다.

빵의 가격은 다음과 같다.
Wheat: 1000원
FlatBread: 1500원
HoneyOat: 2000원

야채의 가격은 다음과 같다.
Lettuce: 300원
Tomato: 500원

소스의 가격은 무료이다.

Decorator 패턴을 사용하여 위의 요구사항들을 표현할 수 있도록 설계하고 코드로 나타내 보자.

단, Bread, Vegetable, Sauce 클래스는 추상(abstract) 클래스이고,
Wheat, FlatBread, HoneyOat, Lettuce, Tomato, Ranch, HoneyMustard 클래스는
구체(concrete) 클래스로 모델링을 하자.

아래는 Bread class이다.

public abstract class Bread {
	String description;
	public String getDescription() {
		return description;
	};
	abstract public int cost();
}

(1) decorator pattern을 사용한 class diagram을 개략적으로 그려 보세요.

-> 이미지 삽입

(2) decorator pattern을 사용하여 설계한 코드를 자바 또는 C++ 으로 구현해 보세요.

Bread, Sauce, Vegitable

public abstract class Bread {
    String description;
    public String getDescription() {
        return description;
    };
    abstract public int cost();
}

public abstract class Sauce extends Bread {

    Bread bread;

    public Sauce(Bread _bread){
        this.bread = _bread;
    }
    public String getDescription() {
        return bread.getDescription();
    };
    abstract public int cost();
}

public abstract class Vegetable extends Bread {

    Bread bread;

    public Vegetable(Bread _bread){
        this.bread = _bread;
    }
    public String getDescription() {
        return bread.getDescription();
    };
    abstract public int cost();
}

HoneyMustart, Ranch

public class HoneyMustard extends Sauce {

    public HoneyMustard(Bread _bread) {
        super(_bread);
        this.description = "허니 머스타드";
    }

    public String getDescription() {
        return description + " " + bread.getDescription();
    };
   
    // 소스는 공짜
    public int cost(){
        return bread.cost();
    }
}

public class Ranch extends Sauce {

    public Ranch(Bread _bread) {
        super(_bread);
        this.description = "랜치";
    }

    public String getDescription() {
        return description + " " + bread.getDescription();
    };
   
    // 소스는 공짜
    public int cost(){
        return bread.cost();
    }
}

Tomato, Lettuce

public class Lettuce extends Vegetable {

    public Lettuce(Bread _bread) {
        super(_bread);
        this.description = "양상추";
    }

    public String getDescription() {
        return description + " " + bread.getDescription();
    };
   
    // 소스는 공짜
    public int cost(){
        return 300 + bread.cost();
    }
}

public class HoneyMustard extends Sauce {

    public HoneyMustard(Bread _bread) {
        super(_bread);
        this.description = "허니 머스타드";
    }

    public String getDescription() {
        return description + " " + bread.getDescription();
    };
   
    // 소스는 공짜
    public int cost(){
        return bread.cost();
    }
}

(3) HoneyOat를 베이스로 Lettuce와 Tomato를 추가하고 HoneyMustard를 세번 두른 샌드위치를 만드는 client 코드를 작성하고, 가격과 description을 출력해 보세요.

public class Main {
    public static void main(String[] args) {

        Bread honeyOat = new HoneyOat();
        honeyOat = new Lettuce(honeyOat);
        honeyOat = new Tomato(honeyOat);
        honeyOat = new HoneyMustard(honeyOat);
        honeyOat = new HoneyMustard(honeyOat);
        honeyOat = new HoneyMustard(honeyOat);
        System.out.println(honeyOat.getDescription());
        System.out.println(honeyOat.cost());

    }
}

(4) FlatBread를 베이스로 Lettuce를 5번 추가한 샌드위치를 만드는 client 코드를 작성하고, 가격과 description을 출력해 보세요.

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
    public static void main(String[] args) {

        Bread flatBread = new FlatBread();
        flatBread = new Lettuce(flatBread);
        flatBread = new Lettuce(flatBread);
        flatBread = new Lettuce(flatBread);
        flatBread = new Lettuce(flatBread);
        flatBread = new Lettuce(flatBread);

        System.out.println(flatBread.getDescription());
        System.out.println(flatBread.cost());

    }
}

(5) 만약 빵의 사이즈를 결정 할 수 있게 요구사항이 추가되면 설계를 어떤식으로 해야할까요? 빵의 사이즈에 따라 토핑의 가격도 달라지고 빵의 가격도 달라집니다.

Size 데코레이터를 만들더라도 토핑이나 빵에 따라서 가격을 달리하기는 어려울 것임

{\rightarrow} 분기문을 통해 빵, 토핑의 가격을 다르게 적용하는 방법

Description 처럼 size를 property로 가지고 있고, cost 함수에서 분기문을 넣는 방법으로 사용하는게 좋을 것 같다.!

만약, 가격 인상률이 사이즈마다 %로 결정된다면, 이를 이용해 Decorator로 만들 수 있을 것이다.

5 ) Decorator Pattern HW 5

미로(maze)를 만들 수 있는 엔티티(Entity)들에는 Wall, Room, Door 가 있고, 동일한 이름의 클래스가 정의되어 있다.
Entity는 추상 클래스이며,
Wall, Room, Door 는 모두 Entity 클래스로부터 상속받은 자식 클래스(child class)이며, 구체 클래스(concrete class)로 가정한다.

초기의 설계 결과는 다음과 같다.

마법이 걸린 상태의 Wall Room, Door 를 표현할 수 있게 Enchanted Wall, Enchanted Room, Enchanted Door 클래스를 만들었다.
다시 폭탄이 숨겨진 Wall, Room, Door 를 표현할 수 있게 Bombed Wall, Bombed Room, Bombed Door 클래스를 만들었으며,
마법이 걸려있으며 폭탄이 숨겨진 것을 표현할 수 있도록 Bombed Enchanted Wall,
Bombed Enchanted Room, Bombed Enchanted Door 클래스를 만들었다.
향후에도 여러 테마들이 추가될 수 있는데 이와 같은 설계로는 그러한 확장에 적절히 대응하기 어려울 것이다.
예를 들어 인공지능 기능이 있는 AI Wall, AI Room, AI Door 가 추가되고,
기존의 테마인 Enchanted, Bombed 등과 조합된 개체를 표현하기 위해
AI Enchanted Wall, AI Enchanted Bombed Room 등의 클래스 정의가 필요하게 되어,
매우 많은 수의 클래스가 생겨나는 좋지 않은 설계로 평가된다.

(1) Decorator 패턴을 사용하여 위의 요구사항들을 표현할 수 있도록 새로 설계한 class diagram을 개략적으로 그려 보세요. 단, Entity, Wall, Room, Door 클래스는 수정할 수 없다고 가정한다.

(2) decorator pattern을 사용하여 설계한 코드를 자바 또는 C++ 으로 구현해 보세요.

Entity

public abstract class Entity {
    String description;
    String getDescription(){
        return description;
    }
}

Property

public abstract class Property extends Entity {

    Entity entity;
    public Property(Entity _entity){
        this.entity = _entity;
    }
    String getDescription(){
        return entity.getDescription();
    }
}

Wall, Door, Room

public class Wall extends Entity {
    public Wall(){
        this.description = "벽";
    }
}

public class Room extends Entity {
    public Room(){
        this.description = "방";
    }
}

public class Door extends Entity {
    public Door(){
        this.description = "문";
    }
}

AIable, Bombed, Enhanced

public class AIable extends Property{
    public AIable(Entity _entity) {
        super(_entity);
    }

    public String getDescription(){
        return "인공지능이 가미된 " + entity.getDescription();
    }
}

public class Bombed extends Property{
    public Bombed(Entity _entity) {
        super(_entity);
    }

    public String getDescription(){
        return "폭탄이 숨겨진 " + entity.getDescription();
    }
}

public class Enhanced extends Property{
    public Enhanced(Entity _entity) {
        super(_entity);
    }

    public String getDescription(){
        return "마법에 걸린 " + entity.getDescription();
    }
}

(3) 본인이 새로 설계한 내용을 기반으로, 클라이언트가 다음의 객체(들)을 만들어내는 코드를 작성하시오

  • 폭탄이 숨겨진(Bombed) Door
  • 마법이 걸려있고(Enchanted) 폭탄이 숨겨진(Bombed) Room
public class Main {
    public static void main(String[] args) {
        Entity door = new Door();
        door = new Bombed(door);
        System.out.println(door.getDescription());

        Entity wall = new Wall();
        wall = new Bombed(wall);
        wall = new Enhanced(wall);
        System.out.println(wall.getDescription());
    }
}

1개의 댓글

comment-user-thumbnail
2023년 11월 9일

2 ) Decorator Pattern HW 2
답이 어떻게 되나요? 업데이트 해주셔요

답글 달기