关于 elixir-antipatterns
elixir-antipatterns 是面向 Elixir 和 Phoenix 开发者的编码规范技能,汇总影响健壮性与可维护性的关键反模式。内容涵盖错误处理、架构、性能和测试,并提供相应的改进做法。
主要功能
- 01明确标注错误返回类型
- 02将业务逻辑与 LiveView 分离
- 03预加载关联以减少 N+1 查询
- 04为常查列建立索引
- 05要求测试断言预期行为
适用场景
- 01实现 Elixir 函数的错误处理
- 02组织 Phoenix LiveView 与业务逻辑
- 03优化 Ecto 关联查询
- 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 种核心模式的快速参考:
- 带标签的元组:返回
{:ok, value} | {:error, reason},而不是nil或异常 - 显式的 @spec:在函数签名中说明错误情况
- 分离上下文:将业务逻辑放在上下文中,而不是 LiveView 中
- 预加载关联:使用
Repo.preload/2避免 N+1 查询 - 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)
影响: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.get | Repo.preload |
| with 链 | validated = fn() | {:ok, validated} <- fn() |
| 频繁查询 | 不创建索引 | create index(:table, [:column]) |
| 测试 | 没有断言 | 使用 assert 断言预期行为 |
| 复杂逻辑 | 6 个以上步骤的 with | 拆分为 3 个函数 |
资源
- Elixir 风格指南
- Phoenix 上下文
- Ecto 查询性能
- ExUnit 最佳实践
- 扩展模式:参见
EXTENDED.md,了解 40 多种反模式
评论
支持 Markdown