「デザインパターンは勉強したけど、いつ使えばいいかわからない」——そんな悩みを抱えていませんか?
この記事では、ウイスキーのテイスティングカードを生成するツールを作りながら、Abstract Factory・Flyweight・Prototypeの3つのパターンを実践します。産地ごとに異なるカードフォーマット、大量の香味語彙、プロファイルの派生——それぞれの問題に対して、パターンがどう解決策を提供するかを体験しましょう。
完成したら、次のテイスティング会で「俺が作ったカード生成器で出力したやつ」と自慢できます。
この記事で学べること
| パターン | 解決する問題 | 本記事での役割 |
|---|---|---|
| Abstract Factory | 関連オブジェクトの組み合わせミス | 産地別キットの一括生成 |
| Flyweight | 同じオブジェクトの大量生成 | 香味語彙の共有プール |
| Prototype | 類似オブジェクトの手動作成 | プロファイルのクローン |
対象読者
- デザインパターンの名前は知っているが、使いどころがわからない方
- Perl入学式を卒業し、次のステップに進みたい方
- ウイスキーが好きで、趣味と学習を両立させたい方
技術スタック
- Perl v5.36以降(signatures使用)
- Moo によるオブジェクト指向
- CLI(コマンドライン)環境
第1章: テイスティングカードを作ってみよう
今回の目標
- ウイスキーのテイスティングカードを1枚出力する
- 最もシンプルな実装からスタート
最初の実装
まずは、1つのウイスキー情報をカードとして出力してみましょう。
| |
実行結果
| |
今回のポイント
- ✅ とりあえず動くものができた
- ⚠️ すべてハードコード——拡張性ゼロ
- ⚠️ 産地が増えたらどうする?
第2章: 産地が増えると分岐地獄
前章の振り返り
ハードコードでカードを1枚出力できました。でも、スコッチだけでなくアイリッシュ、ジャパニーズにも対応したら?
今回の目標
- 複数産地に対応する
- if/elseで分岐を実装
- 問題を体験する
動く:Mooでクラス化
| |
破綻:if/elseが増殖する
産地が3つになっただけで、すでにif/elseが3段階。さらに…
- ペアリング情報も産地ごとに変えたい → 別のif/else追加
- フレーバープロファイルも産地ごとに → また別のif/else追加
- カードのフォーマットも産地ごとに → さらにif/else追加
| |
問題点を整理すると:
- 組み合わせミスのリスク: ScotchCardにIrishPairingを紐付けるミス
- if/elseの肥大化: 産地×製品種類の組み合わせ爆発
- 変更が困難: 新産地追加時に複数箇所を修正
今回のポイント
- ⚠️ 関連するオブジェクト群をif/elseで生成すると組み合わせミスが起きる
- ⚠️ 産地という「ファミリ」ごとに一貫したオブジェクト群が必要
- → 産地別の「キット」を一括生成すればよいのでは?
第3章: 産地別キットを工場で生産

前章の振り返り
if/elseで産地ごとにオブジェクトを生成していましたが、組み合わせミスのリスクがありました。
今回の目標
- 産地別に関連オブジェクトを一括生成
- Abstract Factoryパターンの導入
完成:Abstract Factoryパターン
| |
実行結果
| |
Abstract Factoryの効果
classDiagram
class `TastingKitFactory::Role` {
<<interface>>
+create_profile()
+create_card(name)
+create_pairing()
}
class ScotchFactory {
+create_profile() ScotchProfile
+create_card(name) ScotchCard
+create_pairing() ScotchPairing
}
class IrishFactory {
+create_profile() IrishProfile
+create_card(name) IrishCard
+create_pairing() IrishPairing
}
`TastingKitFactory::Role` <|.. ScotchFactory
`TastingKitFactory::Role` <|.. IrishFactory
note for ScotchFactory "スコッチ製品群を一括生成\n組み合わせミスが起きない"
今回のポイント
- ✅ Abstract Factoryパターン = 関連製品群の一括生成
- ✅ ScotchFactoryは必ずスコッチ製品を返す——組み合わせミスが構造的に防止
- ✅ 新産地追加は新Factoryクラスを追加するだけ(既存コード無修正)
- ✅ クライアントコードはFactoryのインターフェースにだけ依存
第4章: 同じ香味が何度も作られる
前章の振り返り
Abstract Factoryで産地別キットを一括生成できるようになりました。でも、大量のカードを生成すると…
今回の目標
- 大量カード生成時のメモリ問題を認識
- 香味オブジェクトの重複を発見
破綻:香味オブジェクトが爆発
テイスティングイベントで100枚のカードを生成する場合を考えてみましょう。
| |
実行結果
| |
問題分析
| 項目 | 値 |
|---|---|
| カード数 | 100枚 |
| カードあたりの香味数 | 5個 |
| 生成されたオブジェクト | 500個 |
| 実際に必要な種類 | 5種類 |
| 無駄なオブジェクト | 495個(99%が無駄!) |
今回のポイント
- ⚠️ 同じ「smoky」なのに100個の別オブジェクト
- ⚠️ メモリ使用量がカード数に比例して増大
- → 同じ香味は1つのオブジェクトを共有すればよいのでは?
第5章: 香味語彙を共有しよう

