メインコンテンツまでスキップ

第12章:Decorator設計の落とし穴回避🕳️🛑🎀

(=「外付け機能」を気持ちよく使うための安全運転🚗💨)


0. この章でできるようになること🎯✨

  • Decoratorをどこまで使っていいか判断できる🧠✅
  • 「やりすぎDecorator地獄😇」を回避できる🚧
  • 悪い例を見て「直し方」がわかる🔧✨
  • Decoratorを重ねても、読みやすさ・テストしやすさを保てる🧪💕

1. まず混乱ポイントを1分で整理🧩⌛

ここで扱うDecoratorは デザインパターン(ラッパーで包むやつ) 🎀✨ TypeScriptの「@で付けるデコレーター構文」もあるけど、別物として分けて考えると事故らないよ〜🙆‍♀️

  • TSのデコレーター構文は公式ドキュメントにもある(クラスやメソッド等に付けられる)📘✨ (TypeScript)
  • そして experimentalDecorators は「TC39標準化より前のデコレーター」だよ、って明言されてる⚠️ (TypeScript)

この章は主に、ラッパークラス(or ラッパー関数)で包むDecoratorパターンの設計話だよ🎀🧠


2. Decoratorパターンの“正しい気持ち”🌿🎀

Decoratorはひとことで言うと…

本体のコードは触らずに、周辺機能(ログ・計測・リトライ等)を外付けするやり方✨

ポイントはこれ👇

  • 本体(ビジネスの中心)は静かに純粋に保つ🧘‍♀️
  • Decoratorは横断的関心ごと(いろんな場所に同じように付けたくなるやつ)を担当する🧩

3. まずは「落とし穴あるある」7連発🕳️😱

ここからが本題!「やりがちな失敗」を先に知って、回避しよ〜💪✨

3-1. ビジネスルールをDecoratorに入れちゃう🍛🎀❌

ダメな匂い:Decoratorの中に「割引」「判定」「ルール」が入ってくる😇

  • Decoratorは本来、ログや計測みたいな“外側”担当
  • ルールが入ると「どこが本体かわからん」事故が起きる💥

3-2. Decoratorの順番で挙動が変わってカオス🧁🎀🎀🎀

ログ→リトライ→計測…みたいに重ねると、順番でログ回数や計測範囲が変わるよね😵‍💫

  • 「正しい順」が暗黙だと、あとから壊れる😇

3-3. “便利だから”で何でもDecorator化して増殖🌳💣

「キャッシュも認可もバリデーションも例外変換も…全部Decoratorで!」 → そのうち 読めないミルフィーユになる🍰😇

3-4. Decoratorが例外を握りつぶす🫥❌

  • 本体の例外をキャッチしてログだけ出して終了…とか
  • 呼び出し側が期待するエラーが消えて、バグが潜む🐛

3-5. Decoratorが“戻り値”や“意味”を変えちゃう🔁😵

Decoratorは基本「飾り」🎀

  • 戻り値を勝手に丸める
  • nullを返す
  • 成功扱いにする みたいに意味を変えると、もう別物🧨

3-6. DIが崩れて「new地獄」が復活する🧟‍♀️

Decoratorの中で勝手に依存をnewすると、差し替え不能になる🙅‍♀️ (第5章の「外から渡す」思想が大事🚚💨)

3-7. “横断”のはずが、特定ドメイン専用Decoratorになる🎯❌

  • 「VIPだけ特別処理」みたいなドメイン条件がDecoratorに混ざると → 横断じゃないよね…?ってなる😇

4. 使っていいDecoratorか?3秒チェックリスト✅⏱️

DecoratorにしてOKか迷ったら、これを順番に見てね📝✨

✅ Decorator向き(だいたいOK)

  • いろんなサービスに同じ形で付けたい(横断)🧩
  • 本体の結果(成功/失敗)を変えず、副作用だけ足す🎀
  • 外しても本体の意味が変わらない(ログ外しても業務は同じ)🔁
  • テストで「付けても外しても」検証しやすい🧪

例:ログ📝、計測⏱️、メトリクス📈、リトライ🔁、タイムアウト⏳、サーキットブレーカー⚡

❌ Decorator不向き(本体に戻して!)

  • 割引や料金計算など業務ルールそのもの💰
  • ルールが頻繁に変わる&仕様の中心にいる📚
  • 「VIPなら〜」「会員ランクなら〜」みたいな条件分岐が主役👑
  • Decoratorが戻り値の意味を変える(成功扱い等)🧨

5. 例題:悪いDecoratorを直して“いい形”にする🔧✨🎀

ここ、いちばん大事! 「悪い例 → 改善」を体験しよう💪😊

5-1. お題:注文合計を返すサービス🛒

❌ 悪い例:割引ルールがDecoratorに入ってる

// 本体
export interface PriceService {
getTotal(userId: string, basePrice: number): Promise<number>;
}

export class BasicPriceService implements PriceService {
async getTotal(userId: string, basePrice: number): Promise<number> {
return basePrice;
}
}

// 悪いDecorator:横断じゃなく「業務ルール」が主役になってる😇
export class VipDiscountDecorator implements PriceService {
constructor(private inner: PriceService) {}

async getTotal(userId: string, basePrice: number): Promise<number> {
const total = await this.inner.getTotal(userId, basePrice);

// 💥 ドメインルールが混入(VIPなら10%オフ)
if (userId.startsWith("VIP_")) {
return Math.floor(total * 0.9);
}
return total;
}
}

