第12章: Visitor パターン¶
はじめに¶
Visitor パターンは、データ構造と操作を分離し、既存のデータ構造を変更することなく新しい操作を追加できるようにするパターンです。
Elixir では、プロトコルを使用して Visitor インターフェースを定義し、各図形タイプに対する実装を提供します。
1. Element: 図形の定義¶
defmodule Circle do
defstruct [:center, :radius]
def new(center, radius), do: %__MODULE__{center: center, radius: radius}
end
defmodule Square do
defstruct [:top_left, :side]
def new(top_left, side), do: %__MODULE__{top_left: top_left, side: side}
end
defmodule Rectangle do
defstruct [:top_left, :width, :height]
def new(top_left, width, height) do
%__MODULE__{top_left: top_left, width: width, height: height}
end
end
2. Visitor プロトコル¶
defprotocol ShapeVisitor do
@doc "図形を訪問して処理を行う"
def visit(visitor, shape)
end
3. JSON Visitor¶
defmodule JsonVisitor do
defstruct []
def new, do: %__MODULE__{}
def to_json(shape), do: ShapeVisitor.visit(new(), shape)
end
defimpl ShapeVisitor, for: JsonVisitor do
def visit(_visitor, %Circle{center: {x, y}, radius: r}) do
~s({"type":"circle","center":[#{x},#{y}],"radius":#{r}})
end
def visit(_visitor, %Square{top_left: {x, y}, side: s}) do
~s({"type":"square","topLeft":[#{x},#{y}],"side":#{s}})
end
def visit(_visitor, %Rectangle{top_left: {x, y}, width: w, height: h}) do
~s({"type":"rectangle","topLeft":[#{x},#{y}],"width":#{w},"height":#{h}})
end
end
4. Area Visitor¶
defmodule AreaVisitor do
defstruct []
def new, do: %__MODULE__{}
def calculate_area(shape), do: ShapeVisitor.visit(new(), shape)
def total_area(shapes) do
Enum.reduce(shapes, 0, &(&2 + calculate_area(&1)))
end
end
defimpl ShapeVisitor, for: AreaVisitor do
def visit(_visitor, %Circle{radius: r}) do
:math.pi() * r * r
end
def visit(_visitor, %Square{side: s}) do
s * s
end
def visit(_visitor, %Rectangle{width: w, height: h}) do
w * h
end
end
5. Perimeter Visitor¶
defmodule PerimeterVisitor do
defstruct []
def new, do: %__MODULE__{}
def calculate_perimeter(shape), do: ShapeVisitor.visit(new(), shape)
end
defimpl ShapeVisitor, for: PerimeterVisitor do
def visit(_visitor, %Circle{radius: r}), do: 2 * :math.pi() * r
def visit(_visitor, %Square{side: s}), do: 4 * s
def visit(_visitor, %Rectangle{width: w, height: h}), do: 2 * (w + h)
end
6. SVG Visitor¶
defmodule SvgVisitor do
defstruct fill: "none", stroke: "black", stroke_width: 1
def new(opts \\ []) do
%__MODULE__{
fill: Keyword.get(opts, :fill, "none"),
stroke: Keyword.get(opts, :stroke, "black"),
stroke_width: Keyword.get(opts, :stroke_width, 1)
}
end
end
defimpl ShapeVisitor, for: SvgVisitor do
def visit(%{fill: f, stroke: s, stroke_width: sw}, %Circle{center: {cx, cy}, radius: r}) do
~s(<circle cx="#{cx}" cy="#{cy}" r="#{r}" fill="#{f}" stroke="#{s}" stroke-width="#{sw}"/>)
end
# ... 他の図形も同様
end
7. 複合 Visitor¶
defmodule CompositeVisitor do
def apply_all(shape, visitors) do
Map.new(visitors, fn {key, visitor} ->
{key, ShapeVisitor.visit(visitor, shape)}
end)
end
end
# 使用例
results = CompositeVisitor.apply_all(circle, [
{:json, JsonVisitor.new()},
{:area, AreaVisitor.new()}
])
8. DrawingContext¶
defmodule DrawingContext do
defstruct shapes: []
def add_shape(ctx, shape), do: %{ctx | shapes: ctx.shapes ++ [shape]}
def to_json(%{shapes: shapes}) do
json_items = Enum.map(shapes, &JsonVisitor.to_json/1)
"[" <> Enum.join(json_items, ",") <> "]"
end
def total_area(%{shapes: shapes}), do: AreaVisitor.total_area(shapes)
end
まとめ¶
Elixir での Visitor パターンのポイント:
- Protocol で Visitor インターフェースを定義
- defimpl で各図形タイプに対する実装を提供
- 新しい操作を追加する際は新しい Visitor を作成
- 既存の図形コードを変更せずに拡張可能
- CompositeVisitor で複数の操作を一度に適用