mcpField
C
clean-architecture

clean-architecture

github.com/nathankim0开发10859 次安装1 周前更新
代码仓库

关于 clean-architecture

这项技能依据 Robert C. Martin 的 Clean Architecture 原则审查代码并指导设计,适合需要检查分层、依赖方向或规划功能实现的开发者。它涵盖边界检查与机械化验证。

主要功能

  1. 01审查分层与依赖方向
  2. 02检查端口、适配器和 DTO 边界
  3. 03评估 SOLID 原则
  4. 04指导替换模拟适配器
  5. 05用搜索工具验证导入关系

适用场景

  1. 01检查功能是否违反依赖规则
  2. 02规划跨领域层与展示层的功能
  3. 03确认模拟后端能否平滑替换

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nathankim0/clean-architecture-skills --skill clean-architecture

安装后在 Claude Code 中输入/clean-architecture确认已加载。

详细介绍

整洁架构审查技能

概述

此技能根据 Robert C. Martin(Uncle Bob)的整洁架构原则分析和审查代码。

适用场景

  • 用户请求审查“整洁架构”时
  • 用户询问“架构审查”“依赖检查”或“分层”时
  • 使用整洁架构设计新功能时
  • 将某个功能拆分为领域、数据、展示和依赖注入模块时,或将伪实现替换为真实后端时

说明

1. 项目结构分析

首先了解项目的目录结构:

  • 检查各层是否分离
  • 检查模块/包结构
  • 确认依赖方向
  • 找到一个已遵循项目分层方式的现有功能,并沿用其约定(文件名、端口形式、注入实现的方式),而不是另创一种风格

设计而非审查时,在编写代码前先确定以下事项:

  1. 哪些规则由此应用负责,哪些由外部权威方(服务端、支付提供商、广告网络)决定?
  2. 内层需要哪些端口,每个端口应包含哪些操作?
  3. 选择具体实现的组合根在哪里?
  4. 现在需要哪些伪适配器或模拟适配器,之后又将用什么替换它们?
2. 依赖规则

核心原则:源代码依赖只能指向内层(更高层级的策略)。

┌─────────────────────────────────────────────────────────┐
│                  Frameworks & Drivers                    │
│  ┌─────────────────────────────────────────────────┐    │
│  │              Interface Adapters                  │    │
│  │  ┌─────────────────────────────────────────┐    │    │
│  │  │              Use Cases                   │    │    │
│  │  │  ┌─────────────────────────────────┐    │    │    │
│  │  │  │           Entities              │    │    │    │
│  │  │  └─────────────────────────────────┘    │    │    │
│  │  └─────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘
                    ← Dependencies flow inward

检查清单:

  • 内层是否避免引用外层?
  • 实体是否不依赖用例、适配器和基础设施?
  • 用例是否避免直接引用适配器或基础设施?
  • 跨越边界时是否应用了 DIP?
  • Presenter、视图模型、控制器和 UI 组件是否避免导入组合根或 DI 容器?
2-0. 组合根是最外层

选择具体实现的模块(DI 容器、服务定位器、provide* 工厂、应用入口)依赖所有内容,因此内部任何部分都不能依赖它。

  • 在边界处的单一位置(应用入口、根组件、main)连接实现,并通过构造函数、参数或 context/provider 向内传递
  • 内层模块调用 DI 模块来获取自身依赖,相当于将具体依赖隐藏在函数调用后面;即使没有导入具体类,这仍然违反依赖规则
  • 将伪实现替换为真实实现时,只需修改组合根
// Bad: the view model reaches out to the container
class InventoryViewModel {
  private repository = AppContainer.inventoryRepository();
}

// Good: the composition root decides and injects
class InventoryViewModel {
  constructor(private repository: InventoryRepository) {}
}

const viewModel = new InventoryViewModel(createInventoryRepository(config));
2-1. 跨越边界

控制流和源代码依赖可以指向相反的方向。

Controller → Use Case → Presenter
     ↓           ↓           ↑
[dependency] [dependency] [inverted!]

示例:用例需要调用 Presenter 时

  • 用例直接引用 Presenter 会违反依赖规则
  • 解决方案:在用例内部定义输出端口(接口),由 Presenter 实现该接口
// Use Case Layer (inner)
interface OrderOutputPort {
  presentOrder(order: OrderData): void;
}

class CreateOrderUseCase {
  constructor(private outputPort: OrderOutputPort) {}
  execute() {
    // ... business logic
    this.outputPort.presentOrder(orderData);
  }
}

// Adapter Layer (outer) - depends on Use Case
class OrderPresenter implements OrderOutputPort {
  presentOrder(order: OrderData): void {
    // Transform for UI
  }
}
2-2. 跨越边界的数据

原则:跨越边界的数据应为简单的数据结构(DTO)。

