第20章: パターン間の相互作用 — 6言語統合ガイド¶
1. はじめに¶
個々のデザインパターンを学んだ後、次のステップはパターンを組み合わせてより高度な設計を実現することです。Composite + Decorator、Command + Observer、Strategy + Factory — これらの組み合わせにより、単一のパターンでは達成できない柔軟性と表現力を得られます。
2. 共通の本質¶
パターン組み合わせの原則¶
| 組み合わせ | 効果 |
|---|---|
| Composite + Decorator | 階層構造への動的な機能追加 |
| Command + Observer | 操作の実行と監視・通知の統合 |
| Strategy + Factory | 条件に基づく戦略の自動選択 |
すべての組み合わせは関心の分離と合成可能性を実現します。
3. 言語別実装比較¶
3.1 Composite + Decorator¶
階層的なデータ構造(Composite)に、ロギングやスタイリングなどの横断的関心事(Decorator)を追加するパターンです。
Clojure: マルチメソッド + 高階関数
;; Composite: 再帰的な図形構造
(defmulti area :type)
(defmethod area :circle [{:keys [radius]}]
(* Math/PI radius radius))
(defmethod area :composite [{:keys [children]}]
(reduce + (map area children)))
;; Decorator: ロギングを追加
(defn with-logging [shape-fn]
(fn [shape]
(let [result (shape-fn shape)]
(println (str "Area of " (:type shape) ": " result))
result)))
;; 組み合わせ
(def logged-area (with-logging area))
(logged-area composite-shape)
Scala: case class + 拡張メソッド
// Composite
sealed trait Shape:
def area: Double
case class Circle(radius: Double) extends Shape:
def area: Double = math.Pi * radius * radius
case class CompositeShape(children: Vector[Shape]) extends Shape:
def area: Double = children.map(_.area).sum
// Decorator: スタイル付き図形
case class StyledShape(shape: Shape, style: Style) extends Shape:
def area: Double = shape.area
// 組み合わせ
val styled = StyledShape(
CompositeShape(Vector(Circle(5), Circle(3))),
Style(color = "red", outline = true)
)
Haskell: ADT + 関数合成
-- Composite + Decorator を ADT で統合
data Shape
= Circle Double
| Square Double
| Composite [Shape]
| Styled Style Shape -- Decorator を型として表現
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Square s) = s * s
area (Composite ss) = sum (map area ss)
area (Styled _ s) = area s -- スタイルは面積に影響しない
-- 関数合成でデコレータを適用
withLogging :: (Shape -> Double) -> Shape -> IO Double
withLogging f shape = do
let result = f shape
putStrLn $ "Area: " ++ show result
return result
Rust: enum + trait
pub enum Shape {
Circle { radius: f64 },
Composite { shapes: Vec<Shape> },
Styled { shape: Box<Shape>, style: Style },
}
impl Shape {
pub fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Composite { shapes } => shapes.iter().map(|s| s.area()).sum(),
Shape::Styled { shape, .. } => shape.area(),
}
}
}
3.2 Command + Observer¶
コマンドの実行を Observer が監視し、Undo/Redo の履歴管理と変更通知を統合するパターンです。
Clojure: atom + watch
;; Command 履歴の管理
(def history (atom {:undo-stack [] :redo-stack [] :document ""}))
;; Observer: 状態変更を監視
(add-watch history :logger
(fn [_ _ old-state new-state]
(println "Document changed:"
(:document old-state) " -> " (:document new-state))))
;; Command 実行 + 自動通知
(defn execute-command [cmd]
(swap! history
(fn [state]
(-> state
(update :undo-stack conj cmd)
(assoc :redo-stack [])
(update :document (execute cmd))))))
F#: イベント + パイプライン
type CommandEvent =
| Executed of command: TextCommand * document: string
| Undone of command: TextCommand * document: string
type CommandProcessor = {
Document: string
History: TextCommand list
Listeners: (CommandEvent -> unit) list
}
let executeCommand (cmd: TextCommand) (processor: CommandProcessor) =
let newDoc = TextCommand.execute cmd processor.Document
let event = Executed(cmd, newDoc)
processor.Listeners |> List.iter (fun listener -> listener event)
{ processor with
Document = newDoc
History = cmd :: processor.History }
3.3 Strategy + Factory¶
条件に基づいて適切な Strategy を自動選択する Factory パターンです。
全言語のStrategy + Factory比較
;; Clojure: マップベースのファクトリ
(def strategy-factory
{:regular regular-pricing
:member member-pricing
:vip (discount-pricing 0.3)
:bulk (bulk-pricing 10000 0.15)})
(defn get-strategy [customer-type]
(get strategy-factory customer-type regular-pricing))
// Scala: パターンマッチ + 関数リテラル
def createStrategy(customerType: CustomerType): PricingFn =
customerType match
case Regular => identity
case Member => _ * 0.9
case VIP => _ * 0.7
// Factory でコンテキストに基づき自動選択
def autoPricing(customer: Customer, amount: Double): Double =
createStrategy(customer.customerType)(amount)
-- Haskell: 関数の直接返却
createStrategy :: CustomerType -> PricingStrategy
createStrategy Regular = regularPricing
createStrategy Member = memberPricing
createStrategy VIP = discountPricing 0.3
// Rust: match + クロージャ
pub fn create_strategy(customer_type: &CustomerType) -> Box<dyn Fn(f64) -> f64> {
match customer_type {
CustomerType::Regular => Box::new(|amount| amount),
CustomerType::Member => Box::new(|amount| amount * 0.9),
CustomerType::VIP => Box::new(|amount| amount * 0.7),
}
}
4. 比較分析¶
4.1 パターン合成の容易さ¶
| 言語 | 合成の仕組み | 特徴 |
|---|---|---|
| Clojure | 関数合成 + マルチメソッド | 最も動的で柔軟 |
| Scala | trait 継承 + パターンマッチ | OOP と FP のハイブリッド |
| Elixir | パイプライン + プロトコル | 可読性の高い合成 |
| F# | パイプ演算子 + 判別共用体 | 型安全な合成 |
| Haskell | 関数合成 (.) + ADT |
最も数学的な合成 |
| Rust | trait + enum + クロージャ | 所有権を考慮した合成 |
4.2 組み合わせの安全性¶
パターンを組み合わせる際、静的型付け言語はコンパイル時に整合性を検証できます:
- Haskell, F#: ADT / 判別共用体の網羅性チェック
- Scala, Rust: sealed trait / enum の網羅性チェック
- Clojure, Elixir: テストで整合性を確認
5. 実践的な選択指針¶
| 組み合わせ | 推奨言語 | 理由 |
|---|---|---|
| Composite + Decorator | Haskell | ADT で両パターンを型として統合 |
| Command + Observer | Clojure | atom + watch で自然に統合 |
| Strategy + Factory | Scala | パターンマッチで簡潔に選択 |
| 全般的な合成 | F# | パイプ演算子で可読性の高い合成 |
6. まとめ¶
パターン間の相互作用は、関数型プログラミングの合成可能性を最大限に活かす実践です:
- 関数合成がパターン合成: 高階関数でパターンを組み合わせ
- ADT が構造を統合: Composite と Decorator を同じ型階層で表現
- イベント駆動が連携を実現: Command の実行が Observer に自動通知