概要
テキストの大文字変換、前後空白の除去、ログ出力、プレフィックスの付与――こうした処理を個別の条件で組み合わせたい場合、if 文の分岐やサブクラスの組み合わせ爆発に悩まされがちです。Decorator パターンは、同じインターフェースを実装するラッパーを重ねることで、元のオブジェクトを変更せず機能を動的に追加します。Java 標準ライブラリの I/O ストリーム(BufferedReader が InputStreamReader を包み、さらに InputStream を包む)は Decorator パターンの代表例です。この記事ではテキスト処理パイプラインを題材に Decorator の基本構造を示し、Proxy パターンとの違い、Java の I/O ストリームが同じ構造であることを確認します。
使いどころ
テキスト処理パイプラインで、Trim・大文字変換・ログ出力・プレフィックス付与を実行時に任意の順序で組み合わせる
API レスポンスに対して、キャッシュ・認証チェック・ログ記録を段階的に追加し、各デコレータを独立にテストする
ファイル読み書きで BufferedReader / InputStreamReader / FileInputStream のような多段のラッパーを構成する
コード例
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);
}
}
}Version Coverage
var でデコレータチェーンの変数宣言が簡潔になる。record で処理結果のメタ情報を保持できる。
// 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
注意点
デコレータを重ねすぎるとスタックトレースが深くなり、デバッグ時にどの層で問題が起きているか追いにくい
Decorator と Proxy は構造が似ているが目的が異なる。Decorator は機能追加、Proxy はアクセス制御・遅延初期化が主目的
デコレータの適用順序が結果に影響する場合がある。Trim してから大文字にするのと、大文字にしてから Trim するのでは結果が異なるケースがある
各デコレータが同じインターフェースを忠実に守ることが前提。インターフェースの契約を破るデコレータは連鎖の中で予期しない動作を引き起こす
FAQ
機能の組み合わせが実行時に変わる場合は Decorator が適しています。組み合わせが固定なら継承でも構いませんが、クラス爆発に注意してください。
Decorator は機能の追加(ログ・キャッシュ・変換)が目的で、Proxy はアクセス制御・遅延初期化が目的です。構造は似ていますが、設計意図が異なります。
InputStream → InputStreamReader → BufferedReader の多段ラッパーが設計パターンだと分かれば、新しい I/O クラスを見ても構造を理解しやすくなります。