// Bad: Passing Entity directly
class GetUserUseCase {
  constructor(private userRepository: UserRepository) {}
  execute(id: string): Promise<User> { // Returning Entity directly
    return this.userRepository.findById(id);
  }
}
// Good: Transform to DTO
class GetUserUseCase {
  constructor(private userRepository: UserRepository) {}
  async execute(id: string): Promise<UserResponseDTO> {
    const user = await this.userRepository.findById(id);
    return { id: user.id, name: user.name };
  }
}

检查清单:

  • 是否避免让实体或数据库行直接跨越边界?
  • 是否避免让外部框架的数据格式渗入内层?
  • 边界数据结构是否服务于内层,同时避免为每一层都添加冗余 DTO?
3. 分层结构审查
实体(领域层)
  • 包含核心业务规则
  • 独立于外层框架、持久化和 UI
  • 最不容易发生变化

检查清单:

  • 实体是否避免依赖框架?
  • 是否只包含纯业务逻辑?
  • 导入的库是否能保持领域独立性,而不是引入基础设施方面的关注点?
  • 领域是否避免重新实现由外部权威方负责的规则?(见 3-1)
  • 是否避免仅用于展示的规则,例如显示阈值、文案、颜色或格式?
3-1. 谁拥有策略?

当外部权威决定一项规则(配额、资格、定价、奖励金额、过期时间)时,客户端不得在自己的领域中重复实现该规则。重复的规则会逐渐偏离:权威修改规则后,客户端仍会显示过时的数值,直到下次发布。

  • 让端口直接返回决策结果(remainingToday、nextGrantAmount、expiresAt),而不是返回客户端需要用来计算结果的原始输入
  • 仅在领域中保留应用真正拥有的规则
  • 如果权威尚不存在,就将模拟策略放在代表该权威的 fake 适配器中,而不是领域中
3-2. 表现规则应留在表现层

纯函数不一定就是领域规则。“当某个条目将在 3 天内过期时显示警告”是一项显示决策;将 3 改为 2 不会改变任何业务结果。此类阈值、标签和倒计时格式应保留在 presenter 或视图模型中,只有当某个业务流程(例如通知任务)也需要同一规则时,才将其提升到领域层。

用例(应用层)
  • 特定于应用的业务规则
  • 编排实体以实现功能
  • 定义外部系统的接口

检查清单:

  • 每个用例是否只负责一项职责?
  • 输入/输出端口是否定义清晰?
  • 是否使用仓库接口(而非具体实现)?
接口适配器
  • 负责数据转换
  • 包含控制器、Presenter、网关
  • 可应用 MVC、MVVM 等模式

检查清单:

  • DTO 与实体之间的转换是否在此处进行?
  • 外部格式与内部格式的界限是否清晰?
  • UI 组件是否足够精简,状态转换、锁定、取消和副本选择是否已移至 Presenter、控制器或视图模型中,以便无需查看 UI 也能理解?
框架与驱动程序(基础设施层)
  • 数据库、Web 框架、外部服务
  • 主要是胶水代码
  • 最有可能发生变化

检查清单:

  • 是否将框架和数据库细节排除在实体和用例之外?
  • 仓库实现是否依赖于内层的端口,无论这些实现放在适配器层还是基础设施层?
  • 持久化格式或线路格式(序列化器、由架构生成的类型)是否与实体分离,并在适配器中进行显式映射?
3-3. Fake 和 Mock 都是适配器

Fake 后端、内存存储或 Mock SDK 都是同一端口在外层的实现。

  • 将其放在真实适配器旁边,绝不要放在领域层或 UI 中
  • 仅在组合根中选择它,以便无需修改内层代码即可替换为真实实现
  • 让它遵循与真实实现相同的契约,包括幂等性和失败结果,这样替换时调用方无需变更
3-4. 将调试操作移出生产端口

仅用于开发或预发布环境的重置、填充种子数据和时间回溯操作,不属于生产端口;否则每个真实实现都必须实现这些操作(ISP)。应将它们作为单独的可选能力公开,并仅由组合根在这些环境中提供。

3-5. 共享状态只设一个所有者

当多个屏幕或组件显示来自同一端口的数据时,应由一个所有者(适配器层中的 provider、store 或缓存)持有仓库实例及其状态,其他组件则订阅它。

缺少所有者的迹象:

  • 每个使用方都创建自己的仓库实例、加载器或计时器
  • 每个使用方都会各自发送一次相同的请求
  • 在一个屏幕上执行更改后,另一个屏幕仍显示过时的数值,直到重新加载

在所有者中集中管理刷新触发条件(获取焦点、应用恢复、计划边界、更改之后),并将并发加载合并为一个请求。仅用于显示的计时器(例如持续跳动的倒计时)应保留在显示它们的组件中,避免共享状态的变化导致所有订阅者重新渲染。

3-6. 不要猜测适配器契约

如果真实的后端契约(API 规范、GraphQL schema、SDK 回调)尚不存在,不要基于臆造的契约编写“真实”适配器,也不要绕过生成的类型或手动编辑 schema 来让代码通过编译。

  • 保留端口和伪适配器;端口的形状取决于内部层的需求,而不是猜测的服务器响应
  • 将预期契约记录下来,交给后端团队:操作、字段、错误情况、幂等键
  • 契约发布后添加真实适配器,并在组合根中替换伪适配器
4. SOLID 原则审查

面向对象设计原则,用于实现整洁架构。DIP 对跨越边界至关重要。

原则描述与整洁架构的关系
SRP一个类应该只有一个变更原因分离各层职责
OCP对扩展开放,对修改关闭通过接口进行扩展
LSP子类型必须可以替代基类型实现可替换性
ISP不要依赖未使用的接口端口接口隔离
DIP依赖抽象,而非具体实现跨越边界的核心
5. 通过机械方式验证

不要依赖文件夹名称。使用项目的搜索工具列出每一层实际导入的内容,例如:

# Inner layers must not reference implementations, frameworks, or the composition root.
# Port interfaces such as `UserRepository` are expected in the domain, so search for
# implementation names and wiring instead of the word "Repository".
rg -n "Impl\b|Container|Module\.provide" src/domain
rg -n "Impl\b|AppContainer|Module\.provide" src/presentation src/ui
# Fakes must be referenced only by adapters and the composition root
rg -n "Fake|Mock" src --glob '!src/data/**' --glob '!src/di/**'

将模块名称调整为符合项目约定的名称,然后阅读每个搜索结果:匹配项只是线索,不是定论。

报告每项违规时,注明文件和行号、内部模块、其依赖到的外部模块,以及用于修复的边界。

输出格式

请按以下格式提供审查结果:

# Clean Architecture Review Report

## Summary
- Overall Assessment: [Good/Needs Improvement/Critical]
- Key findings summary

## Dependency Rule Analysis
- List of violations
- Improvement suggestions

## Layer Structure Review
### Entities: [Good/Needs Improvement]
### Use Cases: [Good/Needs Improvement]
### Interface Adapters: [Good/Needs Improvement]
### Infrastructure: [Good/Needs Improvement]

## Recommendations
1. High priority improvements
2. Medium priority improvements
3. Long-term suggestions

重要说明

整洁架构并非万能良方
  • 并非所有项目都必须恰好分为 4 层
  • 只要遵循依赖规则,层数就可以灵活调整
  • 根据项目规模和复杂度应用
应避免的常见陷阱
  1. 过度执着于分层:分离各层本身并非目标
  2. 过度抽象:没有有用边界却添加接口;当一个实现就足以通过端口将业务规则与基础设施隔离时,无需更多实现
  3. 流于形式地采用:依赖 IDE 重构,却忽视实际的依赖规则
  4. 为未来过度设计:为不太可能发生的变更添加复杂性
关键问题

审查时始终考虑以下问题:

  • “这种分离真的能带来好处吗?”
  • “是否真正遵循了依赖规则?”
  • “发生变更时,影响范围是否最小化?”
实际验证

问问自己,整洁架构是否真的带来了好处:

  • 带来帮助的是分层,还是仅仅 IDE 自动重构?
  • 无论实现数量多少,这个 Repository 接口是否都能将业务规则与基础设施隔离?
  • 你是否在为未来发生概率仅为 0.001% 的变更过度抽象?
  • 是否可以重命名外部模块中的类,而不影响内部模块?

特定语言示例

TypeScript/JavaScript
// Good: Dependency Inversion
interface UserRepository {
  findById(id: string): Promise<User>;
}

class GetUserUseCase {
  constructor(private userRepository: UserRepository) {}
}

// Bad: Depending on concrete implementation
class GetUserUseCase {
  constructor(private userRepository: PostgresUserRepository) {}
}
函数式语言(OCaml、ReScript、F#、Elm 风格记录)

端口不需要类。函数记录就是端口,而组合根负责构建它。

(* Domain: port as a record of functions *)
type inventory_repository = {
  load : unit -> inventory Lwt.t;
  reserve : request_id:string -> (reservation, failure) result Lwt.t;
}

(* Data: one adapter per backend, each returning the same record *)
let fake_inventory ~store = { load = ...; reserve = ... }
let http_inventory ~client = { load = ...; reserve = ... }

(* Composition root: the only place that chooses *)
let inventory = if config.use_fake then fake_inventory ~store else http_inventory ~client
Python
# Good: Interface definition using Protocol
from typing import Protocol

class UserRepository(Protocol):
    def find_by_id(self, id: str) -> User: ...

# Bad: Direct implementation import
from infrastructure.postgres import PostgresUserRepository
Java/Kotlin
// Good: Interface segregation
interface UserRepository {
    fun findById(id: String): User?
}

class GetUserUseCase(
    private val userRepository: UserRepository
)

// Bad: Using concrete implementation
class GetUserUseCase(
    private val userRepository: JpaUserRepository
)

参考资料

评论

支持 Markdown