从 Claude Code 源码学习关注点分离(SoC)原则
引言
在软件开发中,关注点分离(Separation of Concerns, SoC)是最基础也是最重要的设计原则之一。它的核心思想是:将程序划分为不同的部分,每个部分只负责一个特定的功能或”关注点”。
今天,我们通过分析 Claude Code 的源码结构,看看一个成熟的工业级项目是如何实践这一原则的。
什么是关注点分离?
关注点分离原则要求我们将系统中的不同职责分开,使得:
- 每个模块只做一件事,并且做好
- 模块之间低耦合,通过清晰的接口通信
- 高内聚,相关的功能放在一起
- 易于理解和维护,修改一个关注点不影响其他部分
好处:
- ✅ 代码更易读、易理解
- ✅ 便于单元测试
- ✅ 降低修改风险
- ✅ 支持并行开发
- ✅ 提高代码复用性
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>;
}
为什么这样设计?
- 可扩展性:新增工具只需添加一个新目录
- 可测试性:每个工具可以独立单元测试
- 可组合性:Agent 可以灵活组合多个工具
- 权限控制:可以精细控制每个工具的访问权限
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/:只负责渲染 UIhooks/:只负责业务逻辑和状态管理
示例:
// 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 的源码,我们看到了关注点分离原则在大型项目中的实际应用:
核心要点
- 分层清晰:入口 → 命令 → UI → 服务 → 工具 → 基础设施
- 职责单一:每个模块只做一件事
- 低耦合:模块之间通过接口通信
- 高内聚:相关功能放在同一目录
- 可插拔:通过 feature gate 控制功能启用
- 易扩展:新增功能无需修改现有代码
带来的好处
- 📖 可读性:新人可以快速理解代码结构
- 🧪 可测试性:每个模块可以独立测试
- 🔧 可维护性:修改一个模块不影响其他模块
- 🚀 可扩展性:轻松添加新功能
- 🔄 可复用性:模块可以在不同场景复用
实践建议
- 从小处开始:不要试图一次性重构整个项目
- 持续改进:每次添加新功能时,遵循 SoC 原则
- 代码审查:在 PR 中检查是否违反 SoC
- 文档化:记录模块的职责和依赖关系
- 定期重构:技术债务要及时清理
关注点分离不是一蹴而就的,而是一个持续的过程。Claude Code 的源码为我们提供了一个优秀的参考范例,希望这篇文章能帮助你在自己的项目中更好地实践这一原则。
相关资源: