Skip to content

第 9 章: モジュール設計とビヘイビア

9.1 はじめに

この章では、モジュールの責務分割を保ちながら拡張しやすい構成を作るために、モジュールの入れ子、@behaviour / @callbackalias を使った設計を扱います。FizzBuzz のコマンド層を例に、共通インターフェースを実装する形へ整理します。

9.2 モジュールの入れ子で責務を表現する

Elixir はファイル階層とは独立して、名前空間で責務を整理できます。たとえば FizzBuzz.Command.ValueCommand は「単一値を扱うコマンド」を明確に示します。

defmodule FizzBuzz.Command.ValueCommand do
  def run(number), do: Integer.to_string(number)
end

FizzBuzz.Command 配下に ValueCommandListCommand を置くと、同じ関心事のモジュール群を把握しやすくなります。

9.3 @behaviour と @callback でインターフェースを定義する

まずはコマンド共通の契約を FizzBuzz.Command ビヘイビアとして定義します。

defmodule FizzBuzz.Command do
  @callback run(input :: term()) :: {:ok, term()} | {:error, atom()}
end

この @callback により、実装モジュールは run/1 を提供する必要があります。未実装の場合、コンパイル時に警告が出るため、設計の抜け漏れを防げます。

9.4 ValueCommand と ListCommand でビヘイビアを実装する

@behaviour FizzBuzz.Command を指定し、run/1 を実装します。alias を使ってモジュール参照を簡略化します。

defmodule FizzBuzz.Command.ValueCommand do
  @behaviour FizzBuzz.Command

  alias FizzBuzz.FizzBuzzService

  @impl true
  def run(number) when is_integer(number) and number > 0 do
    {:ok, FizzBuzzService.generate(number)}
  end

  @impl true
  def run(_), do: {:error, :invalid_number}
end

defmodule FizzBuzz.Command.ListCommand do
  @behaviour FizzBuzz.Command

  alias FizzBuzz.FizzBuzzService

  @impl true
  def run(limit) when is_integer(limit) and limit > 0 do
    values =
      1..limit
      |> Enum.map(&FizzBuzzService.generate/1)

    {:ok, values}
  end

  @impl true
  def run(_), do: {:error, :invalid_limit}
end

alias FizzBuzz.FizzBuzzService を使うことで、FizzBuzz.FizzBuzzService.generate/1FizzBuzzService.generate/1 と短く書けます。

9.5 ExUnit でビヘイビア実装を検証する

各コマンドが同じ契約で動くことを ExUnit で確認します。

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

  alias FizzBuzz.Command.{ListCommand, ValueCommand}

  test "ValueCommand は単一値を処理する" do
    assert ValueCommand.run(3) == {:ok, "Fizz"}
  end

  test "ValueCommand は不正値でエラーを返す" do
    assert ValueCommand.run(0) == {:error, :invalid_number}
    assert ValueCommand.run("3") == {:error, :invalid_number}
  end

  test "ListCommand は 1..limit の結果一覧を返す" do
    assert ListCommand.run(5) == {:ok, ["1", "2", "Fizz", "4", "Buzz"]}
  end

  test "ListCommand は不正値でエラーを返す" do
    assert ListCommand.run(-1) == {:error, :invalid_limit}
  end
end

ValueCommandListCommand は入出力対象が異なっても、同じ run/1 契約を守るため、呼び出し側の扱いを統一できます。

9.6 まとめ

この章では、FizzBuzz.Command.ValueCommand のようなモジュール入れ子で責務を整理し、@behaviour / @callback で契約を固定し、alias で記述を簡潔にする方法を確認しました。ビヘイビア中心の設計により、機能追加時も既存コードへの影響を局所化できます。