关于 separation-of-concerns
面向软件开发者的关注点分离编码指南,主张将数据获取、业务逻辑、界面呈现和层间组合拆分。指南通过检测信号、示例和分层建议,说明如何提升代码的可测试性、复用性与可维护性。
主要功能
- 01说明数据、逻辑与呈现分层
- 02列出混合关注点的识别信号
- 03提供分层代码模式
- 04说明测试与复用的收益
适用场景
- 01重构同时获取和展示数据的组件
- 02拆分控制器中的数据库查询与业务逻辑
- 03提高独立测试和复用能力
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill separation-of-concerns安装后在 Claude Code 中输入/separation-of-concerns确认已加载。
详细介绍
职责分离
概述
每段代码都应该只做一件事。数据、逻辑和表现应彼此分离。
职责混杂会导致代码无法测试、无法复用且难以维护。职责分离有助于测试、复用和保持清晰。
适用场景
- 组件同时获取、转换和显示数据
- 业务逻辑与 UI 代码混杂
- 控制器中包含数据库查询
- 难以单独测试某段代码
铁律
NEVER mix data fetching, business logic, and presentation in one place.
没有例外:
- 不能因为“这是个小组件”
- 不能因为“这样更简单”
- 不能因为“只用一次”
- 不能因为“它能运行”
检测:职责混杂的迹象
如果一个文件同时负责获取、转换和显示数据,请立即停止:
// ❌ 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>
);
}
问题:
- 无法在不获取数据的情况下测试格式化
- 无法复用获取数据的逻辑
- 无法复用表现层
- 一个组件承担了 3 项工作
正确模式:分层
// ✅ 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} />;
}
职责分离的好处
| 混杂 | 分离 |
|---|---|
| 无法测试格式化 | 可单独测试 formatUser() |
| 无法复用数据获取逻辑 | useUser() 可在任何地方复用 |
| 无法复用 UI | UserCard 可配合任何数据复用 |
| 1 个复杂组件 | 4 个简单部分 |
各层说明
1. 数据层(Hooks、服务)
- 获取数据
- API 调用
- 状态管理
- 不包含业务逻辑
2. 逻辑层(纯函数)
- 数据转换
- 计算
- 验证
- 业务规则
3. 表现层(组件)
- 渲染 UI
- 样式
- 用户交互
- 不获取数据
4. 组合层(容器)
- 连接各层
- 处理加载和错误状态
- 向下传递数据
抵御压力的准则
1. “这是个小组件”
压力:“对于简单场景,分离职责是小题大做。”
**回应:**小组件会变大。从一开始就保持整洁。
**行动:**即使是小组件也要分离职责。这样做成本很低。
2. “这样更简单”
压力:“把所有内容放在一处更容易理解。”
**回应:**职责混杂看似简单,实际上难以测试、调试和修改。
**行动:**从长远来看,职责分离更简单。
3. “只用一次”
压力:“这个组件很独特,不会复用。”
**回应:**即使是独特的组件,可测试性也很重要。
**行动:**为了可测试性而分离,而不只是为了复用。
危险信号——立即停止并重新考虑
useEffect中同时包含获取数据、转换数据和setState- 渲染函数中包含业务逻辑
- 路由处理程序中包含数据库查询
- 工具函数中包含 API 调用
- 超过 100 行的组件
这些情况都意味着:分离职责。
快速参考
| 职责 | 所属位置 |
|---|---|
| API 调用 | Hooks / 服务 |
| 数据转换 | 纯函数 |
| 业务规则 | 纯函数 |
| UI 渲染 | 表现组件 |
| 连接各部分 | 容器组件 |
常见的自我开脱(均不成立)
| 借口 | 事实 |
|---|---|
| “小组件” | 小组件会变大。现在就分离。 |
| “放在一起更简单” | 分离后更容易测试和修改。 |
| “只用一次” | 可测试性很重要。 |
| “它能运行” | 能运行 ≠ 易于维护。 |
| “过度设计” | 这只是正常的工程实践。 |
归根结底
在 Hooks 中获取数据。在纯函数中处理逻辑。在组件中编写 UI。
职责分离有助于测试、复用和维护。组件应该只负责获取数据、转换数据或显示数据之一——绝不能三者兼顾。
评论
支持 Markdown