structural-design-principles

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Structural Design Principles

结构化设计原则

These principles originated in object-oriented design but apply to any programming paradigm . They're about code structure, not paradigm.
这些原则起源于面向对象设计,但适用于任何编程范式。它们关注的是代码结构,而非范式本身。

Paradigm Translations

范式适配

In functional programming (Elixir), they manifest as:
  • Composition Over Inheritance → Function composition, module composition, pipe operators
  • Law of Demeter → Minimize coupling between data structures, delegate to owning modules
  • Tell, Don't Ask → Push logic to the module owning the data type
  • Encapsulation → Module boundaries, immutability, pattern matching, opaque types
In object-oriented programming (TypeScript/React): Apply traditional OO interpretations with classes, interfaces, and encapsulation.
The underlying principle is the same across paradigms: manage dependencies, reduce coupling, and maintain clear boundaries.

函数式编程(Elixir)中,这些原则的体现形式为:
  • 组合优于继承 → 函数组合、模块组合、管道操作符
  • 迪米特法则 → 最小化数据结构间的耦合,委托给所属模块处理
  • Tell, Don't Ask(命令式编程) → 将逻辑推送给拥有该数据类型的模块
  • 封装 → 模块边界、不可变性、模式匹配、不透明类型
面向对象编程(TypeScript/React)中:采用传统的面向对象解读方式,结合类、接口和封装来实现。
所有范式背后的核心原则是一致的:管理依赖、降低耦合、维持清晰的边界。

Four Core Principles

四大核心原则

1. Composition Over Inheritance

1. 组合优于继承

Favor composition (combining simple behaviors) over inheritance (extending base classes).
Why: Inheritance creates tight coupling and fragile hierarchies. Composition provides flexibility.
优先使用组合(组合简单行为)而非继承(扩展基类)。
原因: 继承会创建紧密耦合的脆弱层级结构。组合则提供了更高的灵活性。

Elixir Approach (No Inheritance)

Elixir 实现方式(无继承)

Elixir doesn't have inheritance - it uses composition naturally through:
  • Module imports (
    use
    ,
    import
    ,
    alias
    )
  • Function composition (
    |>
    pipe operator)
  • Struct embedding
  • Behaviour protocols
elixir
undefined
Elixir 不支持继承——它通过以下方式天然实现组合:
  • 模块导入(
    use
    import
    alias
  • 函数组合(
    |>
    管道操作符)
  • 结构体嵌入
  • 行为协议
elixir
undefined

GOOD - Composition with pipes

GOOD - 使用管道操作符实现组合

def process_payment(order) do order |> validate_items() |> calculate_total() |> apply_discounts() |> charge_payment() |> send_receipt() end
def process_payment(order) do order |> validate_items() |> calculate_total() |> apply_discounts() |> charge_payment() |> send_receipt() end

GOOD - Compose behaviors

GOOD - 组合行为

defmodule User do use YourApp.Model use Ecto.Schema import Ecto.Changeset

Composes functionality from multiple modules

end
defmodule User do use YourApp.Model use Ecto.Schema import Ecto.Changeset

从多个模块组合功能

end

GOOD - Struct embedding

GOOD - 结构体嵌入

defmodule Address do embedded_schema do field :street, :string field :city, :string end end
defmodule User do schema "users" do embeds_one :address, Address end end
undefined
defmodule Address do embedded_schema do field :street, :string field :city, :string end end
defmodule User do schema "users" do embeds_one :address, Address end end
undefined

TypeScript Examples

TypeScript 示例

typescript
// BAD - Inheritance hierarchy
class Animal {
  move() { }
}

class FlyingAnimal extends Animal {
  fly() { }
}

class SwimmingAnimal extends Animal {
  swim() { }
}

class Duck extends FlyingAnimal {
  // Problem: Can't also inherit from SwimmingAnimal
  // Forced to duplicate swim() logic
}

// GOOD - Composition with interfaces
interface Movable {
  move(): void;
}

interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Duck implements Movable, Flyable, Swimmable {
  move() { this.walk(); }
  fly() { /* flying logic */ }
  swim() { /* swimming logic */ }
  private walk() { /* walking logic */ }
}
typescript
// GOOD - React composition (pattern)
// Instead of class inheritance, compose components

// Base behaviors as hooks
function useTaskData(gigId: string) {
  // Data fetching logic
}

function useTaskActions(gig: Task) {
  // Action handlers
}

function useTaskValidation(gig: Task) {
  // Validation logic
}

// Compose in component
function TaskDetails({ gigId }: Props) {
  const gig = useTaskData(gigId);
  const actions = useTaskActions(gig);
  const validation = useTaskValidation(gig);

  // Combines all behaviors through composition
  return <View>{/* render */}</View>;
}
typescript
// BAD - 继承层级结构
class Animal {
  move() { }
}

class FlyingAnimal extends Animal {
  fly() { }
}

class SwimmingAnimal extends Animal {
  swim() { }
}

class Duck extends FlyingAnimal {
  // 问题:无法同时继承自SwimmingAnimal
  // 被迫复制swim()逻辑
}

// GOOD - 使用接口实现组合
interface Movable {
  move(): void;
}

interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Duck implements Movable, Flyable, Swimmable {
  move() { this.walk(); }
  fly() { /* 飞行逻辑 */ }
  swim() { /* 游泳逻辑 */ }
  private walk() { /* 行走逻辑 */ }
}
typescript
// GOOD - React 组合模式
// 替代类继承,组合组件

// 基础行为封装为hooks
function useTaskData(gigId: string) {
  // 数据获取逻辑
}

function useTaskActions(gig: Task) {
  // 动作处理逻辑
}

function useTaskValidation(gig: Task) {
  // 验证逻辑
}

// 在组件中组合
function TaskDetails({ gigId }: Props) {
  const gig = useTaskData(gigId);
  const actions = useTaskActions(gig);
  const validation = useTaskValidation(gig);

  // 通过组合整合所有行为
  return <View>{/* 渲染内容 */}</View>;
}

Composition Guidelines

组合原则指南

  • Elixir: Use pipes, protocols, and behaviors instead of inheritance trees
  • TypeScript: Use interfaces, hooks, and function composition instead of class hierarchies
  • Build complex behavior from simple, reusable parts
  • Favor "has-a" over "is-a" relationships
  • Keep hierarchies shallow (max 2-3 levels if unavoidable)
  • Elixir:使用管道、协议和行为替代继承树
  • TypeScript:使用接口、hooks和函数组合替代类层级结构
  • 用简单、可复用的部件构建复杂行为
  • 优先选择“拥有”关系而非“是”关系
  • 若无法避免继承,保持层级结构浅(最多2-3层)

2. Law of Demeter (Principle of Least Knowledge)

2. 迪米特法则(最少知识原则)

A module should only talk to its immediate friends, not strangers.
The Rule: Only call methods on:
  1. The object itself
  2. Objects passed as parameters
  3. Objects it creates
  4. Its direct properties/fields
一个模块应该只与它的直接“朋友”交互,不与“陌生人”交谈。
规则: 仅调用以下对象的方法:
  1. 对象本身
  2. 作为参数传入的对象
  3. 该对象创建的对象
  4. 其直接属性/字段

DON'T chain through multiple objects (train wrecks)

不要链式调用多个对象(“火车失事”式调用)

Why (Elixir Context)

Elixir 场景下的原因

  • Reduces coupling: When you reach through multiple data structures (
    engagement.worker.address.city
    ), you're coupled to the entire chain. If any intermediate structure changes, your code breaks.
  • Improves testability: Code that only calls functions on its immediate collaborators is easier to test - you don't need to construct deep object graphs.
  • Enables refactoring: You can change internal structure without breaking callers if they only interact with top-level functions.
  • Follows functional boundaries: In Elixir, each module should be responsible for its own data type. The Law of Demeter enforces this by pushing you to delegate to the owning module.
  • 降低耦合:当你穿透多个数据结构(如
    engagement.worker.address.city
    )时,你会与整个调用链产生耦合。如果任何中间结构发生变化,你的代码就会崩溃。
  • 提升可测试性:仅调用直接协作对象的函数的代码更容易测试——你不需要构建深层的对象图。
  • 支持重构:如果调用者只与顶层函数交互,你可以修改内部结构而不破坏调用者。
  • 遵循函数式边界:在Elixir中,每个模块应该对自己的数据类型负责。迪米特法则通过推动你将逻辑委托给所属模块来强化这一点。

Elixir Examples

Elixir 示例

elixir
undefined
elixir
undefined

BAD - Violates Law of Demeter (train wreck)

BAD - 违反迪米特法则(“火车失事”式调用)

def get_worker_city(engagement) do engagement.worker.address.city

Knows too much about internal structure

end
def get_worker_city(engagement) do engagement.worker.address.city

对内部结构了解过多

end

What if worker doesn't have address?

如果worker没有address怎么办?

What if address structure changes?

如果address结构变化怎么办?

Tightly coupled to implementation

与实现细节紧密耦合

GOOD - Delegate to the module that owns the data type

GOOD - 委托给拥有该数据类型的模块

defmodule Assignment do def worker_city(%{worker: worker}) do User.city(worker) end end
defmodule User do def city(%{address: address}) do Address.city(address) end
def city(_), do: nil end
defmodule Assignment do def worker_city(%{worker: worker}) do User.city(worker) end end
defmodule User do def city(%{address: address}) do Address.city(address) end
def city(_), do: nil end

Now can call: Assignment.worker_city(engagement)

现在可以这样调用:Assignment.worker_city(engagement)

Each module is responsible for its own data

每个模块对自己的数据负责

Coupling minimized to immediate collaborators

耦合度最小化到直接协作对象


```elixir

```elixir

BAD - Reaching through associations

BAD - 穿透关联关系

def total_gig_hours(user_id) do user = Repo.get!(User, user_id) assignments = user.assignments Enum.reduce(assignments, 0, fn eng, acc -> acc + eng.shift.hours # Reaching through end) end
def total_gig_hours(user_id) do user = Repo.get!(User, user_id) assignments = user.assignments Enum.reduce(assignments, 0, fn eng, acc -> acc + eng.shift.hours # 穿透调用 end) end

GOOD - Delegate to the domain

GOOD - 委托给领域模块

def total_gig_hours(user_id) do user = Repo.get!(User, user_id) User.total_hours(user) end
defmodule User do def total_hours(%{assignments: assignments}) do Enum.reduce(assignments, 0, fn eng, acc -> acc + Assignment.hours(eng) end) end end
defmodule Assignment do def hours(%{shift: shift}), do: WorkPeriod.hours(shift) end
undefined
def total_gig_hours(user_id) do user = Repo.get!(User, user_id) User.total_hours(user) end
defmodule User do def total_hours(%{assignments: assignments}) do Enum.reduce(assignments, 0, fn eng, acc -> acc + Assignment.hours(eng) end) end end
defmodule Assignment do def hours(%{shift: shift}), do: WorkPeriod.hours(shift) end
undefined

TypeScript Examples: Law of Demeter

TypeScript 示例:迪米特法则

typescript
// BAD - Chain of doom
function displayUserLocation(engagement: Assignment) {
  const location = engagement.worker.profile.address.city;
  // Knows about 4 levels of object structure!
  return `Location: ${location}`;
}

// GOOD - Each object provides what you need
function displayUserLocation(engagement: Assignment) {
  const location = engagement.getUserCity();
  return `Location: ${location}`;
}

class Assignment {
  getUserCity(): string {
    return this.worker.getCity();
  }
}

class User {
  getCity(): string {
    return this.address.city;
  }
}
typescript
// BAD - GraphQL fragments violating Law of Demeter
const fragment = graphql`
  fragment TaskCard_gig on Task {
    id
    requester {
      organization {
        billing {
          paymentMethod {
            last4
          }
        }
      }
    }
  }
`;
// TaskCard shouldn't know about payment details!

// GOOD - Only query what you need
const fragment = graphql`
  fragment TaskCard_gig on Task {
    id
    title
    payRate
    location {
      city
      state
    }
  }
`;
// TaskCard only knows about gig display data
typescript
// BAD - 链式调用灾难
function displayUserLocation(engagement: Assignment) {
  const location = engagement.worker.profile.address.city;
  // 了解4层对象结构!
  return `Location: ${location}`;
}

// GOOD - 每个对象提供所需信息
function displayUserLocation(engagement: Assignment) {
  const location = engagement.getUserCity();
  return `Location: ${location}`;
}

class Assignment {
  getUserCity(): string {
    return this.worker.getCity();
  }
}

class User {
  getCity(): string {
    return this.address.city;
  }
}
typescript
// BAD - 违反迪米特法则的GraphQL片段
const fragment = graphql`
  fragment TaskCard_gig on Task {
    id
    requester {
      organization {
        billing {
          paymentMethod {
            last4
          }
        }
      }
    }
  }
`;
// TaskCard 不应该了解支付细节!

// GOOD - 仅查询所需数据
const fragment = graphql`
  fragment TaskCard_gig on Task {
    id
    title
    payRate
    location {
      city
      state
    }
  }
`;
// TaskCard 仅了解 gig 展示所需的数据

Law of Demeter Guidelines

迪米特法则指南

  • One dot (method call) is okay:
    object.method()
  • Multiple dots is a code smell:
    object.property.property.method()
  • Create wrapper methods instead of chaining
  • Each module should only know about its direct collaborators
  • Particularly important in GraphQL - don't query deep nested data you don't need
Exception: Fluent interfaces designed for chaining
In Elixir, the pipe operator and certain builder patterns (like Ecto) are designed for chaining:
elixir
undefined
  • 单个点(方法调用)是可以的:
    object.method()
  • 多个点是代码异味:
    object.property.property.method()
  • 创建包装方法而非链式调用
  • 每个模块应该只了解其直接协作对象
  • 在GraphQL中尤为重要——不要查询不需要的深层嵌套数据
例外: 专为链式调用设计的流畅接口
在Elixir中,管道操作符和某些构建器模式(如Ecto)是专为链式调用设计的:
elixir
undefined

This is okay - designed for chaining

这是允许的——专为链式调用设计

User.changeset(%{}) |> cast(attrs, [:email]) |> validate_required([:email]) |> unique_constraint(:email)
User.changeset(%{}) |> cast(attrs, [:email]) |> validate_required([:email]) |> unique_constraint(:email)

This is okay - Ecto.Query builder pattern

这是允许的——Ecto.Query 构建器模式

from(u in User) |> where([u], u.active == true) |> join(:inner, [u], p in assoc(u, :profile)) |> select([u, p], {u, p})
from(u in User) |> where([u], u.active == true) |> join(:inner, [u], p in assoc(u, :profile)) |> select([u, p], {u, p})

These patterns are explicitly designed for method chaining

这些模式是明确为方法链式调用设计的

Each function returns a chainable structure

每个函数返回一个可链式调用的结构


The key difference: fluent interfaces are **designed** for chaining as their
primary API, whereas reaching through data structures (`.worker.address.city`)
is **accidental** coupling.

关键区别:流畅接口的**主要API就是专为链式调用设计**的,而穿透数据结构(如`.worker.address.city`)是**意外的**耦合。

3. Tell, Don't Ask

3. Tell, Don't Ask(命令式编程)

Tell objects what to do, don't ask for their data and do it yourself.
告诉对象要做什么,不要询问它们的数据然后自己处理。

Why (Functional Context)

函数式场景下的原因

In Elixir, "Tell, Don't Ask" means delegating to the module that owns the data type . Instead of pulling data out of a structure and making decisions based on it, you pass the structure to the owning module and let it handle the logic.
This principle:
  • Encapsulates business rules with the data they operate on
  • Reduces coupling - callers don't need to know internal state or structure
  • Improves cohesion - related logic lives together in the owning module
  • Enables polymorphism - different implementations can handle the same "tell" differently
Think of it as: "Don't ask a struct for its fields and decide what to do - tell the module to do it."
在Elixir中,“Tell, Don't Ask”意味着将逻辑委托给拥有该数据类型的模块。不要从结构中提取数据然后基于这些数据做决策,而是将该结构传递给所属模块,让它处理逻辑。
这一原则:
  • 将业务规则封装在其操作的数据所在的位置
  • 降低耦合——调用者无需了解内部状态或结构
  • 提升内聚性——相关逻辑集中在所属模块中
  • 支持多态——不同的实现可以以不同方式处理同一个“命令”
可以这样理解:“不要询问结构体的字段然后决定做什么——告诉模块去执行操作。”

Elixir Examples: Tell Don't Ask

Elixir 示例:Tell Don't Ask

elixir
undefined
elixir
undefined

BAD - Asking for data and making decisions

BAD - 询问数据并做决策

def process_engagement(engagement) do if engagement.status == "pending" and engagement.worker_id != nil do attrs = %{status: "confirmed", confirmed_at: DateTime.utc_now()} Repo.update(Assignment.changeset(engagement, attrs)) end end
def process_engagement(engagement) do if engagement.status == "pending" and engagement.worker_id != nil do attrs = %{status: "confirmed", confirmed_at: DateTime.utc_now()} Repo.update(Assignment.changeset(engagement, attrs)) end end

We're asking about the engagement's state and deciding what to do

我们在询问engagement的状态然后决定做什么

GOOD - Delegate to the module that owns the Assignment struct

GOOD - 委托给拥有Assignment结构体的模块

def process_engagement(engagement) do Assignment.confirm(engagement) end
defmodule Assignment do def confirm(%{status: "pending", worker_id: worker_id} = engagement) when not is_nil(worker_id) do changeset = change(engagement, %{ status: "confirmed", confirmed_at: DateTime.utc_now() }) Repo.update(changeset) end
def confirm(engagement), do: {:error, :invalid_state}

Assignment module knows its own business rules

Callers just "tell" it to confirm, don't "ask" about status

end

```elixir
def process_engagement(engagement) do Assignment.confirm(engagement) end
defmodule Assignment do def confirm(%{status: "pending", worker_id: worker_id} = engagement) when not is_nil(worker_id) do changeset = change(engagement, %{ status: "confirmed", confirmed_at: DateTime.utc_now() }) Repo.update(changeset) end
def confirm(engagement), do: {:error, :invalid_state}

Assignment模块了解自己的业务规则

调用者只需“告诉”它执行确认操作,无需“询问”状态

end

```elixir

BAD - Asking and deciding

BAD - 询问数据并做决策

def charge_gig(gig) do if gig.payment_type == "per_hour" do rate = gig.hourly_rate hours = gig.total_hours Money.multiply(rate, hours) else gig.fixed_amount end end
def charge_gig(gig) do if gig.payment_type == "per_hour" do rate = gig.hourly_rate hours = gig.total_hours Money.multiply(rate, hours) else gig.fixed_amount end end

GOOD - Delegate to the module that owns the Task struct

GOOD - 委托给拥有Task结构体的模块

def charge_gig(gig) do Task.total_charge(gig) end
defmodule Task do def total_charge(%{payment_type: "per_hour", hourly_rate: rate, total_hours: hours}) do Money.multiply(rate, hours) end
def total_charge(%{payment_type: "fixed", fixed_amount: amount}) do amount end

Business logic lives with the data in the owning module

end
undefined
def charge_gig(gig) do Task.total_charge(gig) end
defmodule Task do def total_charge(%{payment_type: "per_hour", hourly_rate: rate, total_hours: hours}) do Money.multiply(rate, hours) end
def total_charge(%{payment_type: "fixed", fixed_amount: amount}) do amount end

业务逻辑与数据一起集中在所属模块

end
undefined

TypeScript Examples: Tell Don't Ask

TypeScript 示例:Tell Don't Ask

typescript
// BAD - Asking for data
function renderTaskStatus(gig: Task) {
  let statusText: string;
  let statusColor: string;

  if (gig.status === 'active' && gig.workerCount > 0) {
    statusText = 'In Progress';
    statusColor = 'green';
  } else if (gig.status === 'active') {
    statusText = 'Waiting for Users';
    statusColor = 'yellow';
  } else {
    statusText = 'Completed';
    statusColor = 'gray';
  }

  return <Badge text={statusText} color={statusColor} />;
}

// GOOD - Tell the gig to provide display info
function renderTaskStatus(gig: Task) {
  const { text, color } = gig.getStatusDisplay();
  return <Badge text={text} color={color} />;
}

class Task {
  getStatusDisplay(): { text: string; color: string } {
    if (this.status === 'active' && this.workerCount > 0) {
      return { text: 'In Progress', color: 'green' };
    } else if (this.status === 'active') {
      return { text: 'Waiting for Users', color: 'yellow' };
    }
    return { text: 'Completed', color: 'gray' };
  }
}
typescript
// BAD - 询问数据
function renderTaskStatus(gig: Task) {
  let statusText: string;
  let statusColor: string;

  if (gig.status === 'active' && gig.workerCount > 0) {
    statusText = 'In Progress';
    statusColor = 'green';
  } else if (gig.status === 'active') {
    statusText = 'Waiting for Users';
    statusColor = 'yellow';
  } else {
    statusText = 'Completed';
    statusColor = 'gray';
  }

  return <Badge text={statusText} color={statusColor} />;
}

// GOOD - 告诉gig对象提供展示信息
function renderTaskStatus(gig: Task) {
  const { text, color } = gig.getStatusDisplay();
  return <Badge text={text} color={color} />;
}

class Task {
  getStatusDisplay(): { text: string; color: string } {
    if (this.status === 'active' && this.workerCount > 0) {
      return { text: 'In Progress', color: 'green' };
    } else if (this.status === 'active') {
      return { text: 'Waiting for Users', color: 'yellow' };
    }
    return { text: 'Completed', color: 'gray' };
  }
}

Tell, Don't Ask Guidelines

Tell, Don't Ask 指南

  • Push behavior into the module that owns the data type (Elixir) or object (TypeScript)
  • Commands over queries (when possible)
  • Modules/objects should protect their own invariants
  • Reduces coupling - callers don't need to know internal state
  • Particularly important in Command handlers - they tell, don't ask
  • 将行为推送给拥有该数据类型的模块(Elixir)或对象(TypeScript)
  • 优先使用命令而非查询(在可行的情况下)
  • 模块/对象应该保护自己的不变量
  • 降低耦合——调用者无需了解内部状态
  • 在命令处理器中尤为重要——它们只“命令”,不“询问”

Pattern

模式

Command handlers follow "Tell, Don't Ask":
elixir
undefined
命令处理器遵循“Tell, Don't Ask”原则:
elixir
undefined

Command tells the system what to do

命令告诉系统要做什么

%CreateTask{requester_id: id, title: "Landscaping"} |> CreateTaskHandler.handle()
%CreateTask{requester_id: id, title: "Landscaping"} |> CreateTaskHandler.handle()

Handler tells domain objects to execute

处理器告诉领域对象执行操作

Not: Handler asks domain for data and decides

而非:处理器询问领域数据然后做决策

undefined
undefined

4. Encapsulation

4. 封装

Hide internal implementation details. Expose minimal, stable interfaces.
隐藏内部实现细节。暴露最小、稳定的接口。

Why: Encapsulation (Elixir Context)

Elixir 场景下的封装原因

  • Enables change: You can change internal implementation without breaking callers
  • Enforces invariants: Internal state can only change through controlled functions
  • Improves testability: Test the public interface, not implementation details
  • Reduces cognitive load: Callers only need to understand the public API
  • Supports modularity: Clear boundaries between modules
In Elixir, encapsulation is achieved through:
  • Module boundaries (private functions with
    defp
    )
  • Opaque types (
    @opaque
    )
  • Pattern matching guards
  • Changesets for validation at boundaries
  • Minimizing public API surface
  • 支持变更:你可以修改内部实现而不破坏调用者
  • 强制执行不变量:内部状态只能通过受控函数修改
  • 提升可测试性:测试公共接口而非实现细节
  • 降低认知负荷:调用者只需理解公共API
  • 支持模块化:模块间边界清晰
在Elixir中,封装通过以下方式实现:
  • 模块边界(使用
    defp
    定义私有函数)
  • 不透明类型(
    @opaque
  • 模式匹配守卫
  • 边界处的Changeset验证
  • 最小化公共API的暴露范围

Elixir Examples: Encapsulation

Elixir 示例:封装

elixir
undefined
elixir
undefined

BAD - Exposing internals

BAD - 暴露内部细节

defmodule PaymentProcessor do defstruct [:stripe_client, :api_key, :retry_count]
def process(processor, amount) do # Callers can access processor.stripe_client directly # Breaks if we change internal implementation end end
defmodule PaymentProcessor do defstruct [:stripe_client, :api_key, :retry_count]
def process(processor, amount) do # 调用者可以直接访问processor.stripe_client # 如果我们修改内部实现,代码会崩溃 end end

GOOD - Encapsulate internals

GOOD - 封装内部细节

defmodule PaymentProcessor do @type t :: %MODULE{ stripe_client: term(), api_key: String.t(), retry_count: integer() }
@enforce_keys [:stripe_client, :api_key] defstruct [:stripe_client, :api_key, retry_count: 3]

Public API

def new(api_key), do: %MODULE{ stripe_client: Stripe.Client.new(api_key), api_key: api_key }
def process(%MODULE{} = processor, amount) do # Internal implementation hidden do_process(processor, amount) end

Private implementation

defp do_process(processor, amount) do # Can change internals without affecting callers end end

```elixir
defmodule PaymentProcessor do @type t :: %MODULE{ stripe_client: term(), api_key: String.t(), retry_count: integer() }
@enforce_keys [:stripe_client, :api_key] defstruct [:stripe_client, :api_key, retry_count: 3]

公共API

def new(api_key), do: %MODULE{ stripe_client: Stripe.Client.new(api_key), api_key: api_key }
def process(%MODULE{} = processor, amount) do # 内部实现被隐藏 do_process(processor, amount) end

私有实现

defp do_process(processor, amount) do # 修改内部实现不会影响调用者 end end

```elixir

BAD - Ecto schema with map fields (no structure)

BAD - 使用map字段的Ecto schema(无结构)

defmodule Task do schema "tasks" do field :data, :map # Anything goes! end end
defmodule Task do schema "tasks" do field :data, :map # 可以存储任何内容! end end

GOOD - Explicit fields (encapsulation via type system)

GOOD - 显式字段(通过类型系统实现封装)

defmodule Task do schema "tasks" do field :title, :string field :description, :string field :pay_rate, Money.Ecto.Composite.Type field :status, Ecto.Enum, values: [:draft, :published, :active, :completed] end

Changesets enforce valid transitions

def publish_changeset(gig) do gig |> change(%{status: :published}) |> validate_required([:title, :description, :pay_rate]) end

Can't publish without required fields (encapsulated business rule)

end
undefined
defmodule Task do schema "tasks" do field :title, :string field :description, :string field :pay_rate, Money.Ecto.Composite.Type field :status, Ecto.Enum, values: [:draft, :published, :active, :completed] end

Changeset 强制执行有效的状态转换

def publish_changeset(gig) do gig |> change(%{status: :published}) |> validate_required([:title, :description, :pay_rate]) end

没有必填字段就无法发布(封装的业务规则)

end
undefined

TypeScript Examples: Encapsulation

TypeScript 示例:封装

typescript
// BAD - Public mutable state
class ShoppingCart {
  public items: Item[] = [];  // Anyone can modify directly!

  public total(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

const cart = new ShoppingCart();
cart.items.push(invalidItem);  // Bypasses validation!

// GOOD - Encapsulated state
class ShoppingCart {
  private items: Item[] = [];  // Hidden implementation

  public addItem(item: Item): void {
    if (this.isValid(item)) {
      this.items.push(item);
    } else {
      throw new Error('Invalid item');
    }
  }

  public removeItem(itemId: string): void {
    this.items = this.items.filter(item => item.id !== itemId);
  }

  public getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }

  public getItemCount(): number {
    return this.items.length;
  }

  private isValid(item: Item): boolean {
    return item.price > 0 && item.quantity > 0;
  }
  // All state changes go through controlled methods
}
typescript
// GOOD - React component encapsulation
function TaskCard({ gigRef }: Props) {
  // Encapsulate internal state
  const [expanded, setExpanded] = useState(false);
  const gig = useFragment(fragment, gigRef);

  // Private helpers
  const handleToggle = () => setExpanded(!expanded);

  // Public interface is just the props
  return (
    <Pressable onPress={handleToggle}>
      {/* Internal implementation */}
    </Pressable>
  );
}
// Parent components don't know about 'expanded' state
// Clean interface: pass gig data, get rendered card
typescript
// BAD - 公共可变状态
class ShoppingCart {
  public items: Item[] = [];  // 任何人都可以直接修改!

  public total(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

const cart = new ShoppingCart();
cart.items.push(invalidItem);  // 绕过了验证!

// GOOD - 封装状态
class ShoppingCart {
  private items: Item[] = [];  // 隐藏实现细节

  public addItem(item: Item): void {
    if (this.isValid(item)) {
      this.items.push(item);
    } else {
      throw new Error('Invalid item');
    }
  }

  public removeItem(itemId: string): void {
    this.items = this.items.filter(item => item.id !== itemId);
  }

  public getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }

  public getItemCount(): number {
    return this.items.length;
  }

  private isValid(item: Item): boolean {
    return item.price > 0 && item.quantity > 0;
  }
  // 所有状态变更都通过受控方法进行
}
typescript
// GOOD - React 组件封装
function TaskCard({ gigRef }: Props) {
  // 封装内部状态
  const [expanded, setExpanded] = useState(false);
  const gig = useFragment(fragment, gigRef);

  // 私有辅助函数
  const handleToggle = () => setExpanded(!expanded);

  // 公共接口仅包含props
  return (
    <Pressable onPress={handleToggle}>
      {/* 内部实现 */}
    </Pressable>
  );
}
// 父组件不知道'expanded'状态
// 简洁的接口:传入gig数据,获取渲染后的卡片

Encapsulation Guidelines

封装指南

  • Make fields/functions private by default, public only when needed
  • Use TypeScript
    private
    ,
    protected
    modifiers
  • Use Elixir module attributes (@) for internal data
  • Elixir: prefix private functions with
    defp
  • Validate inputs at boundaries (changesets, prop validation)
  • Don't expose internal data structures
  • Provide focused, minimal public APIs
  • Change detection should be internal (don't expose dirty flags)
  • 默认将字段/函数设为私有,仅在需要时设为公共
  • 使用TypeScript的
    private
    protected
    修饰符
  • 使用Elixir模块属性(@)存储内部数据
  • Elixir:私有函数以
    defp
    为前缀
  • 在边界处验证输入(Changeset、属性验证)
  • 不要暴露内部数据结构
  • 提供聚焦、最小化的公共API
  • 变更检测应该是内部的(不要暴露脏标记)

Patterns

模式

  • Ecto Changesets: Encapsulate validation and constraints
  • GraphQL Types: Only expose fields needed by frontend
  • Command Handlers: Encapsulate business rules
  • Relay Fragments: Encapsulate data requirements in component
  • Ecto Changesets:封装验证和约束
  • GraphQL 类型:仅暴露前端所需的字段
  • 命令处理器:封装业务规则
  • Relay Fragments:在组件中封装数据需求

Application Checklist

应用检查清单

Before implementing

实现前

  • Can I compose instead of inherit? (Composition > Inheritance)
  • Am I reaching through multiple objects? (Law of Demeter)
  • Should this object do the work instead of me? (Tell, Don't Ask)
  • Are internals hidden? (Encapsulation)
  • 我是否可以用组合替代继承?(组合 > 继承)
  • 我是否在穿透多个对象?(迪米特法则)
  • 应该由这个对象来完成工作,而不是我自己处理?(Tell, Don't Ask)
  • 内部细节是否被隐藏?(封装)

During implementation

实现中

  • Use composition (pipes, hooks, interfaces)
  • Call methods on direct collaborators only
  • Push behavior to data owners
  • Make fields/functions private by default
  • Validate at boundaries
  • 使用组合(管道、hooks、接口)
  • 仅调用直接协作对象的方法
  • 将行为推送给数据所有者
  • 默认将字段/函数设为私有
  • 在边界处验证输入

During code review

代码评审中

  • Any deep inheritance hierarchies? (max 2-3 levels)
  • Train wreck chains? (
    a.b.c.d()
    )
  • Logic operating on other objects' data?
  • Public mutable state?
  • 是否存在深层继承层级?(最多2-3层)
  • 是否存在“火车失事”式链式调用?(
    a.b.c.d()
  • 是否有逻辑在操作其他对象的数据?
  • 是否存在公共可变状态?

Red Flags

危险信号

Composition Over Inheritance

组合优于继承

  • Deep class hierarchies (>3 levels)
  • Can't extend because already inheriting
  • Duplicating code because can't multi-inherit
  • 深层类层级(超过3层)
  • 因为已经继承了某个类而无法再扩展
  • 因为无法多继承而重复代码

Law of Demeter

迪米特法则

  • Multiple dots:
    user.profile.address.city
  • GraphQL queries 5+ levels deep
  • Functions taking many parameters to avoid chaining
  • 多个点的调用:
    user.profile.address.city
  • GraphQL查询深度超过5层
  • 函数接收大量参数以避免链式调用

Tell, Don't Ask

Tell, Don't Ask

  • Lots of getters used in if statements
  • Business logic outside the entity
  • Type checking with if/else (
    if type == 'x'
    )
  • 大量getter用于if语句
  • 业务逻辑位于实体外部
  • 使用if/else进行类型检查(如
    if type == 'x'

Encapsulation

封装

  • Public mutable fields
  • Map/any types instead of structured data
  • Callers modifying internal state directly
  • No validation at boundaries
  • 公共可变字段
  • 使用map/any类型而非结构化数据
  • 调用者直接修改内部状态
  • 边界处没有验证

Integration with Existing Skills

与现有技能的集成

Works with

兼容的技能

  • solid-principles
    : Particularly Single Responsibility and Dependency Inversion
  • ecto-patterns
    : Changesets encapsulate validation
  • cqrs-pattern
    : Commands tell, don't ask
  • atomic-design-pattern
    : Components composed from atoms/molecules
  • simplicity-principles
    : Simple composition over complex inheritance
  • solid-principles
    :尤其是单一职责原则和依赖倒置原则
  • ecto-patterns
    :Changesets封装验证
  • cqrs-pattern
    :命令遵循Tell, Don't Ask原则
  • atomic-design-pattern
    :组件由原子/分子组合而成
  • simplicity-principles
    :简单组合优于复杂继承

Remember

总结

Favor object composition over class inheritance (Gang of Four)

优先使用对象组合而非类继承(四人组)

  • Compose simple behaviors into complex ones
  • Delegate to direct collaborators only
  • Tell objects what to do, don't interrogate them
  • Hide implementation details behind clean interfaces
Good design is about managing dependencies and protecting invariants - regardless of paradigm.
  • 组合简单行为以构建复杂功能
  • 委托给直接协作对象
  • 命令对象执行操作,不要询问它们
  • 隐藏实现细节,提供清晰的接口
优秀的设计在于管理依赖和保护不变量——无论采用何种范式。