Decorator パターン

Decorator パターンは、既存のオブジェクトに動的に新しい責任や振る舞いを追加するデザインパターンです。このパターンを使用すると、継承を使わずにオブジェクトの機能を拡張できます。

目的と用途

Decorator パターンの主な目的は以下の通りです:

  • 実行時にオブジェクトに機能を動的に追加する
  • 継承ではなく、コンポジションと委譲を使用して機能拡張を行う
  • クラス爆発を防ぎながら、多彩な組み合わせの機能を実現する
  • オブジェクトの基本責任と拡張機能を分離する

一般的な用途:

  • GUIコンポーネント(境界線、スクロールバー、色などの機能を追加)
  • 入出力ストリーム(バッファリング、暗号化などの機能を追加)
  • ミドルウェア(ロギング、トランザクション、キャッシュなどの機能を追加)
  • サービスレイヤー(認証、権限チェックなどの機能を追加)

クラス図

図1: Decorator パターンのクラス図

シーケンス図

図2: Decorator パターンのシーケンス図

実装例

  • 基本実装
  • コーヒーショップ例
  • 入出力ストリーム例
  • GUI例

Decorator パターンの基本的な実装例です。コンポーネント、具象コンポーネント、デコレータの関係を示しています。

// コンポーネントインターフェース
public interface Component {
    String operation();
}

// 具象コンポーネント - 基本的な機能を提供
public class ConcreteComponent implements Component {
    @Override
    public String operation() {
        return "ConcreteComponent";
    }
}

// デコレータの抽象クラス
public abstract class Decorator implements Component {
    protected Component component;
    
    public Decorator(Component component) {
        this.component = component;
    }
    
    @Override
    public String operation() {
        return component.operation();
    }
}

// 具象デコレータA - 新しい状態を追加
public class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component component) {
        super(component);
    }
    
    @Override
    public String operation() {
        return "ConcreteDecoratorA(" + super.operation() + ")";
    }
}

// 具象デコレータB - 新しい振る舞いを追加
public class ConcreteDecoratorB extends Decorator {
    public ConcreteDecoratorB(Component component) {
        super(component);
    }
    
    @Override
    public String operation() {
        return "ConcreteDecoratorB(" + super.operation() + ")";
    }
    
    // 追加された振る舞い
    public String addedBehavior() {
        return "AddedBehavior";
    }
}

// クライアントコード
public class Client {
    public static void main(String[] args) {
        // 単純なコンポーネント
        Component simple = new ConcreteComponent();
        System.out.println("単純なコンポーネント: " + simple.operation());
        
        // デコレータAで装飾
        Component decoratorA = new ConcreteDecoratorA(simple);
        System.out.println("デコレータAで装飾: " + decoratorA.operation());
        
        // デコレータBで装飾
        Component decoratorB = new ConcreteDecoratorB(simple);
        System.out.println("デコレータBで装飾: " + decoratorB.operation());
        
        // デコレータBとデコレータAを組み合わせる
        Component decoratorBA = new ConcreteDecoratorB(new ConcreteDecoratorA(simple));
        System.out.println("デコレータBとAの組み合わせ: " + decoratorBA.operation());
    }
}

コーヒーショップを模した Decorator パターンの実装例です。基本のコーヒーにトッピングを追加していきます。

// 飲み物のインターフェース
public interface Beverage {
    String getDescription();
    double getCost();
}

// 具象コンポーネント - エスプレッソ
public class Espresso implements Beverage {
    @Override
    public String getDescription() {
        return "エスプレッソ";
    }
    
    @Override
    public double getCost() {
        return 300.0; // 300円
    }
}

// 具象コンポーネント - ハウスブレンドコーヒー
public class HouseBlend implements Beverage {
    @Override
    public String getDescription() {
        return "ハウスブレンドコーヒー";
    }
    
    @Override
    public double getCost() {
        return 250.0; // 250円
    }
}

// デコレータの抽象クラス
public abstract class CondimentDecorator implements Beverage {
    protected Beverage beverage;
    
    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }
    
    @Override
    public abstract String getDescription();
}

