从 Claude Code 源码学习关注点分离(SoC)原则

引言

在软件开发中,关注点分离(Separation of Concerns, SoC)是最基础也是最重要的设计原则之一。它的核心思想是:将程序划分为不同的部分,每个部分只负责一个特定的功能或”关注点”。

今天,我们通过分析 Claude Code 的源码结构,看看一个成熟的工业级项目是如何实践这一原则的。


什么是关注点分离?

关注点分离原则要求我们将系统中的不同职责分开,使得:

  1. 每个模块只做一件事,并且做好
  2. 模块之间低耦合,通过清晰的接口通信
  3. 高内聚,相关的功能放在一起
  4. 易于理解和维护,修改一个关注点不影响其他部分

好处

  • ✅ 代码更易读、易理解
  • ✅ 便于单元测试
  • ✅ 降低修改风险
  • ✅ 支持并行开发
  • ✅ 提高代码复用性

Claude Code 的目录结构分析

Claude Code 是一个复杂的 CLI 应用,包含 1,987 个 TypeScript 文件。让我们看看它是如何组织代码的:

src/
├── tools/              # 工具层:53 个独立工具
├── commands/           # 命令层:87 个斜杠命令
├── services/           # 服务层:API、MCP、Analytics 等
├── components/         # UI 层:148 个终端界面组件
├── hooks/              # Hooks 层:87 个自定义 Hooks
├── buddy/              # 宠物系统(独立功能模块)
├── assistant/          # 助手模式(KAIROS)
├── coordinator/        # 多 Agent 协调器
├── bridge/             # 远程控制桥接(33 个文件)
├── proactive/          # 主动模式
├── vim/                # Vim 模式引擎
├── voice/              # 语音交互
├── state/              # 状态管理
├── context/            # 上下文管理
├── constants/          # 常量定义
├── types/              # 类型定义
├── utils/              # 工具函数(333 个文件)
└── ...

这个结构清晰地体现了关注点分离的思想。让我们逐层分析。


1. 工具层(tools/)— 原子能力封装

设计理念

tools/ 目录包含了 53 个独立的工具,每个工具都是一个原子能力

src/tools/
├── BashTool/           # 执行 Bash 命令
├── FileEditTool/       # 编辑文件
├── FileReadTool/       # 读取文件
├── FileWriteTool/      # 写入文件
├── GlobTool/           # 文件匹配
├── GrepTool/           # 文本搜索
├── WebSearchTool/      # 网络搜索
├── MCPTool/            # MCP 协议工具
├── AgentTool/          # Agent 调度
├── TodoWriteTool/      # 任务管理
└── ... (共 53 个)

SoC 体现

每个工具的职责单一

  • BashTool 只负责执行 shell 命令
  • FileReadTool 只负责读取文件内容
  • WebSearchTool 只负责网络搜索

工具之间相互独立

  • 工具 A 不需要知道工具 B 的存在
  • 可以单独测试、单独替换
  • 新增工具不影响现有工具

统一的接口规范

// 所有工具都遵循相同的接口
interface Tool {
  name: string;
  description: string;
  execute(params: any): Promise<Result>;
}

为什么这样设计?

  1. 可扩展性:新增工具只需添加一个新目录
  2. 可测试性:每个工具可以独立单元测试
  3. 可组合性:Agent 可以灵活组合多个工具
  4. 权限控制:可以精细控制每个工具的访问权限

2. 命令层(commands/)— 用户交互入口

设计理念

commands/ 目录包含了 87 个斜杠命令,是用户与 CLI 交互的主要入口:

src/commands/
├── help/               # 帮助命令
├── clear/              # 清屏
├── compact/            # 压缩上下文
├── mcp/                # MCP 管理
├── model/              # 模型切换
├── theme/              # 主题设置
├── ultraplan.tsx       # 云端规划(65KB)
├── insights.ts         # 洞察分析(113KB)
├── install.tsx         # 安装向导(38KB)
└── ... (共 87 个)

SoC 体现

命令与业务逻辑分离

  • 命令层只负责解析用户输入、展示结果
  • 具体的业务逻辑委托给 services/tools/

示例:/compact 命令

