第20章: パターン間の相互作用¶
はじめに¶
関数型設計パターンは単独で使用するよりも、組み合わせることでより強力になります。この章では、複数のパターンを統合して複雑な問題を解決する方法を学びます。
1. Visitor + Iterator パターン¶
木構造の走査と処理¶
defmodule TreeNode do
defstruct [:value, :left, :right]
def leaf(value), do: %__MODULE__{value: value}
def node(value, left, right), do: %__MODULE__{value: value, left: left, right: right}
end
# イテレータ: 走査戦略
defmodule Iterator do
def preorder(nil), do: []
def preorder(%TreeNode{value: v, left: l, right: r}) do
[v | preorder(l) ++ preorder(r)]
end
def inorder(nil), do: []
def inorder(%TreeNode{value: v, left: l, right: r}) do
inorder(l) ++ [v] ++ inorder(r)
end
end
# ビジター: 要素への操作
defmodule Visitor do
def sum(values), do: Enum.sum(values)
def max(values), do: Enum.max(values)
def map(values, func), do: Enum.map(values, func)
end
組み合わせて使用¶
def traverse_and_visit(tree, iterator_fn, visitor_fn) do
tree
|> iterator_fn.()
|> visitor_fn.()
end
# 使用例
tree
|> traverse_and_visit(&Iterator.inorder/1, &Visitor.sum/1)
2. Strategy + Factory パターン¶
動的な戦略生成¶
defmodule DiscountFactory do
def create(:regular), do: ®ular_discount/1
def create(:premium), do: &premium_discount/1
def create(:vip), do: &vip_discount/1
def create({:seasonal, rate}), do: fn price -> price * (1 - rate) end
def create({:combined, strategies}) do
fn price ->
Enum.reduce(strategies, price, fn s, acc -> create(s).(acc) end)
end
end
defp regular_discount(price), do: price
defp premium_discount(price), do: price * 0.9
defp vip_discount(price), do: price * 0.8
end
# 使用例
strategy = DiscountFactory.create({:combined, [:premium, {:seasonal, 0.1}]})
final_price = strategy.(100.0) # 81.0
3. Observer + State パターン¶
状態変化の通知¶
defmodule OrderStateMachine do
use GenServer
@transitions %{
pending: [:confirmed, :cancelled],
confirmed: [:shipped, :cancelled],
shipped: [:delivered]
}
def transition(pid, new_state) do
GenServer.call(pid, {:transition, new_state})
end
@impl true
def handle_call({:transition, new_state}, _from, state) do
allowed = Map.get(@transitions, state.current, [])
if new_state in allowed do
# オブザーバーに通知
notify_observers(state.observers, %{
old_state: state.current,
new_state: new_state
})
{:reply, {:ok, new_state}, %{state | current: new_state}}
else
{:reply, {:error, :invalid_transition}, state}
end
end
defp notify_observers(observers, event) do
Enum.each(observers, fn {_id, handler} -> handler.(event) end)
end
end
4. Decorator + Pipeline パターン¶
処理の動的拡張¶
defmodule DecoratorPipeline do
def with_logging(func, label) do
fn input ->
IO.puts("[#{label}] Input: #{inspect(input)}")
result = func.(input)
IO.puts("[#{label}] Output: #{inspect(result)}")
result
end
end
def with_timing(func) do
fn input ->
start = System.monotonic_time(:microsecond)
result = func.(input)
elapsed = System.monotonic_time(:microsecond) - start
{result, elapsed}
end
end
def with_error_handling(func, default \\ nil) do
fn input ->
try do
{:ok, func.(input)}
rescue
e -> {:error, e, default}
end
end
end
end
# パイプラインビルダー
defmodule PipelineBuilder do
defstruct steps: []
def new, do: %__MODULE__{}
def add_step(builder, step) do
%{builder | steps: builder.steps ++ [step]}
end
def build(builder, base_func) do
Enum.reduce(builder.steps, base_func, fn decorator, func ->
decorator.(func)
end)
end
end
5. Composite + Visitor パターン¶
ファイルシステムの例¶
defmodule FileSystem do
defmodule File do
defstruct [:name, :size]
end
defmodule Directory do
defstruct [:name, children: []]
end
# ビジター: 合計サイズ
def total_size(%File{size: size}), do: size
def total_size(%Directory{children: children}) do
children |> Enum.map(&total_size/1) |> Enum.sum()
end
# ビジター: ファイル数
def file_count(%File{}), do: 1
def file_count(%Directory{children: children}) do
children |> Enum.map(&file_count/1) |> Enum.sum()
end
# ビジター: パターンマッチでファイル検索
def find_files(%File{name: name} = file, pattern) do
if String.match?(name, pattern), do: [file], else: []
end
def find_files(%Directory{children: children}, pattern) do
children |> Enum.flat_map(&find_files(&1, pattern))
end
end
式の評価¶
defmodule Expression do
defmodule Number do
defstruct [:value]
end
defmodule BinaryOp do
defstruct [:op, :left, :right]
end
# 評価ビジター
def evaluate(%Number{value: v}), do: v
def evaluate(%BinaryOp{op: :add, left: l, right: r}) do
evaluate(l) + evaluate(r)
end
def evaluate(%BinaryOp{op: :mul, left: l, right: r}) do
evaluate(l) * evaluate(r)
end
# 文字列化ビジター
def stringify(%Number{value: v}), do: "#{v}"
def stringify(%BinaryOp{op: op, left: l, right: r}) do
"(#{stringify(l)} #{op_symbol(op)} #{stringify(r)})"
end
defp op_symbol(:add), do: "+"
defp op_symbol(:mul), do: "*"
end
6. 統合例: 注文処理システム¶
defmodule OrderProcessor do
alias DiscountFactory
alias ValidatorFactory
defstruct [:customer_type, :items, :validators]
def new(customer_type) do
%__MODULE__{customer_type: customer_type, items: [], validators: []}
end
def add_item(processor, name, price) do
%{processor | items: [{name, price} | processor.items]}
end
def add_validator(processor, validator_type) do
%{processor | validators: [validator_type | processor.validators]}
end
def calculate_total(processor) do
strategy = DiscountFactory.create(processor.customer_type)
processor.items
|> Enum.map(fn {_name, price} -> strategy.(price) end)
|> Enum.sum()
end
def process(processor) do
# パイプラインで処理
PipelineBuilder.new()
|> PipelineBuilder.add_error_handling(0.0)
|> PipelineBuilder.execute(fn _ -> calculate_total(processor) end, processor)
end
end
まとめ¶
パターンの組み合わせにより、以下が実現できます:
- 柔軟性: 異なる走査戦略と処理を組み合わせ
- 再利用性: 共通のビルディングブロックを様々な文脈で使用
- 拡張性: デコレータで既存の処理を拡張
- 型安全性: パターンマッチによる堅牢な処理分岐
パターンを単独で理解した後は、組み合わせて使う練習が重要です。