// 具象デコレータ - ミルク
public class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) {
        super(beverage);
    }
    
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", ミルク";
    }
    
    @Override
    public double getCost() {
        return beverage.getCost() + 50.0; // ミルク追加料金 50円
    }
}

// 具象デコレータ - モカ
public class Mocha extends CondimentDecorator {
    public Mocha(Beverage beverage) {
        super(beverage);
    }
    
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", モカ";
    }
    
    @Override
    public double getCost() {
        return beverage.getCost() + 70.0; // モカ追加料金 70円
    }
}

// 具象デコレータ - ホイップクリーム
public class Whip extends CondimentDecorator {
    public Whip(Beverage beverage) {
        super(beverage);
    }
    
    @Override
    public String getDescription() {
        return beverage.getDescription() + ", ホイップクリーム";
    }
    
    @Override
    public double getCost() {
        return beverage.getCost() + 80.0; // ホイップクリーム追加料金 80円
    }
}

// クライアントコード
public class CoffeeShop {
    public static void main(String[] args) {
        // 基本のエスプレッソ
        Beverage beverage1 = new Espresso();
        System.out.println(beverage1.getDescription() + ": " + beverage1.getCost() + "円");
        
        // ダブルモカ・ホイップ・エスプレッソ
        Beverage beverage2 = new Espresso();
        beverage2 = new Mocha(beverage2);  // モカを追加
        beverage2 = new Mocha(beverage2);  // モカをもう一つ追加
        beverage2 = new Whip(beverage2);   // ホイップクリームを追加
        System.out.println(beverage2.getDescription() + ": " + beverage2.getCost() + "円");
        
        // ミルク・モカ・ホイップ・ハウスブレンド
        Beverage beverage3 = new HouseBlend();
        beverage3 = new Milk(beverage3);   // ミルクを追加
        beverage3 = new Mocha(beverage3);  // モカを追加
        beverage3 = new Whip(beverage3);   // ホイップクリームを追加
        System.out.println(beverage3.getDescription() + ": " + beverage3.getCost() + "円");
    }
}

Java の I/O ストリームを模した Decorator パターンの実装例です。基本のストリームに機能を追加していきます。

// 入力ストリームの抽象クラス
public abstract class DataInputStream {
    public abstract int read();
    public abstract byte[] readBytes(int length);
}

// 具象コンポーネント - ファイル入力ストリーム
public class FileDataInputStream extends DataInputStream {
    private String filename;
    
    public FileDataInputStream(String filename) {
        this.filename = filename;
    }
    
    @Override
    public int read() {
        System.out.println("ファイル '" + filename + "' から1バイト読み込み");
        return 0; // 実際のファイル操作は省略
    }
    
    @Override
    public byte[] readBytes(int length) {
        System.out.println("ファイル '" + filename + "' から " + length + " バイト読み込み");
        return new byte[length]; // 実際のファイル操作は省略
    }
}

// デコレータの抽象クラス
public abstract class DataInputStreamDecorator extends DataInputStream {
    protected DataInputStream dataInputStream;
    
    public DataInputStreamDecorator(DataInputStream dataInputStream) {
        this.dataInputStream = dataInputStream;
    }
    
    @Override
    public int read() {
        return dataInputStream.read();
    }
    
    @Override
    public byte[] readBytes(int length) {
        return dataInputStream.readBytes(length);
    }
}

// 具象デコレータ - バッファリング機能
public class BufferedDataInputStream extends DataInputStreamDecorator {
    private byte[] buffer;
    private int bufferSize;
    private int position;
    private int count;
    
    public BufferedDataInputStream(DataInputStream dataInputStream, int bufferSize) {
        super(dataInputStream);
        this.bufferSize = bufferSize;
        this.buffer = new byte[bufferSize];
        this.position = 0;
        this.count = 0;
    }
    
    @Override
    public int read() {
        if (position >= count) {
            fill(); // バッファが空の場合、再充填
        }
        if (count <= 0) {
            return -1; // EOFの場合
        }
        return buffer[position++] & 0xff;
    }
    
    @Override
    public byte[] readBytes(int length) {
        System.out.println("バッファを使用して " + length + " バイト読み込み");
        return super.readBytes(length);
    }
    
