概要

テキストの大文字変換、前後空白の除去、ログ出力、プレフィックスの付与――こうした処理を個別の条件で組み合わせたい場合、if 文の分岐やサブクラスの組み合わせ爆発に悩まされがちです。Decorator パターンは、同じインターフェースを実装するラッパーを重ねることで、元のオブジェクトを変更せず機能を動的に追加します。Java 標準ライブラリの I/O ストリーム(BufferedReader が InputStreamReader を包み、さらに InputStream を包む)は Decorator パターンの代表例です。この記事ではテキスト処理パイプラインを題材に Decorator の基本構造を示し、Proxy パターンとの違い、Java の I/O ストリームが同じ構造であることを確認します。

使いどころ

テキスト処理パイプラインで、Trim・大文字変換・ログ出力・プレフィックス付与を実行時に任意の順序で組み合わせる

API レスポンスに対して、キャッシュ・認証チェック・ログ記録を段階的に追加し、各デコレータを独立にテストする

ファイル読み書きで BufferedReader / InputStreamReader / FileInputStream のような多段のラッパーを構成する

コード例

DecoratorPatternSample.java
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.IOException;

public class DecoratorPatternSample {

    // Component インターフェース
    interface TextProcessor {
        String process(String text);
    }

    // 基本実装
    static class PlainTextProcessor implements TextProcessor {
        @Override
        public String process(String text) { return text; }
    }

    // Decorator 基底クラス
    static abstract class TextDecorator implements TextProcessor {
        protected final TextProcessor wrapped;
        public TextDecorator(TextProcessor wrapped) {
            this.wrapped = wrapped;
        }
    }

    // 具体 Decorator: 大文字変換
    static class UpperCaseDecorator extends TextDecorator {
        public UpperCaseDecorator(TextProcessor w) { super(w); }
        @Override
        public String process(String text) {
            return wrapped.process(text).toUpperCase();
        }
    }

    // 具体 Decorator: Trim
    static class TrimDecorator extends TextDecorator {
        public TrimDecorator(TextProcessor w) { super(w); }
        @Override
        public String process(String text) {
            return wrapped.process(text).trim();
        }
    }

    // 具体 Decorator: ログ出力
    static class LoggingDecorator extends TextDecorator {
        public LoggingDecorator(TextProcessor w) { super(w); }
        @Override
        public String process(String text) {
            System.out.println("[LOG] 入力: \"" + text + "\"");
            var result = wrapped.process(text);
            System.out.println("[LOG] 出力: \"" + result + "\"");
            return result;
        }
    }

    public static void main(String[] args) throws IOException {
        // デコレータを重ねて処理パイプラインを構築
        var processor = new LoggingDecorator(
            new UpperCaseDecorator(
                new TrimDecorator(new PlainTextProcessor())));
        System.out.println("結果: " + processor.process("  hello, java!  "));

        // 標準ライブラリの Decorator 例
        var data = "Hello\nDecorator";
        var reader = new BufferedReader(new InputStreamReader(
            new ByteArrayInputStream(data.getBytes("UTF-8"))));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println("読み込み: " + line);
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var でデコレータチェーンの変数宣言が簡潔になる。record で処理結果のメタ情報を保持できる。

Java 17
// Java 17: var + record で処理結果を管理
var processor = new UpperCaseDecorator(
    new TrimDecorator(new PlainTextProcessor()));
var result = processor.process("  hello  ");
record ProcessResult(String original, String processed) {}
var pr = new ProcessResult("  hello  ", result);

Library Comparison

標準 API(interface + 委譲)処理の組み合わせパターンが実行時に決まる場合。I/O ストリームと同じ設計思想で構築できる。デコレータの数が増えるとクラス数も増える。
Java I/O ストリームファイル読み書きでバッファリング・文字コード変換・圧縮などを組み合わせるとき。Decorator パターンの実例として最も身近。I/O に特化しており、汎用のデコレータ設計のテンプレートとしてはやや複雑。

注意点

デコレータを重ねすぎるとスタックトレースが深くなり、デバッグ時にどの層で問題が起きているか追いにくい

Decorator と Proxy は構造が似ているが目的が異なる。Decorator は機能追加、Proxy はアクセス制御・遅延初期化が主目的

デコレータの適用順序が結果に影響する場合がある。Trim してから大文字にするのと、大文字にしてから Trim するのでは結果が異なるケースがある

各デコレータが同じインターフェースを忠実に守ることが前提。インターフェースの契約を破るデコレータは連鎖の中で予期しない動作を引き起こす

FAQ

Decorator と継承のどちらを使うべきですか。

機能の組み合わせが実行時に変わる場合は Decorator が適しています。組み合わせが固定なら継承でも構いませんが、クラス爆発に注意してください。

Decorator パターンと Proxy パターンの見分け方は。

Decorator は機能の追加(ログ・キャッシュ・変換)が目的で、Proxy はアクセス制御・遅延初期化が目的です。構造は似ていますが、設計意図が異なります。

Java の I/O ストリームが Decorator パターンだと知ると何が変わりますか。

InputStream → InputStreamReader → BufferedReader の多段ラッパーが設計パターンだと分かれば、新しい I/O クラスを見ても構造を理解しやすくなります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます