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 (, , )
- Function composition ( pipe operator)
- Struct embedding
- Behaviour protocols
Elixir 不支持继承——它通过以下方式天然实现组合:
- 模块导入(、、)
- 函数组合( 管道操作符)
- 结构体嵌入
- 行为协议
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
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
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:
- The object itself
- Objects passed as parameters
- Objects it creates
- Its direct properties/fields
一个模块应该只与它的直接“朋友”交互,不与“陌生人”交谈。
规则: 仅调用以下对象的方法:
- 对象本身
- 作为参数传入的对象
- 该对象创建的对象
- 其直接属性/字段
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中,每个模块应该对自己的数据类型负责。迪米特法则通过推动你将逻辑委托给所属模块来强化这一点。
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
耦合度最小化到直接协作对象
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
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
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:
- 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:
- 单个点(方法调用)是可以的:
- 多个点是代码异味:
object.property.property.method()
- 创建包装方法而非链式调用
- 每个模块应该只了解其直接协作对象
- 在GraphQL中尤为重要——不要查询不需要的深层嵌套数据
例外: 专为链式调用设计的流畅接口
在Elixir中,管道操作符和某些构建器模式(如Ecto)是专为链式调用设计的:
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
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
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
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
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
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)
- 优先使用命令而非查询(在可行的情况下)
- 模块/对象应该保护自己的不变量
- 降低耦合——调用者无需了解内部状态
- 在命令处理器中尤为重要——它们只“命令”,不“询问”
Command handlers follow "Tell, Don't Ask":
命令处理器遵循“Tell, Don't Ask”原则:
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
而非:处理器询问领域数据然后做决策
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 )
- Opaque types ()
- Pattern matching guards
- Changesets for validation at boundaries
- Minimizing public API surface
- 支持变更:你可以修改内部实现而不破坏调用者
- 强制执行不变量:内部状态只能通过受控函数修改
- 提升可测试性:测试公共接口而非实现细节
- 降低认知负荷:调用者只需理解公共API
- 支持模块化:模块间边界清晰
在Elixir中,封装通过以下方式实现:
- 模块边界(使用定义私有函数)
- 不透明类型()
- 模式匹配守卫
- 边界处的Changeset验证
- 最小化公共API的暴露范围
Elixir Examples: Encapsulation
Elixir 示例:封装
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
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
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
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
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 , modifiers
- Use Elixir module attributes (@) for internal data
- Elixir: prefix private functions with
- 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的、修饰符
- 使用Elixir模块属性(@)存储内部数据
- Elixir:私有函数以为前缀
- 在边界处验证输入(Changeset、属性验证)
- 不要暴露内部数据结构
- 提供聚焦、最小化的公共API
- 变更检测应该是内部的(不要暴露脏标记)
- 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
应用检查清单
Composition Over Inheritance
组合优于继承
- Deep class hierarchies (>3 levels)
- Can't extend because already inheriting
- Duplicating code because can't multi-inherit
- 深层类层级(超过3层)
- 因为已经继承了某个类而无法再扩展
- 因为无法多继承而重复代码
- 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 ()
- 大量getter用于if语句
- 业务逻辑位于实体外部
- 使用if/else进行类型检查(如)
- 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
与现有技能的集成
- : Particularly Single Responsibility and Dependency Inversion
- : Changesets encapsulate validation
- : Commands tell, don't ask
- : Components composed from atoms/molecules
- : Simple composition over complex inheritance
- :尤其是单一职责原则和依赖倒置原则
- :Changesets封装验证
- :命令遵循Tell, Don't Ask原则
- :组件由原子/分子组合而成
- :简单组合优于复杂继承
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.
- 组合简单行为以构建复杂功能
- 委托给直接协作对象
- 命令对象执行操作,不要询问它们
- 隐藏实现细节,提供清晰的接口
优秀的设计在于管理依赖和保护不变量——无论采用何种范式。