mcpField
E
elixir-antipatterns

elixir-antipatterns

github.com/gentleman-programming开发661191 次安装31 周前更新
代码仓库

关于 elixir-antipatterns

elixir-antipatterns 是面向 Elixir 和 Phoenix 开发者的编码规范技能,汇总影响健壮性与可维护性的关键反模式。内容涵盖错误处理、架构、性能和测试,并提供相应的改进做法。

主要功能

  1. 01明确标注错误返回类型
  2. 02将业务逻辑与 LiveView 分离
  3. 03预加载关联以减少 N+1 查询
  4. 04为常查列建立索引
  5. 05要求测试断言预期行为

适用场景

  1. 01实现 Elixir 函数的错误处理
  2. 02组织 Phoenix LiveView 与业务逻辑
  3. 03优化 Ecto 关联查询
  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 强制执行代码风格
扩展参考:参见 EXTENDED.md,了解 40 多种模式和深入示例


适用场景

主题: 错误处理(3 种模式) • 架构(2 种模式) • 性能(2 种模式) • 测试(1 种模式)

在以下场景中加载此技能:

  • 编写 Elixir 模块和函数
  • 使用 Phoenix Framework(Controllers、LiveView)
  • 构建 Ecto schemas 和数据库查询
  • 实现 BEAM 并发(Task、GenServer)
  • 使用带标签的元组处理错误
  • 使用 ExUnit 编写测试

关键模式

以下是此技能要求遵循的 8 种核心模式的快速参考:

  1. 带标签的元组:返回 {:ok, value} | {:error, reason},而不是 nil 或异常
  2. 显式的 @spec:在函数签名中说明错误情况
  3. 分离上下文:将业务逻辑放在上下文中,而不是 LiveView 中
  4. 预加载关联:使用 Repo.preload/2 避免 N+1 查询
  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)

影响:100 个用户的 N+1 查询需要 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 中放在 context 模块中
关联Enum.map + Repo.getRepo.preload
with 链validated = fn(){:ok, validated} <- fn()
频繁查询不创建索引create index(:table, [:column])
测试没有断言使用 assert 断言预期行为
复杂逻辑6 个以上步骤的 with拆分为 3 个函数

资源

评论

支持 Markdown