elixir-antipatterns について
elixir-antipatternsは、堅牢性や保守性を損なうパターンを扱う、ElixirおよびPhoenix開発者向けのコーディングスキルです。エラー処理、アーキテクチャ、パフォーマンス、テストと改善方法をまとめています。
主な機能
- 01エラー戻り値の型を明記
- 02LiveViewと業務ロジックを分離
- 03関連データのプリロードでN+1を回避
- 04頻繁に検索する列にインデックス
- 05テストで期待動作をアサート
活用シーン
- 01Elixir関数のエラー処理
- 02Phoenix LiveViewのロジック整理
- 03Ecto関連クエリの最適化
- 04明示的なアサーションを含むExUnitテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ gentleman-programming/ gentleman-skills --skill elixir-antipatternsインストール後、Claude Code で/elixir-antipatternsを入力して読み込まれたか確認します。
同梱ファイルassets/extended.mdREADME.md
詳細
Elixir のアンチパターン
Elixir/Phoenix アプリケーションの堅牢性と保守性を損なう重大なアンチパターンです。
併用するツール:スタイルを徹底するために
mix formatとCredoを使用してください
詳細リファレンス:40 以上のパターンと詳しい例についてはEXTENDED.mdを参照してください
使用する場面
トピック: エラー処理(3 パターン)• アーキテクチャ(2 パターン)• パフォーマンス(2 パターン)• テスト(1 パターン)
以下の作業時にこのスキルを読み込んでください。
- Elixir のモジュールや関数を記述する
- Phoenix Framework(コントローラー、LiveView)を使用する
- Ecto スキーマやデータベースクエリを作成する
- BEAM の並行処理(Task、GenServer)を実装する
- タグ付きタプルを使用してエラーを処理する
- ExUnit でテストを記述する
重要なパターン
このスキルが徹底する 8 つの主要なパターンのクイックリファレンスです。
- タグ付きタプル:
nilや例外の代わりに{:ok, value} | {:error, reason}を返す - 明示的な @spec:関数シグネチャにエラーケースを記述する
- コンテキストの分離:ビジネスロジックは LiveView ではなくコンテキストに置く
- 関連付けの事前読み込み:N+1 クエリを避けるために
Repo.preload/2を使用する - with の矢印による束縛:失敗する可能性のある
with内の操作にはすべて<-を使用する - データベースインデックス:頻繁にクエリされるカラムにインデックスを付ける
- テストのアサーション:すべてのテストで期待される動作をアサートする
- まとまりのある関数:4 ステップを超える
withチェーンは関数にまとめる
詳細な ❌ BAD / ✅ CORRECT のコード例については、以下の
## Anti-Patternsセクションを参照してください。
コード例
例 1:タグ付きタプルによるエラー処理
# ✅ CORRECT - Errors as values, explicit in @spec
defmodule UserService do
@spec fetch_user(String.t()) :: {:ok, User.t()} | {:error, :not_found}
def fetch_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
end
# ❌ BAD - Exceptions for business errors
def fetch_user(id) do
Repo.get(User, id) || raise "User not found"
end
例 2:コンテキストを分離した Phoenix LiveView
Architecture Layers:
User Request → LiveView (UI only) → Context (business logic) → Schema/Repo (data)
↓ ↓ ↓
handle_event() Accounts.create_user() Repo.insert()
# ✅ CORRECT - Thin LiveView, logic in context
defmodule MyAppWeb.UserLive.Index do
use MyAppWeb, :live_view
def handle_event("create", params, socket) do
case Accounts.create_user(params) do
{:ok, user} -> {:noreply, redirect(socket, to: ~p"/users/#{user}")}
{:error, changeset} -> {:noreply, assign(socket, changeset: changeset)}
end
end
end
# ❌ BAD - Business logic in LiveView
def handle_event("create", %{"user" => params}, socket) do
if String.length(params["name"]) < 3 do
{:noreply, put_flash(socket, :error, "Too short")}
else
case Repo.insert(User.changeset(%User{}, params)) do
{:ok, user} -> send_email(user); redirect(socket)
end
end
end
例 3:Ecto の N+1 クエリ最適化
# ✅ CORRECT - Preload associations (2 queries total)
users = User |> Repo.all() |> Repo.preload(:posts)
Enum.map(users, fn user -> process(user, user.posts) end)
# Note: For complex filtering (e.g., WHERE posts.status = 'published'),
# use join + preload in the query itself. See EXTENDED.md for advanced patterns.
# ❌ BAD - Query in loop (101 queries for 100 users)
users = Repo.all(User)
Enum.map(users, fn user ->
posts = Repo.all(from p in Post, where: p.user_id == ^user.id)
{user, posts}
end)
アンチパターン
エラー管理
してはいけないこと:ビジネスエラーに raise を使用する
# ❌ BAD
def fetch_user(id) do
Repo.get(User, id) || raise "User not found"
end
# ✅ CORRECT
@spec fetch_user(String.t()) :: {:ok, User.t()} | {:error, :not_found}
def fetch_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
理由:@spec でエラーを記述し、パターンマッチングによって明示的な処理を促します。
してはいけないこと:エラーに対して nil を返す
# ❌ BAD - No context on failure
def find_user(email), do: Repo.get_by(User, email: email)
# ✅ CORRECT - Explicit error reason
@spec find_user(String.t()) :: {:ok, User.t()} | {:error, :not_found}
def find_user(email) do
case Repo.get_by(User, email: email) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
してはいけないこと:失敗する可能性のある操作で with 内に = を使用する
# ❌ BAD - Validate errors silenced
with {:ok, user} <- fetch_user(id),
validated = validate(user), # ← Doesn't check for {:error, _}
{:ok, saved} <- save(validated) do
{:ok, saved}
end
# ✅ CORRECT - All operations use <-
with {:ok, user} <- fetch_user(id),
{:ok, validated} <- validate(user),
{:ok, saved} <- save(validated) do
{:ok, saved}
end
アーキテクチャと境界
してはいけないこと:ビジネスロジックを LiveView に置く
# ❌ BAD - Validation in view
def handle_event("create", %{"user" => params}, socket) do
if String.length(params["name"]) < 3 do
{:noreply, put_flash(socket, :error, "Too short")}
else
case Repo.insert(User.changeset(%User{}, params)) do
{:ok, user} -> redirect(socket)
end
end
end
# ✅ CORRECT - Delegate to context
def handle_event("create", params, socket) do
case Accounts.create_user(params) do
{:ok, user} -> {:noreply, redirect(socket, to: ~p"/users/#{user}")}
{:error, changeset} -> {:noreply, assign(socket, changeset: changeset)}
end
end
理由:コンテキストは Phoenix なしでテストでき、ロジックを再利用できます。
してはいけないこと:with で 4 ステップを超えてチェーンする
# ❌ BAD - Too many responsibilities
with {:ok, a} <- step1(),
{:ok, b} <- step2(a),
{:ok, c} <- step3(b),
{:ok, d} <- step4(c),
{:ok, e} <- step5(d) do
{:ok, e}
end
# ✅ CORRECT - Group into cohesive functions
with {:ok, validated} <- validate_and_fetch(id),
{:ok, processed} <- process_business_rules(validated),
{:ok, result} <- persist_and_notify(processed) do
{:ok, result}
end
データとパフォーマンス
してはいけないこと:ループ内でクエリする(N+1)
# ❌ BAD - 101 queries for 100 users
users = Repo.all(User)
Enum.map(users, fn user ->
posts = Repo.all(from p in Post, where: p.user_id == ^user.id)
end)
# ✅ CORRECT - 2 queries total
User |> Repo.all() |> Repo.preload(:posts)
影響:N+1 クエリではユーザー 100 人に 10 秒かかりますが、事前読み込みを使えば 5 ミリ秒で済みます。
してはいけないこと:インデックスなしでクエリする
# ❌ BAD - No index on frequently queried column
# Migration:
create table(:users) do
add :email, :string
end
# ✅ CORRECT - Add index
create table(:users) do
add :email, :string
end
create unique_index(:users, [:email])
理由:100 万件を超える行では全表スキャンが発生しますが、インデックス検索なら即座に結果が得られます。
テスト
してはいけないこと:アサーションなしでテストを記述する
# ❌ BAD - What's being tested?
test "creates user" do
UserService.create_user(%{name: "Juan"})
end
# ✅ CORRECT - Assert expected behavior
test "creates user successfully" do
assert {:ok, user} = UserService.create_user(%{name: "Juan"})
assert user.name == "Juan"
end
クイックリファレンス
| 状況 | アンチパターン | 正しいパターン |
|---|---|---|
| エラー処理 | raise "Not found" | {:error, :not_found} |
| データの欠落 | nil を返す | {:error, :not_found} |
| ビジネスロジック | LiveView に置く | コンテキストモジュールに置く |
| 関連付け | Enum.map + Repo.get | Repo.preload |
| with チェーン | validated = fn() | {:ok, validated} <- fn() |
| 頻繁なクエリ | インデックスなし | create index(:table, [:column]) |
| テスト | アサーションなし | 期待される動作を assert する |
| 複雑なロジック | 6 ステップ以上の with | 3 つの関数にまとめる |
リソース
- Elixir スタイルガイド
- Phoenix コンテキスト
- Ecto クエリのパフォーマンス
- ExUnit のベストプラクティス
- 拡張パターン:40 以上のアンチパターンについては
EXTENDED.mdを参照してください