    private void fill() {
        buffer = dataInputStream.readBytes(bufferSize);
        position = 0;
        count = buffer.length;
        System.out.println("バッファに " + count + " バイト読み込み");
    }
}

// 具象デコレータ - 暗号化解除機能
public class DecryptionDataInputStream extends DataInputStreamDecorator {
    private String algorithm;
    
    public DecryptionDataInputStream(DataInputStream dataInputStream, String algorithm) {
        super(dataInputStream);
        this.algorithm = algorithm;
    }
    
    @Override
    public int read() {
        int data = super.read();
        return decrypt(data);
    }
    
    @Override
    public byte[] readBytes(int length) {
        byte[] data = super.readBytes(length);
        System.out.println(algorithm + " アルゴリズムを使用してデータを復号化");
        return data; // 実際の復号化処理は省略
    }
    
    private int decrypt(int data) {
        System.out.println(algorithm + " アルゴリズムを使用してバイトを復号化");
        return data; // 実際の復号化処理は省略
    }
}

// クライアントコード
public class IOExample {
    public static void main(String[] args) {
        // 基本のファイル入力ストリーム
        DataInputStream fileInput = new FileDataInputStream("data.txt");
        fileInput.read();
        
        // バッファリング機能を追加
        DataInputStream bufferedInput = new BufferedDataInputStream(fileInput, 8192);
        bufferedInput.read();
        
        // 暗号化解除機能を追加
        DataInputStream decryptedInput = new DecryptionDataInputStream(fileInput, "AES");
        decryptedInput.read();
        
        // バッファリングと暗号化解除を組み合わせる
        DataInputStream bufferedDecryptedInput = new BufferedDataInputStream(
            new DecryptionDataInputStream(fileInput, "AES"), 8192);
        bufferedDecryptedInput.readBytes(1024);
    }
}

GUIコンポーネントを表現する Decorator パターンの実装例です。基本のコンポーネントに視覚効果を追加していきます。

// GUIコンポーネントのインターフェース
public interface VisualComponent {
    void draw();
    String getDescription();
}

// 具象コンポーネント - テキストフィールド
public class TextField implements VisualComponent {
    private int width;
    private int height;
    
    public TextField(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public void draw() {
        System.out.println("テキストフィールドを描画: " + width + "x" + height);
    }
    
    @Override
    public String getDescription() {
        return "テキストフィールド";
    }
}

// 具象コンポーネント - ボタン
public class Button implements VisualComponent {
    private String label;
    
    public Button(String label) {
        this.label = label;
    }
    
    @Override
    public void draw() {
        System.out.println("ボタンを描画: " + label);
    }
    
    @Override
    public String getDescription() {
        return "ボタン";
    }
}

// デコレータの抽象クラス
public abstract class VisualDecorator implements VisualComponent {
    protected VisualComponent component;
    
    public VisualDecorator(VisualComponent component) {
        this.component = component;
    }
    
    @Override
    public void draw() {
        component.draw();
    }
    
    @Override
    public String getDescription() {
        return component.getDescription();
    }
}

// 具象デコレータ - 境界線
public class BorderDecorator extends VisualDecorator {
    private int thickness;
    
    public BorderDecorator(VisualComponent component, int thickness) {
        super(component);
        this.thickness = thickness;
    }
    
    @Override
    public void draw() {
        super.draw();
        addBorder();
    }
    
    @Override
    public String getDescription() {
        return super.getDescription() + " + " + thickness + "px境界線";
    }
    
    private void addBorder() {
        System.out.println("境界線を追加: " + thickness + "px");
    }
}

// 具象デコレータ - スクロールバー
public class ScrollDecorator extends VisualDecorator {
    private String orientation; // "horizontal", "vertical", "both"
    
    public ScrollDecorator(VisualComponent component, String orientation) {
        super(component);
        this.orientation = orientation;
    }
    
    @Override
    public void draw() {
        super.draw();
        addScrollBar();
    }
    
    @Override
    public String getDescription() {
        return super.getDescription() + " + " + orientation + "スクロールバー";
    }
    
    private void addScrollBar() {
        System.out.println(orientation + "スクロールバーを追加");
    }
}

// 具象デコレータ - 背景色
public class BackgroundDecorator extends VisualDecorator {
    private String color;
    
