gof-decorator について
gof-decoratorは、オブジェクトを変更せずに振る舞いを拡張する設計パターンを学ぶ開発者向けスキルです。インターフェースを保つラッパーの構成や順序、注意点を解説します。
主な機能
- 01実行時の振る舞い拡張
- 02インターフェースを保つラッパー
- 03組み合わせ可能な単一責務のデコレーター
- 04順序の指針と注意点
活用シーン
- 01オブジェクトへのログ記録やキャッシュ追加
- 02サブクラスを増やさず任意機能を組み合わせる
- 03Java I/Oのようなオブジェクトチェーンの拡張
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ yonatankarp/ software-design-skills --skill gof-decoratorインストール後、Claude Code で/gof-decoratorを入力して読み込まれたか確認します。
詳細
1 行での概要
オブジェクトを、同じインターフェースを共有して機能を追加する別のオブジェクトで包みます。元のオブジェクトを変更せず、実行時に任意の組み合わせで追加できます。
このスキルを使う場面
- クラスに直交する拡張が N 個必要な場合(コーヒーにミルク、砂糖、フォーム、シロップなどを任意に組み合わせて加えるなど)。サブクラス化では N! 個のバリエーションが必要になりますが、デコレーターなら小さなラッパーを N 個作るだけです。
- Java の I/O ストリーム(
BufferedReader(InputStreamReader(FileInputStream(...))))は、教科書に載るような現実世界の例です。 - 既存のオブジェクトのコードに手を加えず、ロギング、キャッシュ、検証、レート制限などの横断的関心事を追加する場合。
このスキルを使わない場面
- 拡張同士が直交していない場合(ミルクを加える前に砂糖を加える必要があるなど)。デコレーターでは順不同で合成できるため、順序が重要なら隠れた結合が生じています。
- 拡張が 1 つしかなく、2 つ目も見込まれない場合。サブクラス化やフラグの方がシンプルです。
- ラッパーが振る舞いだけでなくインターフェースも変更する場合。その場合は
gof-adapterを使います。
中核となる内容
構造は 2 つの要素から成ります。
- 「コア」オブジェクトとすべてのデコレーターが共有する共通インターフェース(または抽象クラス)。
- 各デコレーターは、同じインターフェースを持つ「ラップされた」インスタンスへの参照を保持し、その呼び出しを転送します。必要に応じて、前後に処理を追加できます。
合成は外側から内側へと読みます。Foam(Milk(Espresso)) は「エスプレッソにミルクを加え、その後フォームを加える」という意味です。クライアントが使うのは最外側のデコレーターです。
「オープン・クローズドの原則」が標準的な動機です。クラスは拡張に対して開かれ、変更に対して閉じられるべきです。デコレーターはサブクラス化ではなく合成によって拡張を実現します。
判断のヒント
- コアクラス内に
if (hasMilk) … if (hasSugar) … if (hasFoam) …と書くことになるなら、デコレーターによって各関心事を独立した型に分けられます。 - 各デコレーターが追加する関心事は1 つにしてください。デコレーターが 2 つのことをするなら、分割してください。
- ラップする順序が、外側から内側への実行順序になります。意図的に決め、その順序をテストしてください。
アンチパターン
- 「デコレーターのごった煮」 — 関心事が重複する小さなデコレーターが多数あり、実際の呼び出しチェーンを把握しにくくなります。
- 契約を変更するデコレーター(ラップ対象のオブジェクトが返さない場面で
nullを返すなど) — 呼び出し元をひそかに破壊します。 - 「デコレーターは余計なものを加えたサブクラスにすぎないから」と、結局は継承を使って振る舞いを追加すること — デコレーターを使う意味がなくなります。
注意点:デコレーターはラップ対象の型ではありません
デコレーターが実装するのは、ラップ対象の具体クラスではなく、そのインターフェースです。そのため、デコレーターはラップ対象の具体型の is / instanceof チェックでは false になります。たとえその型と同じように振る舞うとしても、です。
val loggedRepo: Repository = LoggingRepository(DefaultRepository())
loggedRepo is Repository // true
loggedRepo is DefaultRepository // false — even though it wraps one
loggedRepo is LoggingRepository // true
デコレーターチェーンを「通して」実行時の型チェックを行うと、この問題に直面します。「基になるオブジェクトは X 型か」と尋ねたくなったなら、すでに抽象化が漏れています。チェーンをインターフェース経由で扱うか、構造を見直してください。
関連項目
gof-adapter— 振る舞いだけでなくインターフェースを変更するラッパーを使う場合。gof-proxy— 同じ構造ですが、目的は異なります(プロキシはアクセスを制御し、デコレーターは振る舞いを追加します)。gof-composite— デコレーターとコンポジットは構造的に近く、どちらもラップし、再帰的に呼び出しを転送します。gof-kotlin-idioms— Kotlin のbyキーワード(クラス委譲)を使うと、デコレーターを 1 行で書けます。
参考資料
- Head First Design Patterns(第 2 版)、第 3 章(「オブジェクトを装飾する:デコレーターパターン」) — Starbuzz コーヒーの代表的な例と Java I/O の解説。
- Kotlin Design Patterns and Best Practices(Alexey Soshin)、第 3 章 — デコレーターについて。「デコレーターデザインパターンの注意点」の節では、上記の
isチェックに関する注意点の動機が説明されています。
コメント
Markdown 対応