// commands/compact/index.ts
export default async function compactCommand() {
  // 1. 解析参数
  const params = parseArgs();
  
  // 2. 调用服务层
  await compactService.execute(params);
  
  // 3. 展示结果
  displayResult();
}

命令之间互不干扰

  • /theme 命令不知道 /mcp 的存在
  • 每个命令有独立的目录和文件
  • 可以单独启用/禁用某个命令

Feature Gate 机制

Claude Code 使用编译开关控制命令的可见性:

// 只有内部用户才能看到的命令
if (USER_TYPE === 'ant') {
  registerCommand('/bughunter');
  registerCommand('/mock-limits');
}

// 需要特定 feature 开关的命令
if (feature('BUDDY')) {
  registerCommand('/buddy');
}

这体现了配置与代码分离的原则。


3. 服务层(services/)— 业务逻辑核心

设计理念

services/ 目录是真正的业务逻辑所在,包含了各种服务:

src/services/
├── api/                # API 客户端(20 个文件)
├── mcp/                # MCP 协议实现(23 个文件)
├── analytics/          # 数据分析(9 个文件)
├── autoDream/          # 自动记忆整合(4 个文件)
├── compact/            # 上下文压缩(15 个文件)
├── lsp/                # LSP 语言服务(8 个文件)
├── oauth/              # OAuth 认证(6 个文件)
├── SessionMemory/      # 会话记忆(3 个文件)
├── teamMemorySync/     # 团队记忆同步(5 个文件)
└── ... (共 38 个子目录)

SoC 体现

按领域划分服务

  • api/:只负责与后端 API 通信
  • mcp/:只负责 MCP 协议相关逻辑
  • analytics/:只负责数据上报和分析
  • oauth/:只负责身份认证

服务之间通过接口通信

// 服务 A 使用服务 B
class CompactService {
  constructor(
    private apiService: ApiService,
    private memoryService: MemoryService
  ) {}
  
  async execute() {
    // 通过接口调用其他服务
    const data = await this.apiService.fetch();
    await this.memoryService.save(data);
  }
}

依赖注入

  • 服务不直接创建依赖,而是通过构造函数注入
  • 便于 mocking 和单元测试
  • 降低耦合度

4. UI 层(components/ + hooks/)— 界面呈现

设计理念

UI 层分为两部分:

components/(148 个组件):

src/components/
├── ChatMessage/        # 聊天消息组件
├── ToolOutput/         # 工具输出展示
├── PermissionDialog/   # 权限对话框
├── StatusBar/          # 状态栏
├── Spinner/            # 加载动画
└── ... (共 148 个)

hooks/(87 个 Hooks):

src/hooks/
├── useChat/            # 聊天逻辑
├── usePermissions/     # 权限管理
├── useTheme/           # 主题切换
├── useKeybindings/     # 快捷键
└── ... (共 87 个)

SoC 体现

展示与逻辑分离

  • components/:只负责渲染 UI
  • hooks/:只负责业务逻辑和状态管理

示例

// Hook:处理逻辑
function useChat() {
  const [messages, setMessages] = useState([]);
  
  const sendMessage = async (text: string) => {
    // 业务逻辑
    const response = await api.send(text);
    setMessages([...messages, response]);
  };
  
  return { messages, sendMessage };
}

// Component:负责展示
function ChatComponent() {
  const { messages, sendMessage } = useChat();
  
  return (
    <View>
      {messages.map(msg => <Message key={msg.id} {...msg} />)}
      <Input onSend={sendMessage} />
    </View>
  );
}

组件可复用

  • Spinner 组件可以在任何地方使用
  • PermissionDialog 可以被多个命令调用
  • 组件不关心数据来源,只关心 props

5. 功能模块层 — 独立子系统

Claude Code 有一些相对独立的功能模块,每个模块都有自己的目录:

Buddy(宠物系统)

src/buddy/
├── species.ts          # 物种定义
├── rarity.ts           # 稀有度系统
├── animation.ts        # 动画引擎
└── buddy.tsx           # 主组件

Bridge(远程控制)

src/bridge/
├── websocket.ts        # WebSocket 连接
├── protocol.ts         # 通信协议
├── permissionCallbacks.ts  # 权限回调
├── statusUtil.ts       # 状态同步
└── ... (共 33 个文件)

Coordinator(多 Agent 编排)

