この記事は?
オライリー社から出版されている「HeadFirst デザインパターン」を読んだ内容を
理解の整理のためTypescriptで実装してみる内容です。(定期で載せれる様がんばる)
この記事の進め方
デザインパターンの概要
Decoratorパターンについて。詳細はWiki様。
実装
設定背景
カフェのオーダーシステムを作成する。
色々なトッピングで自由なカスタマイズが可能。
改修前コード
index.ts
// 飲料のインターフェース
interface Beverage {
cost(): number;
}
// 紅茶クラス
class Tea implements Beverage {
cost() {
return 500; // 価格は500円
}
}
// ミルククラス
class Milk implements Beverage {
cost() {
return 200; // 価格は 200円
}
}
// シナモンクラス
class Sinamon implements Beverage {
cost() {
return 50; // 価格は 50円
}
}
期待結果
// シナモン+ミルク+ティーを注文して価格750円を表示したい
console.log("total: ", total)
BADケース1
ハードコードしちゃうパターン(参考書のBADサンプル)
index.ts
interface Beverage {
cost(): number;
}
class Milk implements Beverage {
cost() {
return 200;
}
}
class Sinamon implements Beverage {
cost() {
return 50;
}
}
class Tea implements Beverage {
total: number = 500; // デフォルト価格を内部で保持
// ミルク追加時に金額を追加
addMilk() {
this.total += new Milk().cost();
return this;
}
// シナモン追加時に金額を追加
addSinamon() {
this.total += new Sinamon().cost();
return this;
}
// 小計を算出
cost() {
return this.total;
}
}
結果
// シナモン+ミルク+ティーを注文して価格750円を表示する
const total = new Tea().addMilk().addSinamon().cost();
console.log("total: ", total) // => total: 750
- 何が悪い?
- 正直みるからに悪そう...。
- 商品が増えた際に修正箇所、追加実装が大量にでそうっすね。
- 変更耐性が低そう。テストが欲しくなりますね。
BADケース2
上の例は流石に極端だったので、もう少しありそうなケースを考えてみたいですね。
Observerパターンを使用しても作れそうな気がしたので、考えてみました。
index.ts
// トッピングinterfaceを定義
interface Topping {
cost: number;
}
// 飲料の抽象クラスを定義
abstract class Beverage {
abstract price: number; // 元々の金額
toppings: Topping[] = []; // 特徴は自身に追加された toppings を管理するとこ。Observerパターンに影響受けてますね...
// toppings追加メソッド。ここら辺もObserverに近い
addTopping(t: Topping): void {
this.toppings = [...this.toppings, t];
}
// トッピングの合計金額から小計を算出する
cost(): number {
let sum = 0;
this.toppings.forEach((t) => {
sum += t.cost;
});
return sum + this.price;
}
}
class Tea extends Beverage {
price = 500;
}
class Milk implements Topping {
cost = 200;
}
class Sinamon implements Topping {
cost = 50;
}
結果
// 特徴は飲料クラスにトッピングを追加していくイメージ
const t = new Tea();
t.addTopping(new Milk());
t.addTopping(new Sinamon());
const total = t.cost();
console.log("total: ", total) // => total: 750
- 何が悪い?
- ぱっと見いい感じに見える、、、が
- しかし、飲料とトッピングの概念が存在し、依存関係が存在する
- その為、飲料同士を掛け合わせたり、トッピング同士の掛け合わせはできない...
- 例: ミルクを飲料として扱いたい場合、飲料クラスの
Milk
と、トッピングクラスのMilk
が必要になる
- 例: ミルクを飲料として扱いたい場合、飲料クラスの
デザインパターンを使用した場合
Decoratorパターンを使用した場合
index.ts
interface Beverage {
cost(): number;
}
// 飲料の抽象クラスを定義
abstract class AbsBeverage implements Beverage {
abstract price: number; // 飲料の値段はサブクラスで定義する
baseBeverage?: Beverage; // デコレート対象の具象クラス
// トッピングする場合は、トッピング元の飲料を初期化時に渡す
constructor(b?: Beverage) {
this.baseBeverage = b;
}
cost() {
const preCost = this.baseBeverage ? this.baseBeverage.cost() : 0; // beverageがundefinedの場合、元々のコストは0
return preCost + this.price;
}
}
class Tea extends AbsBeverage {
price = 500;
}
class Milk extends AbsBeverage {
price = 200;
}
class Sinamon extends AbsBeverage {
price = 50;
}
結果
// どの掛け合わせも可能。オーダー順も関係ない。(トッピング -> 飲み物順でのオーダーも可能)
let order = new Milk();
order = new Sinamon(order);
order = new Coffee(order);
order = new Tea(order);
console.log("total: ", order.cost()) // => total: 750
- 何が良い?
- 拡張が簡単
- 修正の場合も範囲が限定できる
- 実装の際もトッピングや飲料やら細かいことを考慮する必要がなくなる
まとめ
Observerパターンと比べてちょっと使う上で思考がいるけど、覚えておくと便利ですし、実際多く使われているテクニックだと感じる。
業務で使えると少しデキるエンジニア感だせそうです。
Pythonなどでもデコレータ機能があるけど、そこが同様の技術で行われているのかは、よく分かってないので時間があれば調査してみたい。(<- 限りなくやらないやつ)