2. Decorator Pattern

2021. 9. 6. 18:03Computer Sciences/Design Patterns

문제

커피 주문 시스템에서 각 커피에 대한 첨가물이 추가해달라는 요구사항이 들어왔다. 이를 해결해보자.

상속

위처럼 상속을 이용해서 설계를 하면 첨가물과 커피가 늘어날 때마다 클래스를 만들어서 상속해야 한다. 이는 엄청난 복잡도를 유발하게 될 것이다.

인스턴스 변수와 슈퍼 클래스 상속

이렇게 하면 어떨까? 이 역시 문제가 많다.

  • 첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 한다.
  • 첨가물 종류가 늘어날 경우 그에 따른 메서드를 추가해야 한다(ex.시럽).
  • 첨가물이 들어가지 않는 음료의 경우에도 불필요한 메서드를 상속받는다(ex.아이스티).
  • 중복된 첨가물에 대한 표현이 불가능하다(ex.더블 모카).

해결 방법

객체에 추가적인 조건을 동적으로 추가해야 하는 문제는 데코레이터 패턴을 통해 해결할 수 있다.

데코레이터 패턴

위에서 말한대로 데코레이터 패턴은 객체에 추가 조건을 동적으로 추가하며, 서브 클래스를 만드는 것을 통해 기능을 유연하게 확장할 수 있는 방법을 제공한다. 위 요구사항을 적용한 클래스 다이어그램은 다음과 같다.

이를 바탕으로 코드를 구현해보자.

구현 코드

public abstract class Beverage {

    String description = "Unknown Beverage";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}
public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}
public class Espresso extends Beverage {

    public Espresso() {
        description = "Espresso";
    }

    @Override
    public double cost() {
        return 1.99;
    }
}
public class HouseBlend extends Beverage {

    public HouseBlend() {
        description = "House Blend Coffee";
    }

    @Override
    public double cost() {
        return .89;
    }
}
public class Mocha extends CondimentDecorator {

    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double cost() {
        return .20 + beverage.cost();
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Mocha";
    }
}
public class Whip extends CondimentDecorator {

    Beverage beverage;

    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double cost() {
        return .15 + beverage.cost();
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Whip";
    }
}
public class Whip extends CondimentDecorator {

    Beverage beverage;

    public Whip(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double cost() {
        return .15 + beverage.cost();
    }

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

실행 결과

Espresso $1.99
House Blend Coffee, Mocha, Mocha, Whip $1.44

코드 분석

먼저 Beverage 클래스를 분석해보자. Beverage는 추상 클래스로 작성됐고 cost()가 추상 메서드로 선언되어 있다. Beverage를 상속하는 음료(ex. Espresso)의 가격 설정을 유도하기 위해서다.

CondimentDecorator 클래스 또한 추상 클래스로 작성됐는데 여기서는 getDescription() 또한 추상 메서드로 선언됐다. 이 클래스를 상속받는 첨가물(ex. Mocha)이 첨가됐다는 것을 작성하도록 유도한 것이다. 그리고 cost() 또한 구현할 때 첨가물의 가격을 기존 beverage 인스턴스의 가격에 더하도록 한다. 이것이 beverage를 필드로 가지는 이유이다.

자바에서 사용되는 데코레이터 패턴 - I/O

자바에서 제공하는 I/O가 데코레이터 패턴으로 구현되어 있다. FileInputStream은 파일을 기본적으로 1바이트씩 읽는다. 그런데 파일의 용량이 크면 클수록 1바이트씩 읽어서는 속도가 너무 느리게 된다. 이를 BufferedInputStream을 적용해서 8KB 정도의 크기를 한 번에 읽어올 수 있다. 예를 들어 다음 코드와 같다.

import java.io.*;

public class JavaIODecorator {

    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("test.txt");
        BufferedInputStream bis = new BufferedInputStream(fis);

        // ...
    }
}

위에서 MochaWhip을 적용할 때와 똑같다. FileInputStream에서는 1바이트씩만 읽을 수 있었지만 BufferedInputStream의 기능을 추가하여 지정한 버퍼 크기만큼 파일을 읽을 수 있게 됐다. 실제로 I/O 클래스들을 살펴보면 모두 추상 클래스인 InputStream을 상속받아서 기능을 구현한다. FileInputStream는 추상 클래스는 아니지만 BufferedInputStreamInputStream을 구현한 클래스를 생성자로 받을 수 있어서 위와 같은 방식이 가능하다. 즉 데코레이터 패턴을 적용할 때 반드시 추상 클래스를 사용할 필요는 없다.
이러한 방식을 잘 이용하면 커스텀한 I/O 데코레이터를 만들 수도 있다. 예를 들어 FilterInputStream을 상속받아서 입력되는 문자를 모두 소문자로 변경하는 LowerCaseInputStream을 만들 수 있다.

import java.io.*;

public class LowerCaseInputStream extends FilterInputStream {

	public LowerCaseInputStream(InputStream in) {
		super(in);
	}

	public int read() throws IOException {
		int c = super.read();
		return (c == -1 ? c : Character.toLowerCase((char)c));
	}

	public int read(byte[] b, int offset, int len) throws IOException {
		int result = super.read(b, offset, len);
		for (int i = offset; i < offset+result; i++) {
			b[i] = (byte)Character.toLowerCase((char)b[i]);
		}
		return result;
	}
}
import java.io.*;

public class InputTest {

    public static void main(String[] args) throws IOException {
        int c;

        try {
            InputStream in = new BufferedInputStream(
                    new FileInputStream("src/decorator/test.txt")
            );

            while ((c = in.read()) >= 0) {
                System.out.print((char) c);
            }

            System.out.println();

            InputStream in2 = new LowerCaseInputStream(
                    			new BufferedInputStream(
                            			new FileInputStream("src/decorator/test.txt")
                    			)
            			);

            while ((c = in2.read()) >= 0) {
                System.out.print((char) c);
            }


            in.close();
        } catch (IOException e) {
            e.getStackTrace();
        }
    }
}

실행 결과

Nice to Meet You, Decorator!!
nice to meet you, decorator!!

'Computer Sciences > Design Patterns' 카테고리의 다른 글

6. Command Pattern  (0) 2021.10.14
5. Singleton Pattern  (0) 2021.09.15
4. Abstract Factory Method  (0) 2021.09.15
3. Factory & Factory Method Pattern  (0) 2021.09.15
1. Observer Pattern  (0) 2021.09.06