elixir-antipatterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Elixir Anti-Patterns

Elixir 反模式

Critical anti-patterns that compromise robustness and maintainability in Elixir/Phoenix applications.
Complement with:
mix format
and
Credo
for style enforcement
Extended reference: See
EXTENDED.md
for 40+ patterns and deep-dive examples

这些会影响Elixir/Phoenix应用健壮性和可维护性的关键反模式。
配套工具:使用
mix format
Credo
进行代码风格检查
扩展参考:查看
EXTENDED.md
获取40+种模式及深度示例

When to Use

适用场景

Topics: Error handling (3 patterns) • Architecture (2 patterns) • Performance (2 patterns) • Testing (1 pattern)
Load this skill when:
  • Writing Elixir modules and functions
  • Working with Phoenix Framework (Controllers, LiveView)
  • Building Ecto schemas and database queries
  • Implementing BEAM concurrency (Task, GenServer)
  • Handling errors with tagged tuples
  • Writing tests with ExUnit

主题:错误处理(3种模式)• 架构(2种模式)• 性能(2种模式)• 测试(1种模式)
在以下场景使用本技能:
  • 编写Elixir模块和函数
  • 使用Phoenix Framework(Controllers、LiveView)开发
  • 构建Ecto schema和数据库查询
  • 实现BEAM并发(Task、GenServer)
  • 使用标记元组处理错误
  • 使用ExUnit编写测试

Critical Patterns

核心模式

Quick reference to the 8 core patterns this skill enforces:
  1. Tagged Tuples: Return
    {:ok, value} | {:error, reason}
    instead of
    nil
    or exceptions
  2. Explicit @spec: Document error cases in function signatures
  3. Context Separation: Business logic in contexts, not LiveView
  4. Preload Associations: Use
    Repo.preload/2
    to avoid N+1 queries
  5. with Arrow Binding: Use
    <-
    for all failable operations in
    with
  6. Database Indexes: Index frequently queried columns
  7. Test Assertions: Every test must assert expected behavior
  8. Cohesive Functions: Group
    with
    chains >4 steps into functions
See
## Anti-Patterns
section below for detailed ❌ BAD / ✅ CORRECT code examples.

本技能涵盖的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
    链封装为函数
查看下方「## 反模式」章节获取详细的❌ 错误示例 / ✅ 正确示例。

Code Examples

代码示例

Example 1: Error Handling with Tagged Tuples

示例1:使用标记元组处理错误

elixir
undefined
elixir
undefined

✅ CORRECT - Errors as values, explicit in @spec

✅ 正确 - 错误作为值,在@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
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
undefined
def fetch_user(id) do Repo.get(User, id) || raise "User not found" end
undefined

Example 2: Phoenix LiveView with Context Separation

示例2:Phoenix LiveView的上下文分离

Architecture Layers:
  User Request → LiveView (UI only) → Context (business logic) → Schema/Repo (data)
               ↓                    ↓                           ↓
           handle_event()     Accounts.create_user()      Repo.insert()
elixir
undefined
架构分层:
  用户请求 → LiveView(仅处理UI)→ 上下文(业务逻辑)→ Schema/Repo(数据层)
               ↓                    ↓                           ↓
           handle_event()     Accounts.create_user()      Repo.insert()
elixir
undefined

✅ CORRECT - Thin LiveView, logic in context

✅ 正确 - 轻量LiveView,逻辑委托给上下文

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
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

❌ 错误 - 业务逻辑放在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
undefined
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
undefined

Example 3: Ecto N+1 Query Optimization

示例3:Ecto N+1查询优化

elixir
undefined
elixir
undefined

✅ CORRECT - Preload associations (2 queries total)

✅ 正确 - 预加载关联(共2次查询)

users = User |> Repo.all() |> Repo.preload(:posts) Enum.map(users, fn user -> process(user, user.posts) end)
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'),

注意:对于复杂过滤(如WHERE posts.status = 'published'),

use join + preload in the query itself. See EXTENDED.md for advanced patterns.

需在查询中同时使用join + preload。查看EXTENDED.md获取进阶模式。

❌ BAD - Query in loop (101 queries for 100 users)

❌ 错误 - 循环内查询(100个用户会产生101次查询)

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)