    public BackgroundDecorator(VisualComponent component, String color) {
        super(component);
        this.color = color;
    }
    
    @Override
    public void draw() {
        addBackground();
        super.draw();
    }
    
    @Override
    public String getDescription() {
        return super.getDescription() + " + " + color + "背景";
    }
    
    private void addBackground() {
        System.out.println(color + "背景を追加");
    }
}

// クライアントコード
public class GUIExample {
    public static void main(String[] args) {
        // 基本のテキストフィールド
        VisualComponent textField = new TextField(200, 30);
        textField.draw();
        System.out.println("説明: " + textField.getDescription());
        System.out.println();
        
        // 境界線付きのテキストフィールド
        VisualComponent borderedTextField = new BorderDecorator(textField, 2);
        borderedTextField.draw();
        System.out.println("説明: " + borderedTextField.getDescription());
        System.out.println();
        
        // 境界線とスクロールバー付きのテキストフィールド
        VisualComponent scrollableBorderedTextField = new ScrollDecorator(
            new BorderDecorator(textField, 2), "vertical");
        scrollableBorderedTextField.draw();
        System.out.println("説明: " + scrollableBorderedTextField.getDescription());
        System.out.println();
        
        // 背景色と境界線付きのボタン
        VisualComponent coloredBorderedButton = new BackgroundDecorator(
            new BorderDecorator(new Button("送信"), 1), "青");
        coloredBorderedButton.draw();
        System.out.println("説明: " + coloredBorderedButton.getDescription());
    }
}

実際の使用例

Javaの標準ライブラリやフレームワークには、Decoratorパターンを使用している例が多くあります:

  • java.io パッケージ - 最も有名な例で、InputStream/OutputStream クラス階層です。例えば BufferedInputStream は InputStream を装飾し、バッファリング機能を追加します。
  • java.util.Collections - unmodifiableList(), synchronizedList(), checkedList() などのメソッドは、既存のリストを装飾し、新しい振る舞いを追加します。
  • javax.swing.JScrollPane - コンポーネントを装飾してスクロール機能を追加します。
  • javax.servlet.http.HttpServletRequestWrapper とHttpServletResponseWrapper - サーブレットリクエストとレスポンスを装飾します。

メリットとデメリット

メリット

  • 単一責任の原則に従い、個々の責任を分離できる
  • 継承よりも柔軟に機能拡張できる
  • 実行時に動的に機能を追加できる
  • 機能の組み合わせによるクラス爆発を防ぐ
  • 既存のコードを修正せずに新しい機能を追加できる(開放閉鎖の原則)

デメリット

  • 小さなデコレータオブジェクトが多数作成されるため、コードが複雑になる可能性がある
  • デコレータの順序が重要な場合があり、その管理が難しい
  • デコレータとデコレートされたコンポーネントの型の同一性が失われる
  • デコレータの重ねがけで、コンポーネントの特定の操作を見つけにくくなる

関連パターン

  • Adapter: Adapterはインターフェースを変更し、Decoratorは機能を追加しますが、インターフェースは変更しません。
  • Composite: Decoratorはしばしば単一コンポーネントで使用されますが、単一コンポーネントと複合コンポーネントを同じように扱うCompositeと共に使われることもあります。
  • Strategy: Decoratorはオブジェクトの外側(振る舞い)を変更し、Strategyはオブジェクトの内側(アルゴリズム)を変更します。
  • Chain of Responsibility: Chain of Responsibilityはコマンド処理の責任を委譲し、Decoratorは機能を追加します。
  • Proxy: Proxyはオブジェクトへのアクセスを制御し、Decoratorは機能を追加します。

実装時の注意点

  • インターフェースの一貫性: デコレータとコンポーネントは同じインターフェースを実装する必要があります。
  • 透過性: クライアントがデコレータとデコレートされたオブジェクトを区別できないようにします。
  • 軽量性: デコレータは軽量にし、多数のデコレータが存在する場合でもパフォーマンスに影響が出ないようにします。
  • 抽象デコレータの必要性判断: すべてのデコレータに共通コードがある場合のみ、抽象デコレータクラスを導入します。
  • デコレータの順序: 複数のデコレータを組み合わせる場合、その順序を考慮します。