[Design Pattern] Decorator Pattern

younghyun·2022년 10월 26일
0

Design Pattern

목록 보기
8/14
post-thumbnail

Decorator Pattern 이란

객체를 결합하고 객체에 추가적인 책임을 동적으로 부여하여, 서브 클래스를 만들지 않고 기능을 유연하게 확장할 수 있게 해주는 패턴이다.

설계

  • Component (인터페이스 or 추상클래스)
    : 특정 기능 제공 메소드 define
    • 각 구성요소는 직접 쓰일 수도 있고 데코레이터로 감싸져서 쓰일 수도 있음
  • Decorator (Component와 같은 인터페이스 또는 추상 클래스)
    : Component의 기능 재정의, 기타 메소드 추가
    • Component 객체가 들어있고 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수 존재
    • Component의 기능 및 상태를 확장할 수 있음
    • 상속을 통해서 형식만 맞추는 것이지, 구체적인 행동을 물려받진 않음
  • ConcreteComponent
    : 새로운 행동을 동적으로 추가
  • ConcreteDecorator (Decorator의 구체적인 행동)
    : Component 객체를 위한 인스턴스 변수가 있음

예시1

스타버즈 커피 주문 어플리케이션

  • 메뉴
    • 커피 종류: HouseBlend, DarkRoast, Decaf, Espresso
    • 토핑 종류: 스팀 우유, 두유, 모카, 휘핑 크림
  • 기능: 음료 설명, cost 계산

Bad Case

  • 문제점: 옵션 종류가 추가되면 Beverage에 새로운 메소드를 추가해야하고, 이로 인해 Beverage를 상속받는 다른 클래스에 버그가 생길 수 있다.

    ⭐디자인원칙: 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.


Good Case

1. 데코레이터 패턴을 활용한 생각의 전환

커피 종류에서 시작해서 토핑으로 장식
모카와 휘핑 크림을 추가한 에스프레소 -> Wrapper class: Mocha, Whip

  • 에스프레소Beverage로부터 상속
  • 모카휘핑 객체는 데코레이터. 이 객체의 형식은 이 객체가 장식하고 있는 객체(에스프레소)를 반영하므로 Beverage
  • 가격 계산 과정
    • 가장 바깥쪽에 있는 데코레이터인 Whipcost()를 호출
    • 그 객체가 장식하고 있는 객체한테 가격 계산을 위임
    • 가격이 구해지고 나면, 구해진 가격에 휘핑 크림의 가격을 더한 다음 그 결과를 반환

2. 클래스 구성

  1. Component: Beverage
  2. ConcreteComponent: 구성요소를 나타내는 구체적인 클래스. 커피 종류 (HouseBlend, DarkRoast, Espresso, Decaf)
  3. Decorator: Beverage 클래스를 확장하여 만든 클래스 (CondimentDecorator)
  4. ConcreteDecorator: 토핑 종류 (Milk, Mocha, Soy, Whip)

3. 실제 구현

Component

public abstract class Beverage {
    String description = "제목 없음";
    
    public String getDescription() {   // 음료 설명
        return description;
    }
    
    public abstract double cost();    // cost 계산 뼈대
}

Decorator

public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();     // '정의'만 구현, 구체적인 행동은 ConcreteDecorator에서.
}

ConcreteComponent

public class Espresso extends Beverage {
    public Espresso() {
        description = "에스프레소";   // description은 Beverage로부터 상속받음
    }
    public double cost() {
        return 4000;      // 가격은 에스프레소 가격만 입력
    }
}

ConcreteDecorator

public class Mocha extends CondimentDecorator {

    Beverage beverage; // 데코레이션 할 음료를 저장하기 위한 인스턴스 변수
    
    public Mocha(Beverage beverage) {
        this.beverage = beverage;  // 생성자를 통해 데코레이션 할 음료객체를 전달
    }
    
    public String getDescription() {
        return beverage.getDescription() + ", 모카";   // 음료 설명에 Mocha 추가
    }
    
    public double cost() {
        return beverage.cost() + 1000;  // 음료 가격에 토핑요금 추가
    }
}

Main

public class StarbuzzCoffee {

    public static void main(String[] args[]) {
    
    Beverage b = new Espresso();
    System.out.println(b.getDescription() + " $" + b.cost());
    
    Beverage b2 = new DarkRoast();
    b2 = new Mocha(b2);
    b2 = new Mocha(b2); // 모카 한 개 더 추가
    b2 = new Whip(b2);
    System.out.println(b2.getDescription() + " $" + b2.cost());
    
    Beverage b3 = new HouseBlend();
    b3 = new Soy(b3);
    b3 = new Mocha(b3);
    b3 = new Whip(b3);
    System.out.println(b3.getDescription() + " $" + b3.cost());
    }
    
}

예시2

