mcpField
E
elixir-antipatterns

elixir-antipatterns

github.com/gentleman-programming開発661191 回インストール31 週間前に更新
リポジトリ

elixir-antipatterns について

elixir-antipatternsは、堅牢性や保守性を損なうパターンを扱う、ElixirおよびPhoenix開発者向けのコーディングスキルです。エラー処理、アーキテクチャ、パフォーマンス、テストと改善方法をまとめています。

主な機能

  1. 01エラー戻り値の型を明記
  2. 02LiveViewと業務ロジックを分離
  3. 03関連データのプリロードでN+1を回避
  4. 04頻繁に検索する列にインデックス
  5. 05テストで期待動作をアサート

活用シーン

  1. 01Elixir関数のエラー処理
  2. 02Phoenix LiveViewのロジック整理
  3. 03Ecto関連クエリの最適化
  4. 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 つの主要なパターンのクイックリファレンスです。

  1. タグ付きタプル:nil や例外の代わりに {:ok, value} | {:error, reason} を返す
  2. 明示的な @spec:関数シグネチャにエラーケースを記述する
  3. コンテキストの分離:ビジネスロジックは LiveView ではなくコンテキストに置く
  4. 関連付けの事前読み込み:N+1 クエリを避けるために Repo.preload/2 を使用する
  5. with の矢印による束縛:失敗する可能性のある with 内の操作にはすべて <- を使用する
  6. データベースインデックス:頻繁にクエリされるカラムにインデックスを付ける
  7. テストのアサーション:すべてのテストで期待される動作をアサートする
  8. まとまりのある関数: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.getRepo.preload
with チェーンvalidated = fn(){:ok, validated} <- fn()
頻繁なクエリインデックスなしcreate index(:table, [:column])
テストアサーションなし期待される動作を assert する
複雑なロジック6 ステップ以上の with3 つの関数にまとめる

リソース

コメント