Skip to content

第 7 章: 構造体とプロトコルによるポリモーフィズム

7.1 はじめに

この章では、defstructdefprotocol を使って、Elixir で型ごとに振る舞いを切り替える方法を学びます。FizzBuzz の値オブジェクトを導入し、Type01Type02Type03 が同じインターフェース generate/1 を実装する形でポリモーフィズムを実現します。

7.2 defstruct で値オブジェクトを定義する

FizzBuzz の入力値を表す値オブジェクト FizzBuzz.Model.Value を定義します。@enforce_keys で必須フィールドを宣言すると、不完全な構造体生成を防げます。

defmodule FizzBuzz.Model.Value do
  @enforce_keys [:number]
  defstruct [:number]

  @type t :: %__MODULE__{
          number: pos_integer()
        }
end

@enforce_keys [:number] により、%FizzBuzz.Model.Value{} のような生成は実行時エラーになり、number の設定漏れを早期に検出できます。

7.3 defprotocol で共通インターフェースを定義する

次に、生成処理の共通契約として Generatable プロトコルを定義します。

defprotocol FizzBuzz.Generatable do
  @spec generate(t()) :: String.t()
  def generate(value)
end

プロトコルは「どの型であっても generate/1 を呼べる」という抽象化を提供します。実際の処理は各構造体の defimpl で分岐します。

7.4 defimpl で Type01 / Type02 / Type03 を実装する

Type01Type02Type03 は、それぞれ通常数値、3 の倍数、5 の倍数を表す構造体とします。

defmodule FizzBuzz.Model.Type01 do
  @enforce_keys [:value]
  defstruct [:value]
end

defmodule FizzBuzz.Model.Type02 do
  @enforce_keys [:value]
  defstruct [:value]
end

defmodule FizzBuzz.Model.Type03 do
  @enforce_keys [:value]
  defstruct [:value]
end

defimpl FizzBuzz.Generatable, for: FizzBuzz.Model.Type01 do
  def generate(%FizzBuzz.Model.Type01{value: value}), do: Integer.to_string(value.number)
end

defimpl FizzBuzz.Generatable, for: FizzBuzz.Model.Type02 do
  def generate(%FizzBuzz.Model.Type02{}), do: "Fizz"
end

defimpl FizzBuzz.Generatable, for: FizzBuzz.Model.Type03 do
  def generate(%FizzBuzz.Model.Type03{}), do: "Buzz"
end

この設計では、呼び出し側は具体型を意識せず、FizzBuzz.Generatable.generate/1 だけを呼べばよくなります。

7.5 ExUnit でプロトコル実装を検証する

プロトコルの振る舞いは ExUnit で確認できます。

defmodule FizzBuzz.GeneratableTest do
  use ExUnit.Case, async: true

  alias FizzBuzz.Generatable
  alias FizzBuzz.Model.{Type01, Type02, Type03, Value}

  test "Type01 は数値文字列を返す" do
    value = %Value{number: 1}
    assert Generatable.generate(%Type01{value: value}) == "1"
  end

  test "Type02 は Fizz を返す" do
    value = %Value{number: 3}
    assert Generatable.generate(%Type02{value: value}) == "Fizz"
  end

  test "Type03 は Buzz を返す" do
    value = %Value{number: 5}
    assert Generatable.generate(%Type03{value: value}) == "Buzz"
  end

  test "Value は number が必須" do
    assert_raise ArgumentError, fn ->
      struct!(Value, %{})
    end
  end
end

struct!/2 を使うと、@enforce_keys の違反をテストで明示的に確認できます。

7.6 まとめ

この章では、defstruct@enforce_keys で値オブジェクトを定義し、defprotocol / defimpl で型ごとに generate/1 を実装しました。Elixir では継承ではなく、プロトコルでポリモーフィズムを組み立てるのが実践的です。