DPDesign Pattern Quest
#09 / 23
S構造DIFF

デコレータ

Decorator

ピザのトッピングだ。生地はそのまま、上にチーズ・サラミ・バジル……後乗せで好みに育てる。

Intent · 目的

オブジェクトに機能を、サブクラス化ではなく『包む』ことで動的に追加する。同じインタフェースのラッパーを重ねるだけで、必要な機能を必要な順に積み上げられる。

Motivation · 動機

「ファイル読み込みに、ログを足したい。あ、キャッシュも。圧縮も。暗号化も」── これをサブクラスで全組合せ作るとクラス爆発する(圧縮ありログなしキャッシュあり…)。Decorator なら、それぞれを1個のラッパーにして、欲しい順に重ねるだけ。コーヒーに『ホイップ追加 → シロップ追加 → エスプレッソ追加』と注文するみたいに、後から後から盛れる。`new Logging(new Compress(new Cache(file)))` で全部入りの完成。

適用場面

  • 1実行時にオブジェクトの機能を動的に増やしたり減らしたりしたい場合。
  • 2サブクラス化での拡張が現実的でない場合(機能の組合せが多すぎる)。
  • 3ロギング・認証・キャッシュ・圧縮といった横断的関心事を本体から剥がしたい場合。

概念図

サンプルコード

interface DataSource { read(): string; }

class FileDataSource implements DataSource {
  read(){ return "raw"; }
}

class LoggingDecorator implements DataSource {
  constructor(private inner: DataSource) {}
  read(){ console.log("[log] reading..."); return this.inner.read(); }
}

class CompressingDecorator implements DataSource {
  constructor(private inner: DataSource) {}
  read(){ return `gz(${this.inner.read()})`; }
}

const ds: DataSource = new LoggingDecorator(new CompressingDecorator(new FileDataSource()));
console.log(ds.read());
Pros · メリット
  • 実行時に機能を組み合わせて足し引きできる。組合せ爆発をクラス爆発に変えずに済む。
  • 1機能=1クラスの単純な責務分割。テストも個別にできる。
  • 横断的関心事(ロギング、計測、認証)を本体から綺麗に剥がせる。
Cons · デメリット
  • ラッパーの『順番』で挙動が変わる。Logging→Compress と Compress→Logging では出力が違う。
  • 等価性の判定が難しい(中身が同じでもラッパーが違うと別物に見える)。
  • デバッグ時にスタックがミルフィーユ状になり、追いかけるのに目が回る。

関連パターン

⚔ ready to test

理解度を確かめる時間だ

3 問のクイズで、 このパターンを身体に染み込ませよう。

挑戦する →