前章の振り返り
同じ「smoky」なのに毎回新しいオブジェクトを作っていました。これを解決します。
今回の目標
- 香味オブジェクトを共有する
- Flyweightパターンの導入
完成:Flyweightパターン
| |
実行結果
| |
Flyweightの効果
| 項目 | Before | After |
|---|---|---|
| FlavorDescriptorオブジェクト数 | 500個 | 5個 |
| メモリ削減率 | - | 99%削減 |
classDiagram
class FlavorPool {
-_pool: Hash
+get_flavor(name, category): FlavorDescriptor
+pool_size(): Int
}
class FlavorDescriptor {
+name: Str
+category: Str
+describe(intensity): Str
}
class TastingCard {
+name: Str
+flavor_pool: FlavorPool
+flavor_refs: Array
+add_flavor(name, category, intensity)
}
FlavorPool "1" --> "*" FlavorDescriptor : caches
TastingCard "*" --> "1" FlavorPool : uses
TastingCard "*" ..> "*" FlavorDescriptor : references
note for FlavorPool "同じ香味は同じオブジェクトを返す"
note for TastingCard "intensity(強度)は外部状態として保持"
今回のポイント
- ✅ Flyweightパターン = オブジェクトの共有
- ✅ 内部状態(name, category)は共有、外部状態(intensity)はカードごと
- ✅ 100枚のカードでも5種類分のメモリで済む
- ✅ FlavorPoolがFlyweight Factoryとして機能
第6章: Islayベースから派生を作りたい
前章の振り返り
Flyweightで香味オブジェクトを共有できるようになりました。でも、プロファイルを細かくカスタマイズしたい場合は…
今回の目標
- ベースプロファイルから派生を作る問題を認識
- new()の限界を体験
破綻:派生プロファイルを手作業で作成
Islayの基本プロファイルを元に、Ardbeg風やLaphroaig風を作りたい場合:
| |
問題分析
| |
問題点:
- DRY原則違反: 同じ値を何度も書いている
- 変更に弱い: Islay全体のベースを変えたら全派生を修正
- タイプミスリスク: コピペで値を書き間違える可能性
今回のポイント
- ⚠️ ベースから少しだけ変えた派生を作りたいのに、全部指定し直し
- ⚠️ 属性が増えるほど、派生作成のコストが増大
- → ベースをコピーして、一部だけ変更できればよいのでは?
第7章: プロファイルをクローンして派生

前章の振り返り
ベースプロファイルから派生を作るのに、全パラメータを再指定していました。
今回の目標
- ベースをクローンして一部変更
- Prototypeパターンの導入
完成:Prototypeパターン
| |
実行結果
| |
Prototypeの効果
classDiagram
class Cloneable {
<<role>>
+clone(): Object
+clone_with(%overrides): Object
}
class WhiskyProfile {
+region: Str
+sub_region: Str
+distillery: Str
+peat_level: Int
+smoke_level: Int
+sweetness: Int
+clone(): WhiskyProfile
+clone_with(%overrides): WhiskyProfile
}
Cloneable <|.. WhiskyProfile
WhiskyProfile <.. WhiskyProfile : clones
note for WhiskyProfile "clone()でベースから派生を簡単に作成\nclone_with()で一部だけ変更"
今回のポイント
- ✅ Prototypeパターン = クローンによるオブジェクト生成
- ✅
clone()で完全コピー、clone_with()で一部変更 - ✅ Storable::dclone()で深いコピーを実現
- ✅ 派生の派生(Ardbeg → Bowmore)も簡単
第8章: 3つのパターンで完成!
全体の振り返り
ここまでの成長を振り返りましょう。
| 章 | 問題 | 解決策 | パターン |
|---|---|---|---|
| 2-3 | 産地別オブジェクトの組み合わせミス | ファミリ一括生成 | Abstract Factory |
| 4-5 | 香味オブジェクトの大量生成 | 共有プール | Flyweight |
| 6-7 | 派生プロファイルの手動作成 | クローン | Prototype |
3パターンの連携
graph TD
subgraph "Abstract Factory"
AF[TastingKitFactory]
SF[ScotchFactory]
IF[IrishFactory]
AF --> SF
AF --> IF
end
subgraph "Flyweight"
FP[FlavorPool]
F1[smoky]
F2[peaty]
F3[vanilla]
FP --> F1
FP --> F2
FP --> F3
end
subgraph "Prototype"
BASE[Islay Base Profile]
ARD[Ardbeg Variant]
LAP[Laphroaig Variant]
BASE -.->|clone| ARD
BASE -.->|clone| LAP
end
SF --> BASE
SF --> FP
style AF fill:#e1f5fe
style FP fill:#f3e5f5
style BASE fill:#e8f5e9
最終版コードのハイライト
| |
実行結果
| |
まとめ
学んだ3つのパターン
| パターン | 一言で | 使うタイミング |
|---|---|---|
| Abstract Factory | 関連製品群の一括生成 | 組み合わせミスを構造的に防ぎたい |
| Flyweight | オブジェクト共有でメモリ節約 | 同じ内容のオブジェクトが大量にある |
| Prototype | クローンで派生作成 | ベースから少し変えた派生が欲しい |
3パターンが協力する場面
この3パターンは「オブジェクト生成の効率化」という共通テーマで連携しやすい組み合わせです:
- Abstract Factory → 産地という「ファミリ」で一貫したオブジェクト群を生成
- Flyweight → 香味語彙という「共有すべきデータ」を効率管理
- Prototype → 蒸溜所バリエーションという「派生」を簡単に作成
次のステップ
- 「関連するオブジェクトを一緒に作りたい」と思ったら → Abstract Factory
- 「同じデータのオブジェクトが大量にある」と思ったら → Flyweight
- 「ベースから少しだけ変えた派生を作りたい」と思ったら → Prototype
パターンの名前を覚えるより、**「この問題にはこの解決策」**という感覚を身につけることが大切です。
動作環境・コード
本記事のコードは以下の環境で動作確認しています:
- Perl v5.36以降(
signatures機能を使用) - cpanm経由で
MooとStorableをインストール
| |
Sláinte! 🥃
