2021. 9. 6. 18:03ㆍComputer 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);
// ...
}
}
위에서 Mocha
와 Whip
을 적용할 때와 똑같다. FileInputStream
에서는 1바이트씩만 읽을 수 있었지만 BufferedInputStream
의 기능을 추가하여 지정한 버퍼 크기만큼 파일을 읽을 수 있게 됐다. 실제로 I/O 클래스들을 살펴보면 모두 추상 클래스인 InputStream
을 상속받아서 기능을 구현한다. FileInputStream
는 추상 클래스는 아니지만 BufferedInputStream
은 InputStream
을 구현한 클래스를 생성자로 받을 수 있어서 위와 같은 방식이 가능하다. 즉 데코레이터 패턴을 적용할 때 반드시 추상 클래스를 사용할 필요는 없다.
이러한 방식을 잘 이용하면 커스텀한 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 |