---
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)

---

Anti-Patterns

反模式

Error Management

错误管理

Don't: Use
raise
for Business Errors

不要:对业务错误使用
raise

elixir
undefined
elixir
undefined

❌ BAD

❌ 错误

def fetch_user(id) do Repo.get(User, id) || raise "User not found" end
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

**Why**: `@spec` documents errors, pattern matching forces explicit handling.

---
@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`会记录错误类型,模式匹配强制显式处理错误。

---

Don't: Return
nil
for Errors

不要:用
nil
表示错误

elixir
undefined
elixir
undefined

❌ BAD - No context on failure

❌ 错误 - 失败时无上下文信息

def find_user(email), do: Repo.get_by(User, email: email)
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

---
@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

---

Don't: Use
=
Inside
with
for Failable Operations

不要:在
with
中对可能失败的操作使用
=

elixir
undefined
elixir
undefined

❌ 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
with {:ok, user} <- fetch_user(id), validated = validate(user), # ← 未检查{: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

---
with {:ok, user} <- fetch_user(id), {:ok, validated} <- validate(user), {:ok, saved} <- save(validated) do {:ok, saved} end

---

Architecture & Boundaries

架构与边界

Don't: Put Business Logic in LiveView

不要:将业务逻辑放在LiveView中

elixir
undefined
elixir
undefined

❌ 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
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

**Why**: Contexts testable without Phoenix, logic reusable.

---
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即可测试,逻辑可复用。

---

Don't: Chain More Than 4 Steps in
with

不要:
with
链超过4步

elixir
undefined
elixir
undefined

❌ 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
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

---
with {:ok, validated} <- validate_and_fetch(id), {:ok, processed} <- process_business_rules(validated), {:ok, result} <- persist_and_notify(processed) do {:ok, result} end

---

Data & Performance

数据与性能

Don't: Query Inside Loops (N+1)

不要:循环内查询(N+1问题)

elixir
undefined
elixir
undefined

❌ BAD - 101 queries for 100 users

❌ 错误 - 100个用户产生101次查询

users = Repo.all(User) Enum.map(users, fn user -> posts = Repo.all(from p in Post, where: p.user_id == ^user.id) end)
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

✅ 正确 - 共2次查询

User |> Repo.all() |> Repo.preload(:posts)

**Impact**: 100 users with N+1 = 10 seconds vs 5ms with preload.

---
User |> Repo.all() |> Repo.preload(:posts)

**影响**:N+1查询处理100个用户需10秒,预加载仅需5毫秒。

---

Don't: Query Without Indexes

不要:无索引查询

elixir
undefined
elixir
undefined

❌ BAD - No index on frequently queried column

❌ 错误 - 频繁查询的列无索引

Migration:

迁移文件:

create table(:users) do add :email, :string end
create table(:users) do add :email, :string end

✅ CORRECT - Add index

✅ 正确 - 添加索引

create table(:users) do add :email, :string end create unique_index(:users, [:email])

**Why**: Full table scan on 1M+ rows vs instant index lookup.

---
create table(:users) do add :email, :string end create unique_index(:users, [:email])

**原因**:百万级数据下全表扫描 vs 索引即时查找。

---

Testing

测试

Don't: Write Tests Without Assertions

不要:编写无断言的测试

elixir
undefined
elixir
undefined

❌ BAD - What's being tested?

❌ 错误 - 测试目标不明确

test "creates user" do UserService.create_user(%{name: "Juan"}) end
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

---
test "creates user successfully" do assert {:ok, user} = UserService.create_user(%{name: "Juan"}) assert user.name == "Juan" end

---

Quick Reference

速查表

SituationAnti-PatternCorrect Pattern
Error handling
raise "Not found"
{:error, :not_found}
Missing dataReturn
nil
{:error, :not_found}
Business logicIn LiveViewIn context modules
Associations
Enum.map
+
Repo.get
Repo.preload
with chains
validated = fn()
{:ok, validated} <- fn()
Frequent queriesNo index
create index(:table, [:column])
TestingNo assertions
assert
expected behavior
Complex logic6+ step
with
Group into 3 functions

场景反模式正确模式
错误处理
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个内聚函数

Resources

参考资源