java-recipes

ホーム デザインパターン › DP-09

DP-09: Decorator パターン

継承ではなく「委譲(いじょう)」によってオブジェクトに機能を動的に追加するパターンです。BufferedInputStream BufferedReader など、 Java 標準ライブラリでも広く使われています。

Decorator パターンとは

あるオブジェクトに機能を追加したいとき、最初に思いつくのは「継承してサブクラスを作る」方法です。 しかし継承では「静的」に機能が決まってしまい、実行時に「この機能だけ追加する」「あの機能は不要」という柔軟な組み合わせができません。 Decorator パターンでは、元のオブジェクトを「ラップ(包む)」する形で機能を追加します。 ラップするオブジェクト(デコレータ)も同じインターフェースを実装しているため、 利用側はデコレータが何重に巻かれていても同じように扱えます。

継承との違い

  • 継承(静的): クラス設計時に機能の組み合わせを全て決める必要がある。「Trim かつ UpperCase かつ Logging」のクラスを事前に作らなければならない
  • Decorator(動的): 実行時に機能を組み合わせられる。new LoggingDecorator(new UpperCaseDecorator(new TrimDecorator(...))) のように自由に積み重ねられる

Java 標準ライブラリでは I/O クラスが Decorator パターンの代表例です。 それぞれが同じインターフェース(InputStream / Reader)を実装しながら、 包む度に機能が追加されます。

Java 標準ライブラリの Decorator 実例

// 例1: BufferedReader + InputStreamReader(重ね包み)
//   InputStreamReader: バイトストリーム → 文字ストリームへ変換
//   BufferedReader: バッファリングを追加して readLine() が使えるようになる
BufferedReader reader = new BufferedReader(
    new InputStreamReader(System.in, "UTF-8"));

// 例2: BufferedInputStream(バッファリングのみ追加)
//   InputStream をそのままでは1バイトずつ読む
//   BufferedInputStream でラップするだけでバッファリングが有効になる
InputStream raw = new FileInputStream("data.bin");
BufferedInputStream buffered = new BufferedInputStream(raw);

// 例3: GZIPOutputStream(圧縮を追加)
//   FileOutputStream の上に GZIPOutputStream を重ねるだけで圧縮書き込みになる
OutputStream file = new FileOutputStream("data.gz");
GZIPOutputStream gzip = new GZIPOutputStream(file);

サンプルコード

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

public class DecoratorPatternSample {

    // テキスト処理のインターフェース
    interface TextProcessor {
        String process(String text);
    }

    // 基本実装: テキストをそのまま返す
    static class PlainTextProcessor implements TextProcessor {
        @Override
        public String process(String text) {
            return text;
        }
    }

    // デコレータの抽象基底クラス: TextProcessor を実装し、委譲先を保持する
    static abstract class TextProcessorDecorator implements TextProcessor {
        protected final TextProcessor wrapped; // 委譲先

        public TextProcessorDecorator(TextProcessor wrapped) {
            this.wrapped = wrapped;
        }
    }

    // 大文字変換デコレータ
    static class UpperCaseDecorator extends TextProcessorDecorator {
        public UpperCaseDecorator(TextProcessor wrapped) {
            super(wrapped);
        }

        @Override
        public String process(String text) {
            // 委譲先の処理結果を大文字に変換する
            String result = wrapped.process(text);
            return result.toUpperCase();
        }
    }

    // 前後の空白除去デコレータ
    static class TrimDecorator extends TextProcessorDecorator {
        public TrimDecorator(TextProcessor wrapped) {
            super(wrapped);
        }

        @Override
        public String process(String text) {
            // 委譲先の処理結果の前後空白を除去する
            String result = wrapped.process(text);
            return result.trim();
        }
    }

    // ログ出力デコレータ: 処理前後にログを出力する
    static class LoggingDecorator extends TextProcessorDecorator {
        public LoggingDecorator(TextProcessor wrapped) {
            super(wrapped);
        }

        @Override
        public String process(String text) {
            System.out.println("[LOG] 処理開始: 入力=\"" + text + "\"");
            String result = wrapped.process(text);
            System.out.println("[LOG] 処理完了: 出力=\"" + result + "\"");
            return result;
        }
    }

    // プレフィックス付与デコレータ
    static class PrefixDecorator extends TextProcessorDecorator {
        private final String prefix;