자바 I/O

  • 자바의 입출력은 io 패키지에서 처리하고, 4개의 클래스(InputStream, OutputStream,Reader, Writer) 를 중심으로 데코레이터 패턴을 사용
  • 바이트 기반 스트림문자 기반 스트림: 데코레이터의 구성요소로 쓰이며, 추상 클래스라서 직접 사용할 수 없음

Input/Output Stream

  • 바이트 기반 스트림: InputStream, OutputStream (바이트단위로 데이터를 전송하는 클래스)
    • FileIntputstream/FileOutputStream (파일 입출력에 사용되는 스트림 클래스)
  • 바이트 기반 보조 스트림 (스트림의 기능을 보완하기 위한 클래스)
    • BufferedInputStream/BufferedOutputStream
    • DataInputStream/DataOutputStream
  • 문자 기반 스트림: Reader, Writer
    • FileReader/FileWriter
  • 문자 기반 보조 스트림
    • BufferedReader/BufferedWriter
    • InputStreamReader/OutputStreamWriter
  • PrintWriter

예제1. 파일 입력 (바이트 기반 스트림: InputStream)

바이트 기반 스트림은 바이트단위로 데이터를 전송하는 클래스로 InputStream과 OutputStream을 상속받는 FileStream, ByteArrayStream, PipedStream, AudioStream, StringBufferStream 등이 있다.

  • FileInputStream 구성요소
    : 파일에서부터 데이터를 입력받기 위해서 만들어진 클래스
  • BufferedInputStream 데코레이터
    • 입력된 내용을 버퍼에 저장
    • 입력 내용을 한 줄씩 읽을 수 있게 readLine() 제공
  • LineNumberInputStream 데코레이터
    • 줄 번호를 붙여줌
import java.io.FileInputStream;

public class ReadFile { 
    public static void main(String[] args) {
        try {
            // 읽기
            FileInputStream readme = new FileInputStream("readme.txt");
            int b = readme.read();   // 바이트 단위로 읽음
            System.out.println("b = " + b);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileInputStream;
import java.io.BufferedInputStream;

public class ReadFile { 
    public static void main(String[] args) {
        try {
            BufferedInputStream readme   
                     = new BufferedInputStream(   // 기반 스트림 생성 후 기반 스트림을 이용한 보조 스트림 생성
                         new FileInputStream("readme.txt"));   // 기반 스트림 생성 
            int b = readme.read();
            System.out.println("b = " + b);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

예제2. 파일 입력 (문자 기반 스트림: Reader)

Reader 클래스는 문자 기반 스트림 클래스들이 상속받은 부모 클래스로 byte 배열이 아닌 char 배열을 사용하는 read()가 추상메서드로 구현되어 있다.

import java.io.FileReader;

public class ReadFile { 
    public static void main(String[] args) {
        try {
            // 읽기
            FileReader readme = new FileReader("readme.txt");
            int b = readme.read();
            System.out.println("b = " + b);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileReader;
import java.io.BufferedReader;
 
public class ReadFile { 
    public static void main(String[] args) {
        try {
            BufferedReader readme = new BufferedReader(     // BufferedReader가 FileReader 감싸기
                                      new FileReader("readme.txt"));
            String line = readme.readLine();        // readLine()은 BufferedReader에 존재
            System.out.println("line = " + line);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileReader;
import java.io.LineNumberReader;

public class ReadFile { 
    public static void main(String[] args) {
        try {
            LineNumberReader readme = new LineNumberReader(    // LineNumberReader를 FileReader 위에 덧씌움
                                         new FileReader("readme.txt"));
            String line = readme.readLine();
            System.out.println("line " + readme.getLineNumber() + " = " + line);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

예제3. 소문자 데코레이터

입력 스트림에 있는 대문자를 전부 소문자로 바꿔주는 데코레이터

import java.io.FilterInputStream;    // 이것을 상속받아서 새로운 데코레이터 만들기
import java.io.InputStream;
import java.io.IOException;

public class LowerCaseInputStream extends FilterInputStream { 

    public LowerCaseInputStream(InputStream in) {      // InputStream = Component
        super(in);
    }
    
    public int read() throws IOException {    // InputStream 안에 read() 존재
        int c = super.read();
        return ((c == -1) ? c : Character.toLowerCase((char) c));
    }
    
    public int read(byte[] b, int offset, int len) throws IOException {  // InputStream 안에 read(b, offset, len) 존재
        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 LowerCaseInputStream(          // LowerCaseInputStream으로 감싸기
                               new BufferedInputStream(          // BufferedInputStream으로 감싸고
                                 new FileInputStream("test.txt")));    // FileInputStream = 기본 Component
            while ((c = in.read()) >= 0) {
                System.out.print((char) c);
            }
            in.close();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}
profile
🌱 주니어 백엔드 개발자입니당

0개의 댓글