separation-of-concerns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Separation 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

关注点分离的优势

MixedSeparated
Can't test formatting
formatUser()
tested in isolation
Can't reuse fetch
useUser()
reusable anywhere
Can't reuse UI
UserCard
reusable with any data
1 complex component4 simple pieces
职责混杂分层分离
无法测试格式化逻辑
formatUser()
可独立测试
无法复用数据获取逻辑
useUser()
可在任意地方复用
无法复用UI组件
UserCard
可适配任意数据源
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

危险信号 - 立即停止并重新考虑

  • useEffect
    with fetch + transform + setState
  • 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

快速参考

ConcernWhere It Belongs
API callsHooks / Services
Data transformationPure functions
Business rulesPure functions
UI renderingPresentation components
Connecting piecesContainer components
职责所属层级
API调用Hooks / 服务类
数据转换纯函数
业务规则纯函数
UI渲染展示组件
模块组合容器组件

Common Rationalizations (All Invalid)

常见自我合理化借口(均不成立)

ExcuseReality
"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展示中的一项,绝不能同时承担多项职责。