第20章 パターンの相互作用 - 複合パターンの活用¶
概要¶
この章では、複数の関数型デザインパターンを組み合わせて、複雑な問題を解決する方法を学びます。ECサイトのショッピングカートシステムを例に、Strategy、Decorator、Command、Composite、Visitor パターンの統合を示します。
学習目標¶
- 複数パターンの効果的な組み合わせ方
- パターン間の協調動作の設計
- 実用的なシステムでのパターン適用
- 関数型での柔軟なアーキテクチャ構築
商品と価格設定¶
商品の定義¶
-- | 商品
data Product = Product
{ productId :: String
, productName :: String
, productBasePrice :: Double
, productCategory :: String
} deriving (Show, Eq)
-- | 商品の作成
makeProduct :: String -> String -> Double -> String -> Product
makeProduct = Product
価格設定戦略 (Strategy パターン)¶
-- | 価格設定戦略
newtype PricingStrategy = PricingStrategy
{ applyPricing :: Product -> Int -> Double
}
-- | 通常価格
regularPricing :: PricingStrategy
regularPricing = PricingStrategy $ \prod qty ->
productBasePrice prod * fromIntegral qty
-- | 割引価格
discountPricing :: Double -> PricingStrategy
discountPricing percent = PricingStrategy $ \prod qty ->
productBasePrice prod * fromIntegral qty * (1 - percent / 100)
-- | まとめ買い価格
bulkPricing :: Int -> Double -> PricingStrategy
bulkPricing threshold percent = PricingStrategy $ \prod qty ->
let basePrice = productBasePrice prod * fromIntegral qty
in if qty >= threshold
then basePrice * (1 - percent / 100)
else basePrice
割引デコレータ (Decorator パターン)¶
-- | 割引デコレータ
type DiscountDecorator = Double -> Double
-- | パーセント割引
percentageDiscount :: Double -> DiscountDecorator
percentageDiscount percent price = price * (1 - percent / 100)
-- | 固定額割引
fixedDiscount :: Double -> DiscountDecorator
fixedDiscount amount price = max 0 (price - amount)
-- | 季節割引
seasonalDiscount :: Double -> DiscountDecorator
seasonalDiscount multiplier price = price * multiplier
-- | 複数割引の適用
applyDiscounts :: [DiscountDecorator] -> Double -> Double
applyDiscounts decorators price = foldl' (\p d -> d p) price decorators
注文コマンド (Command パターン)¶
-- | 注文コマンド型クラス
class OrderCommand cmd where
execute :: cmd -> ShoppingCart -> ShoppingCart
-- | 商品追加コマンド
data AddItem = AddItem Product Int
instance OrderCommand AddItem where
execute (AddItem prod qty) cart =
let item = CartItem prod qty (productBasePrice prod)
newItems = item : scItems cart
in cart { scItems = newItems }
-- | 商品削除コマンド
data RemoveItem = RemoveItem String
instance OrderCommand RemoveItem where
execute (RemoveItem prodId) cart =
let newItems = filter (\item -> productId (ciProduct item) /= prodId) (scItems cart)
in cart { scItems = newItems }
-- | 割引適用コマンド
data ApplyDiscount = ApplyDiscount DiscountDecorator
instance OrderCommand ApplyDiscount where
execute (ApplyDiscount discount) cart =
cart { scDiscount = Just discount }
ショッピングカート (Composite パターン)¶
-- | カートアイテム
data CartItem = CartItem
{ ciProduct :: Product
, ciQuantity :: Int
, ciUnitPrice :: Double
} deriving (Show, Eq)
-- | ショッピングカート
data ShoppingCart = ShoppingCart
{ scItems :: [CartItem]
, scDiscount :: Maybe DiscountDecorator
}
-- | 空のカート
emptyCart :: ShoppingCart
emptyCart = ShoppingCart [] Nothing
-- | カート合計
cartTotal :: ShoppingCart -> Double
cartTotal cart =
let subtotal = sum [ciUnitPrice item * fromIntegral (ciQuantity item)
| item <- scItems cart]
in case scDiscount cart of
Nothing -> subtotal
Just discount -> discount subtotal
注文処理パイプライン¶
-- | 注文処理結果
data OrderResult
= OrderSuccess ShoppingCart
| OrderFailure String
-- | パイプラインステップ
type OrderPipeline = ShoppingCart -> OrderResult
-- | 検証ステップ
validationStep :: OrderPipeline
validationStep cart =
if null (scItems cart)
then OrderFailure "Cart is empty"
else OrderSuccess cart
-- | 割引ステップ
discountStep :: Double -> OrderPipeline
discountStep percent cart =
OrderSuccess cart { scDiscount = Just (percentageDiscount percent) }
-- | パイプライン実行
runPipeline :: [OrderPipeline] -> ShoppingCart -> OrderResult
runPipeline [] cart = OrderSuccess cart
runPipeline (step:steps) cart =
case step cart of
OrderFailure msg -> OrderFailure msg
OrderSuccess cart' -> runPipeline steps cart'
カート分析ビジター (Visitor パターン)¶
-- | カートビジター
newtype CartVisitor a = CartVisitor
{ runVisitor :: ShoppingCart -> a
}
-- | アイテム数カウント
itemCountVisitor :: CartVisitor Int
itemCountVisitor = CartVisitor $ \cart ->
sum [ciQuantity item | item <- scItems cart]
-- | 合計金額
totalValueVisitor :: CartVisitor Double
totalValueVisitor = CartVisitor cartTotal
-- | 平均単価
averageItemPriceVisitor :: CartVisitor Double
averageItemPriceVisitor = CartVisitor $ \cart ->
let items = scItems cart
count = length items
in if count == 0
then 0
else sum [ciUnitPrice item | item <- items] / fromIntegral count
-- | ビジターの適用
visitCart :: CartVisitor a -> ShoppingCart -> a
visitCart visitor cart = runVisitor visitor cart
使用例¶
完全な注文フロー¶
-- 商品の定義
laptop = makeProduct "P001" "Laptop" 1000.0 "Electronics"
mouse = makeProduct "P002" "Mouse" 50.0 "Electronics"
-- コマンドを使ってカートを構築
cart1 = executeCommand (AddItem laptop 1) emptyCart
cart2 = executeCommand (AddItem mouse 2) cart1
cart3 = executeCommand (ApplyDiscount (percentageDiscount 5)) cart2
-- パイプラインで処理
pipeline = [validationStep]
result = runPipeline pipeline cart3
-- ビジターで分析
case result of
OrderSuccess cart -> do
putStrLn $ "Items: " ++ show (visitCart itemCountVisitor cart)
putStrLn $ "Total: $" ++ show (visitCart totalValueVisitor cart)
OrderFailure msg -> putStrLn $ "Error: " ++ msg
-- 出力:
-- Items: 3
-- Total: $1045.0 (1100 * 0.95)
パターンの協調¶
-- Strategy: 価格戦略を選択
strategy = if isBulkOrder then bulkPricing 10 15 else regularPricing
-- Decorator: 割引を積み重ね
discounts = [percentageDiscount 10, fixedDiscount 50]
finalPrice = applyDiscounts discounts basePrice
-- Command: 操作をデータ化
commands = [AddItem prod1 1, AddItem prod2 2, ApplyDiscount (percentageDiscount 5)]
cart = foldl (\c cmd -> executeCommand cmd c) emptyCart commands
-- Visitor: カートを分析
stats = (visitCart itemCountVisitor cart, visitCart totalValueVisitor cart)
テスト¶
spec :: Spec
spec = do
describe "Pattern Integration" $ do
it "processes complete order with multiple patterns" $ do
let product1 = makeProduct "P001" "Laptop" 1000.0 "Electronics"
product2 = makeProduct "P002" "Mouse" 50.0 "Electronics"
-- コマンドでカートを構築
cart1 = executeCommand (AddItem product1 1) emptyCart
cart2 = executeCommand (AddItem product2 2) cart1
cart3 = executeCommand (ApplyDiscount (percentageDiscount 5)) cart2
-- パイプラインで処理
pipeline' = [validationStep]
case runPipeline pipeline' cart3 of
OrderSuccess cart' -> do
visitCart itemCountVisitor cart' `shouldBe` 3
cartTotal cart' `shouldBe` 1045.0
OrderFailure _ -> expectationFailure "Order should succeed"
パターン組み合わせの原則¶
1. 責任の分離¶
各パターンは単一の責任を持つ: - Strategy: 価格計算方法 - Decorator: 価格修飾 - Command: 操作のカプセル化 - Composite: 構造の管理 - Visitor: 横断的な処理
2. 合成可能性¶
パターンは互いに干渉せず、組み合わせ可能:
-- どのパターンも独立して使用可能
price1 = applyPricing regularPricing product 1
price2 = percentageDiscount 10 price1
cart = executeCommand (AddItem product 1) emptyCart
count = visitCart itemCountVisitor cart
3. 型による安全性¶
型システムが不正な組み合わせを防止:
-- 型クラスで操作を統一
class OrderCommand cmd where
execute :: cmd -> ShoppingCart -> ShoppingCart
-- 新しいコマンドの追加が安全
data NewCommand = ...
instance OrderCommand NewCommand where
execute = ...
まとめ¶
- 複数のパターンを組み合わせることで、複雑な問題を分割統治できる
- 各パターンは独立して進化・テスト可能
- 関数型アプローチにより、パターンの合成が自然になる
- 型システムにより、安全なパターンの組み合わせが保証される