        public PrefixDecorator(TextProcessor wrapped, String prefix) {
            super(wrapped);
            this.prefix = prefix;
        }

        @Override
        public String process(String text) {
            // 委譲先の処理結果にプレフィックスを付与する
            String result = wrapped.process(text);
            return prefix + result;
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println("=== Decorator パターン: テキスト処理 ===");

        // 基本実装だけを使う場合
        TextProcessor plain = new PlainTextProcessor();
        System.out.println("基本: " + plain.process("  Hello, World!  "));

        // デコレータをチェーン: Trim → UpperCase
        TextProcessor trimAndUpper = new UpperCaseDecorator(
                new TrimDecorator(
                        new PlainTextProcessor()));
        System.out.println("Trim + UpperCase: " + trimAndUpper.process("  Hello, World!  "));

        // デコレータをチェーン: Prefix → UpperCase → Trim(ログあり)
        TextProcessor full = new LoggingDecorator(
                new PrefixDecorator(
                        new UpperCaseDecorator(
                                new TrimDecorator(
                                        new PlainTextProcessor())),
                        "[INFO] "));
        String result = full.process("  hello, java!  ");
        System.out.println("最終結果: " + result);

        System.out.println("\n=== Java 標準ライブラリの Decorator 例 ===");
        // BufferedReader(InputStreamReader(...)) は Decorator パターンの実例
        // InputStreamReader: バイトストリーム → 文字ストリームへ変換
        // BufferedReader: バッファリングを追加
        String data = "Hello\nDecorator\nPattern";
        ByteArrayInputStream bais = new ByteArrayInputStream(data.getBytes("UTF-8"));
        BufferedReader reader = new BufferedReader(new InputStreamReader(bais, "UTF-8"));
        String line;
        while ((line = reader.readLine()) != null) {
            System.out.println("読み込み: " + line);
        }
        reader.close();

        // BufferedInputStream: バッファリングを追加するデコレータ
        ByteArrayInputStream rawStream = new ByteArrayInputStream(new byte[]{72, 101, 108, 108, 111});
        BufferedInputStream buffered = new BufferedInputStream(rawStream);
        System.out.println("BufferedInputStream の available(): " + buffered.available());
        buffered.close();
    }
}

Java 8 では抽象クラス TextProcessorDecorator を使ってデコレータの共通ロジックを集約します。wrapped フィールドに委譲先を保持し、process() を呼び出してから機能を追加します。

よくあるミス・注意点

⚠️ デコレータチェーンが深くなりすぎてデバッグが困難になる

デコレータを何重にも重ねると、どのデコレータが何番目に処理されているか追いにくくなります。 エラーが発生したとき「どのデコレータが原因か」を特定するのが難しくなるため、LoggingDecorator のような ログ出力デコレータを活用して処理の流れを可視化しましょう。 また、デコレータの数が多くなる場合はファクトリメソッドにまとめると管理しやすくなります。

⚠️ デコレータの順序を間違える

デコレータは最も外側から順に処理されます。 例えば new TrimDecorator(new UpperCaseDecorator(...))new UpperCaseDecorator(new TrimDecorator(...)) では 処理の順序が逆になります。「まずトリムしてから大文字化する」か 「まず大文字化してからトリムする」かで結果が変わる場合があります。

⚠️ デコレータを使うべき場面で継承してしまう

「Trim かつ UpperCase かつ Logging の TextProcessor が欲しい」と思ったとき、TrimUpperCaseLoggingTextProcessor という サブクラスを作ってしまうと、機能の組み合わせの数だけクラスが増えてしまいます。 機能の追加・取り外しが実行時に必要な場合は Decorator パターンを検討しましょう。

テストする観点

  • 各デコレータが単独で正しく動作すること(UpperCaseDecorator は大文字変換、TrimDecorator は空白除去)
  • デコレータを2つ以上組み合わせたとき、期待通りの順序で処理されること
  • 空文字列(境界値)を渡したとき、各デコレータが正しく動作すること
  • null を渡したとき、適切に例外がスローされること(または null 安全に処理されること)
  • LoggingDecorator が処理前後に正しくログを出力すること
  • デコレータの順序を変えたとき(Trim→UpperCase と UpperCase→Trim)、処理結果の違いが期待通りであること

GitHub でソースコードを見る →