**何がダメ?**😵‍💫

  • 「VIP判定」も「割引率」も超・業務の中心

  • これをDecoratorに入れると、

    • 割引の仕様変更が起きるたびにDecoratorが増える🌳
    • どこで価格が変わってるか追跡が難しい🕵️‍♀️

5-2. ✅ 改善方針:割引はStrategyへ戻す🧠🔁

  • 本体は「合計を出す流れ」を持つ
  • 割引は差し替え点(Strategy)にする
  • Decoratorはログや計測など“外側”だけにする🎀

✅ 改善コード(PriceRule = Strategy)

export type DiscountRule = (userId: string, price: number) => number;

export const noDiscount: DiscountRule = (_userId, price) => price;

export const vip10PercentOff: DiscountRule = (userId, price) => {
if (userId.startsWith("VIP_")) return Math.floor(price * 0.9);
return price;
};

export interface PriceService {
getTotal(userId: string, basePrice: number): Promise<number>;
}

export class PriceServiceWithRule implements PriceService {
constructor(
private rule: DiscountRule
) {}

async getTotal(userId: string, basePrice: number): Promise<number> {
const afterDiscount = this.rule(userId, basePrice);
return afterDiscount;
}
}

✅ Decoratorは“外側”に戻す(ログだけ)

export class LoggingPriceService implements PriceService {
constructor(private inner: PriceService) {}

async getTotal(userId: string, basePrice: number): Promise<number> {
console.log("[Price] start", { userId, basePrice });
try {
const total = await this.inner.getTotal(userId, basePrice);
console.log("[Price] success", { total });
return total;
} catch (e) {
console.log("[Price] error", { message: String(e) });
throw e; // ✅ 握りつぶさない
}
}
}

**これで何が嬉しい?**🥰

  • 本体はスッキリ✨
  • Decoratorは“横断”だけを担当できる🎀

6. 「Decoratorの順番問題」も潰しておこう🧁🛑

重ねるときのコツはこれ👇

6-1. ルール①:順番が意味を持つDecoratorは“まとめて生成”する🏗️

「適当にnewで積む」んじゃなくて、組み立て関数を作ると安全😊

export function buildPriceService(): PriceService {
const core = new PriceServiceWithRule(vip10PercentOff);

// ✅ ここで順番を固定する(チームのルール)
const withLog = new LoggingPriceService(core);

return withLog;
}

6-2. ルール②:Decoratorが増えたら“名前で説明”する📛✨

  • buildPriceService()
  • buildPriceServiceForProduction()
  • buildPriceServiceForTest() みたいに「何が付いてるか」を名前で表現すると迷子になりにくいよ🧭😊

7. 演習✍️💪(章のメイン課題)

お題:このDecorator、どこが危ない?直して!😈➡️😇

次のコードは「例外を握りつぶして成功扱いにする」最悪パターン🫠 やってはいけない理由を3つ書いて、修正版を作ってね📝✨

export interface ReportService {
generate(userId: string): Promise<string>;
}

export class BasicReportService implements ReportService {
async generate(userId: string): Promise<string> {
if (userId === "bad") throw new Error("DB failure");
return "report";
}
}

// ❌ 最悪Decorator
export class SwallowErrorDecorator implements ReportService {
constructor(private inner: ReportService) {}

async generate(userId: string): Promise<string> {
try {
return await this.inner.generate(userId);
} catch {
return "report"; // 💥 成功扱いにして返す
}
}
}

✅ 方向性ヒント🌟

  • 例外は基本、握りつぶさず投げ直す
  • “どうしても丸めたい”なら「戻り値にエラー情報を含める設計」を本体側で検討(DecoratorでこっそりはNG)

8. AI(Copilot/Codex等)に頼むときの安全プロンプト🤖✨

DecoratorはAIが“盛りがち”だから、お願いの仕方が超大事だよ〜😆💦

✅ 良いお願い例💡

  • 「このDecoratorがビジネスルールを持っていないかレビューして。横断的関心ごとだけにして」
  • 「例外を握りつぶしていないか確認して。握りつぶしてたら投げ直す形に直して」
  • 「Decoratorを重ねる順番が挙動に影響するか説明して。影響するならbuild関数で固定して」

✅ AI出力のチェック観点👀✅

  • 例外は投げ直してる?
  • 戻り値の意味を変えてない?
  • newで依存を作ってない?
  • ドメイン条件(VIP、会員ランク等)が紛れ込んでない?

9. 章末ミニテスト🎓📝(サクッと!)

Q1. Decoratorが向いているのはどっち?(1つ選ぶ) A. 会員ランクで割引率を変える B. 処理時間を計測してログに出す

Q2. Decoratorがやっちゃダメなことを2つ書いてね✍️ (例:例外を握りつぶす、戻り値の意味を変える、など)

Q3. Decoratorの順番問題を減らす方法は?(1つ) A. その場で適当にnewして重ねる B. 組み立て関数で順番を固定する


10. まとめ📌✨

  • Decoratorは「外付け(横断)」が主役🎀
  • ビジネスルールが混ざったら黄色信号🚥😇
  • 順番が意味を持つなら、組み立て場所で固定🏗️✅
  • 例外・戻り値の意味は絶対に壊さない🛑

次の第13章は、外部APIの形を合わせる Adapter 🎁🔌 に進むよ〜!

(参考:TSのデコレーター構文やexperimentalDecoratorsの位置づけは公式ドキュメントも確認できるよ📘✨ (TypeScript)) (なお本日時点のTypeScriptは 5.9.3 が最新として公開されているよ📦✨ (GitHub))