src/coordinator/
├── coordinator.ts      # 协调器核心
└── worker.ts           # Worker 进程

SoC 体现

模块内高内聚

  • 所有与 Buddy 相关的代码都在 buddy/ 目录
  • 所有与 Bridge 相关的代码都在 bridge/ 目录

模块间低耦合

  • Buddy 模块不依赖 Bridge 模块
  • 通过清晰的接口与其他部分交互

可插拔设计

  • 通过 feature() 开关控制模块是否启用
  • 可以轻松添加/移除整个模块

6. 基础设施层 — 支撑系统

State(状态管理)

src/state/
├── store.ts            # 全局状态存储
├── actions.ts          # 状态变更动作
└── selectors.ts        # 状态选择器

Context(上下文管理)

src/context/
├── projectContext.ts   # 项目上下文
├── sessionContext.ts   # 会话上下文
└── ...

Constants(常量定义)

src/constants/
├── models.ts           # 模型常量
├── limits.ts           # 限制常量
├── colors.ts           # 颜色常量
└── ... (共 22 个文件)

Types(类型定义)

src/types/
├── tool.ts             # 工具类型
├── command.ts          # 命令类型
├── message.ts          # 消息类型
└── ... (共 16 个文件)

Utils(工具函数)

src/utils/
├── string.ts           # 字符串工具
├── file.ts             # 文件工具
├── date.ts             # 日期工具
└── ... (共 333 个文件)

SoC 体现

基础设施与业务逻辑分离

  • state/ 只提供状态管理机制,不包含业务逻辑
  • constants/ 只定义常量,不包含计算逻辑
  • types/ 只定义类型,不包含实现

工具函数纯净化

  • utils/ 中的函数都是纯函数
  • 无副作用,易于测试
  • 可以被任何模块复用

7. 入口层 — 应用启动

Entrypoints

src/entrypoints/
├── cli.ts              # CLI 入口
├── daemon.ts           # 守护进程入口
├── repl.ts             # REPL 入口
└── ...

Main

src/main.tsx            # 主应用入口(785KB)

SoC 体现

启动逻辑与业务逻辑分离

  • 入口文件只负责初始化和启动
  • 不包含具体业务逻辑
  • 便于测试不同的启动场景

Claude Code 的 SoC 实践总结

分层架构

┌─────────────────────────────────┐
│     Entrypoints (入口层)         │
│  - CLI / Daemon / REPL          │
└──────────────┬──────────────────┘

┌──────────────▼──────────────────┐
│     Commands (命令层)            │
│  - 87 个用户命令                 │
└──────────────┬──────────────────┘

┌──────────────▼──────────────────┐
│     Components + Hooks (UI 层)   │
│  - 148 个组件 + 87 个 Hooks      │
└──────────────┬──────────────────┘

┌──────────────▼──────────────────┐
│     Services (服务层)            │
│  - API / MCP / Analytics 等      │
└──────────────┬──────────────────┘

┌──────────────▼──────────────────┐
│     Tools (工具层)               │
│  - 53 个原子能力                 │
└──────────────┬──────────────────┘

┌──────────────▼──────────────────┐
│     Infrastructure (基础设施)     │
│  - State / Types / Utils 等      │
└─────────────────────────────────┘

关键设计原则

1. 单一职责

  • 每个文件/目录只做一件事
  • BashTool 只执行命令,不处理权限
  • compact 命令只响应用户,不执行压缩逻辑

2. 依赖倒置

  • 高层模块不依赖低层模块,都依赖抽象
  • 服务通过接口注入依赖
  • 便于替换实现

3. 开闭原则

  • 对扩展开放,对修改关闭
  • 新增工具:添加新目录,不改现有代码
  • 新增命令:添加新文件,不改现有命令

4. 接口隔离

  • 每个模块暴露最小化的接口
  • 工具层提供统一的 execute() 接口
  • 服务层通过明确的 API 通信

5. 组合优于继承

  • 通过组合多个小模块构建大功能
  • Buddy 模块由 species + rarity + animation 组合
  • 命令由多个服务和工具组合而成

对比:如果没有 SoC 会怎样?

假设所有代码都写在一个文件中:

// ❌ 反例:所有代码混在一起
function main() {
  // 解析命令行参数
  const args = parseArgs();
  
  // 执行 bash 命令
  const result = execSync(args.cmd);
  
  // 读取文件
  const content = fs.readFileSync('file.txt');
  
  // 调用 API
  const response = fetch('https://api.example.com');
  
  // 渲染 UI
  console.log(renderUI(response));
  
  // 保存状态
  saveState({ result, content, response });
  
  // 上报分析
  analytics.track('command_executed');
}

问题

  • ❌ 难以理解:1000+ 行代码混在一起
  • ❌ 难以测试:无法单独测试某个功能
  • ❌ 难以维护:修改一处可能影响多处
  • ❌ 难以扩展:新增功能需要修改主函数
  • ❌ 难以复用:代码耦合在一起,无法单独使用

如何在自己的项目中实践 SoC?

1. 识别关注点

问自己:

  • 这个模块的核心职责是什么?
  • 它依赖哪些其他模块?
  • 哪些模块依赖它?

2. 划清边界

  • 为每个关注点创建独立的目录
  • 定义清晰的接口
  • 避免循环依赖

3. 提取公共逻辑

  • 将重复代码提取到 utils/
  • 将共享状态放到 state/
  • 将常量定义放到 constants/

4. 使用依赖注入

// ❌ 硬编码依赖
class UserService {
  private db = new Database();  // 紧耦合
}

// ✅ 依赖注入
class UserService {
  constructor(private db: Database) {}  // 松耦合
}

5. 编写单元测试

  • 每个模块都应该可以独立测试
  • 如果难以测试,说明耦合度过高
  • 重构直到可以轻松测试

6. 定期重构

  • 发现违反 SoC 的代码及时重构
  • 保持目录结构清晰
  • 文档化模块间的依赖关系

实际案例:添加一个新功能

假设我们要在 Claude Code 中添加一个”代码审查”功能。

按照 SoC 原则的实现步骤:

1. 创建工具(如果需要)

src/tools/CodeReviewTool/
├── index.ts            # 工具实现
├── types.ts            # 类型定义
└── utils.ts            # 辅助函数

2. 创建服务

src/services/codeReview/
├── analyzer.ts         # 代码分析逻辑
├── reporter.ts         # 报告生成
└── cache.ts            # 缓存管理

3. 创建命令

src/commands/review/
├── index.ts            # 命令入口
└── options.ts          # 选项解析

4. 创建 UI 组件(如果需要)

src/components/CodeReview/
├── ReviewPanel.tsx     # 审查面板
├── IssueList.tsx       # 问题列表
└── DiffViewer.tsx      # 差异查看器

5. 注册和集成

// 在 commands.ts 中注册
if (feature('CODE_REVIEW')) {
  registerCommand('review', reviewCommand);
}

// 在 tools.ts 中注册
registerTool('code_review', CodeReviewTool);

关键点

  • ✅ 没有修改现有代码
  • ✅ 新功能完全独立
  • ✅ 可以通过 feature gate 控制
  • ✅ 可以单独测试每个部分

总结

通过分析 Claude Code 的源码,我们看到了关注点分离原则在大型项目中的实际应用:

核心要点

  1. 分层清晰:入口 → 命令 → UI → 服务 → 工具 → 基础设施
  2. 职责单一:每个模块只做一件事
  3. 低耦合:模块之间通过接口通信
  4. 高内聚:相关功能放在同一目录
  5. 可插拔:通过 feature gate 控制功能启用
  6. 易扩展:新增功能无需修改现有代码

带来的好处

  • 📖 可读性:新人可以快速理解代码结构
  • 🧪 可测试性:每个模块可以独立测试
  • 🔧 可维护性:修改一个模块不影响其他模块
  • 🚀 可扩展性:轻松添加新功能
  • 🔄 可复用性:模块可以在不同场景复用

实践建议

  1. 从小处开始:不要试图一次性重构整个项目
  2. 持续改进:每次添加新功能时,遵循 SoC 原则
  3. 代码审查:在 PR 中检查是否违反 SoC
  4. 文档化:记录模块的职责和依赖关系
  5. 定期重构:技术债务要及时清理

关注点分离不是一蹴而就的,而是一个持续的过程。Claude Code 的源码为我们提供了一个优秀的参考范例,希望这篇文章能帮助你在自己的项目中更好地实践这一原则。

相关资源