separation-of-concerns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSeparation of Concerns
关注点分离
Overview
概述
Each piece of code should do one thing. Data, logic, and presentation should be separate.
Mixed concerns create untestable, unreusable, unmaintainable code. Separation enables testing, reuse, and clarity.
每一段代码都应该只负责一件事。数据处理、业务逻辑与UI展示应当相互分离。
职责混杂会导致代码难以测试、复用和维护。而关注点分离则能提升代码的可测试性、可复用性与清晰度。
When to Use
适用场景
- Component fetches, transforms, and displays data
- Business logic mixed with UI code
- Database queries in controllers
- Hard to test a piece of code in isolation
- 组件同时负责数据获取、转换与展示
- 业务逻辑与UI代码混杂
- 控制器中包含数据库查询
- 代码难以单独进行隔离测试
The Iron Rule
铁规则
NEVER mix data fetching, business logic, and presentation in one place.No exceptions:
- Not for "it's a small component"
- Not for "it's simpler this way"
- Not for "only used once"
- Not for "it works"
NEVER mix data fetching, business logic, and presentation in one place.无例外情况:
- 即使是“小型组件”也不例外
- 即使“这样写更简单”也不例外
- 即使“只使用一次”也不例外
- 即使“当前能运行”也不例外
Detection: Mixed Concerns Smell
识别:职责混杂的代码异味
If one file does fetch + transform + display, STOP:
typescript
// ❌ VIOLATION: Component does everything
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Data fetching
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
// Business logic / transformation
const fullName = `${data.firstName} ${data.lastName}`;
const memberSince = new Date(data.createdAt).toLocaleDateString();
const isVIP = data.orderCount > 100;
setUser({ ...data, fullName, memberSince, isVIP });
setLoading(false);
});
}, [userId]);
// Presentation
if (loading) return <div>Loading...</div>;
return (
<div className="user-profile">
<h1>{user.fullName}</h1>
{user.isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {user.memberSince}</p>
</div>
);
}Problems:
- Can't test formatting without fetching
- Can't reuse fetch logic
- Can't reuse presentation
- Component does 3 jobs
如果单个文件同时负责数据获取+转换+展示,请立即停止:
typescript
// ❌ VIOLATION: Component does everything
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Data fetching
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
// Business logic / transformation
const fullName = `${data.firstName} ${data.lastName}`;
const memberSince = new Date(data.createdAt).toLocaleDateString();
const isVIP = data.orderCount > 100;
setUser({ ...data, fullName, memberSince, isVIP });
setLoading(false);
});
}, [userId]);
// Presentation
if (loading) return <div>Loading...</div>;
return (
<div className="user-profile">
<h1>{user.fullName}</h1>
{user.isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {user.memberSince}</p>
</div>
);
}存在的问题:
- 无法在不发起请求的情况下测试数据格式化逻辑
- 无法复用数据获取逻辑
- 无法复用UI展示组件
- 一个组件承担了3项职责
The Correct Pattern: Separated Layers
正确模式:分层分离
typescript
// ✅ CORRECT: Separated concerns
// 1. Data fetching (hook)
// hooks/useUser.ts
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
// 2. Business logic (pure functions)
// utils/userFormatters.ts
interface FormattedUser {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function formatUser(user: User): FormattedUser {
return {
fullName: `${user.firstName} ${user.lastName}`,
memberSince: new Date(user.createdAt).toLocaleDateString(),
isVIP: user.orderCount > 100,
};
}
// 3. Presentation (dumb component)
// components/UserCard.tsx
interface UserCardProps {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function UserCard({ fullName, memberSince, isVIP }: UserCardProps) {
return (
<div className="user-profile">
<h1>{fullName}</h1>
{isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {memberSince}</p>
</div>
);
}
// 4. Composition (container component)
// pages/UserProfile.tsx
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
const formatted = formatUser(user);
return <UserCard {...formatted} />;
}typescript
// ✅ CORRECT: Separated concerns
// 1. Data fetching (hook)
// hooks/useUser.ts
function useUser(userId: string) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
})
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
return { user, loading, error };
}
// 2. Business logic (pure functions)
// utils/userFormatters.ts
interface FormattedUser {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function formatUser(user: User): FormattedUser {
return {
fullName: `${user.firstName} ${user.lastName}`,
memberSince: new Date(user.createdAt).toLocaleDateString(),
isVIP: user.orderCount > 100,
};
}
// 3. Presentation (dumb component)
// components/UserCard.tsx
interface UserCardProps {
fullName: string;
memberSince: string;
isVIP: boolean;
}
function UserCard({ fullName, memberSince, isVIP }: UserCardProps) {
return (
<div className="user-profile">
<h1>{fullName}</h1>
{isVIP && <span className="vip-badge">VIP</span>}
<p>Member since: {memberSince}</p>
</div>
);
}
// 4. Composition (container component)
// pages/UserProfile.tsx
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
const formatted = formatUser(user);
return <UserCard {...formatted} />;
}Benefits of Separation
关注点分离的优势
| Mixed | Separated |
|---|---|
| Can't test formatting | |
| Can't reuse fetch | |
| Can't reuse UI | |
| 1 complex component | 4 simple pieces |
| 职责混杂 | 分层分离 |
|---|---|
| 无法测试格式化逻辑 | |
| 无法复用数据获取逻辑 | |
| 无法复用UI组件 | |
| 1个复杂组件 | 4个简单模块 |
The Layers
分层说明
1. Data Layer (Hooks, Services)
1. 数据层(Hooks、服务类)
- Fetching data
- API calls
- State management
- No business logic
- 数据获取
- API调用
- 状态管理
- 不包含业务逻辑
2. Logic Layer (Pure Functions)
2. 逻辑层(纯函数)
- Transformations
- Calculations
- Validations
- Business rules
- 数据转换
- 计算逻辑
- 数据校验
- 业务规则
3. Presentation Layer (Components)
3. 展示层(组件)
- Rendering UI
- Styling
- User interactions
- No data fetching
- UI渲染
- 样式处理
- 用户交互
- 不包含数据获取
4. Composition Layer (Containers)
4. 组合层(容器组件)
- Connects layers
- Handles loading/error states
- Passes data down
- 连接各层
- 处理加载/错误状态
- 向下传递数据
Pressure Resistance Protocol
常见质疑应对方案
1. "It's a Small Component"
1. “这只是个小型组件”
Pressure: "For simple cases, separation is overkill"
Response: Small becomes large. Start clean, stay clean.
Action: Separate even for small components. It costs little.
质疑: “对于简单场景,分离是过度设计”
回应: 小型组件会逐渐变大。从一开始就保持代码整洁。
行动: 即使是小型组件也要进行分离,代价极小。
2. "It's Simpler This Way"
2. “放在一起更简单”
Pressure: "Everything in one place is easier to understand"
Response: Mixed concerns seem simple but are hard to test, debug, and modify.
Action: Separation is simpler in the long run.
质疑: “所有代码放在一处更容易理解”
回应: 职责混杂看似简单,但会导致代码难以测试、调试和修改。
行动: 从长远来看,分离的代码更简单。
3. "Only Used Once"
3. “只会被使用一次”
Pressure: "This component is unique, won't be reused"
Response: Testability matters even for unique components.
Action: Separate for testability, not just reuse.
质疑: “这个组件是独有的,不会被复用”
回应: 即使不考虑复用,可测试性也很重要。
行动: 为了可测试性而进行分离,而非仅仅为了复用。
Red Flags - STOP and Reconsider
危险信号 - 立即停止并重新考虑
- with fetch + transform + setState
useEffect - Business logic in render functions
- Database queries in route handlers
- API calls in utility functions
- Components with 100+ lines
All of these mean: Separate the concerns.
- 中同时包含数据获取、转换与状态更新
useEffect - 渲染函数中包含业务逻辑
- 路由处理器中包含数据库查询
- 工具函数中包含API调用
- 组件代码超过100行
出现以上任意情况都意味着:需要进行关注点分离。
Quick Reference
快速参考
| Concern | Where It Belongs |
|---|---|
| API calls | Hooks / Services |
| Data transformation | Pure functions |
| Business rules | Pure functions |
| UI rendering | Presentation components |
| Connecting pieces | Container components |
| 职责 | 所属层级 |
|---|---|
| API调用 | Hooks / 服务类 |
| 数据转换 | 纯函数 |
| 业务规则 | 纯函数 |
| UI渲染 | 展示组件 |
| 模块组合 | 容器组件 |
Common Rationalizations (All Invalid)
常见自我合理化借口(均不成立)
| Excuse | Reality |
|---|---|
| "Small component" | Small grows. Separate now. |
| "Simpler together" | Separated is simpler to test/modify. |
| "Only used once" | Testability matters. |
| "It works" | Working ≠ maintainable. |
| "Over-engineering" | This is just engineering. |
| 借口 | 实际情况 |
|---|---|
| “组件很小” | 小型组件会逐渐膨胀。现在就进行分离。 |
| “放在一起更简单” | 分离后的代码更易于测试和修改。 |
| “只会用一次” | 可测试性同样重要。 |
| “当前能运行” | 能运行 ≠ 可维护。 |
| “过度设计” | 这只是标准的工程实践。 |
The Bottom Line
核心总结
Data fetching in hooks. Logic in pure functions. UI in components.
Separation enables testing, reuse, and maintainability. A component should either fetch data, transform it, or display it - never all three.
数据获取放在Hooks中,业务逻辑放在纯函数中,UI展示放在组件中。
关注点分离能提升代码的可测试性、可复用性与可维护性。一个组件应当只负责数据获取、数据转换或UI展示中的一项,绝不能同时承担多项职责。