mcpField
C
code-architecture

code-architecture

github.com/curiositech开发257157 次安装2 天前更新
代码仓库

关于 code-architecture

code-architecture 是帮助开发者组织单体应用内部代码的架构技能,适用于新项目选型和现有代码诊断。它介绍分层、六边形、功能导向等模式,并讨论依赖反转、模块边界与测试布局。

主要功能

  1. 01清洁与六边形架构模式
  2. 02功能导向的代码组织
  3. 03依赖反转与注入
  4. 04模块边界和公共 API
  5. 05循环依赖诊断
  6. 06按架构层规划测试

适用场景

  1. 01为新项目选择代码架构
  2. 02整理不断增长的应用模块
  3. 03排查控制器逻辑过重或循环依赖
  4. 04调整测试以隔离业务逻辑

安装

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

$npx skills add https://github.com/curiositech/some_claude_skills --skill code-architecture

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.claude-plugin/
plugin.json
references/
architecture-patterns.md
dependency-inversion.md

详细介绍

代码架构

能够经受现实考验的架构模式和代码组织方式。目标是让代码在编写 18 个月后依然易于理解、测试和修改。本文涵盖模块和应用层面的决策——不涉及如何拆分服务,不涉及如何设计架构,而是介绍如何在单个可部署单元内组织代码。

适用场景

适用于:

  • 为新项目选择架构模式(整洁架构、六边形架构、按功能组织、纵向切片)
  • 诊断现有代码中的架构问题(臃肿的控制器、循环依赖、纠缠不清的业务逻辑)
  • 应用依赖倒置,解开紧密耦合的代码
  • 决定文件夹结构和模块组织方式
  • 在模块层面应用 SOLID 原则(而不仅仅是类层面)
  • 设置依赖注入容器
  • 定义模块边界和 barrel 导出
  • 根据架构层设计测试金字塔

不适用于:

  • 将单体拆分为微服务(请使用 microservices-patterns)
  • 数据库架构设计、规范化、查询优化(请使用 database-design-patterns)
  • 特定框架的路由或中间件设置
  • CI/CD 流水线架构

核心决策:选择哪种架构模式?

flowchart TD
    Start[New project or restructuring?] --> Team{Team size?}

    Team -->|Solo / 2 dev| Size{App complexity?}
    Team -->|3-8 dev| MedComplex{Domain complexity?}
    Team -->|8+ dev| Large[Feature-based / Vertical Slice]

    Size -->|Simple CRUD, low domain logic| Simple[Flat + MVC or Feature-Based]
    Size -->|Complex domain, long-lived| Hex[Hexagonal Architecture]

    MedComplex -->|Low: thin business layer| FeatureBased[Feature-Based Organization]
    MedComplex -->|High: rich business rules| Clean[Clean Architecture]
    MedComplex -->|Mixed: CRUD + some complex| Vertical[Vertical Slice Architecture]

    Simple --> SCheck{Will it grow?}
    SCheck -->|No, short-lived| MVC[Layer-based MVC is fine]
    SCheck -->|Yes| FeatureBased

    Large --> Testing{Testing rigor?}
    Testing -->|Low: mostly E2E| Vertical
    Testing -->|High: unit + integration| Clean

    Clean --> ClNote[Entities → Use Cases → Interface Adapters → Frameworks]
    Hex --> HexNote[Domain core ↔ Ports ↔ Adapters]
    FeatureBased --> FBNote[features/X contains all layers for X]
    Vertical --> VSNote[Each slice owns its full stack top to bottom]

整洁架构分层

经典的洋葱结构——每一环只依赖内侧,绝不依赖外侧。

graph TB
    subgraph Frameworks ["Frameworks & Drivers (outer)"]
        DB[(Database)]
        HTTP[HTTP Server]
        UI[UI / CLI]
        External[External APIs]
    end

    subgraph Adapters ["Interface Adapters"]
        Controllers[Controllers / Routes]
        Presenters[Presenters]
        Gateways[Repository Implementations]
        DTOs[DTOs / View Models]
    end

    subgraph UseCases ["Use Cases (Application Layer)"]
        UC1[CreateOrder]
        UC2[ProcessPayment]
        UC3[NotifyUser]
        Ports[Repository Interfaces]
    end

    subgraph Entities ["Entities (Domain)"]
        Order[Order]
        User[User]
        Payment[Payment]
        Rules[Business Rules]
    end

    HTTP --> Controllers
    Controllers --> UC1
    UC1 --> Ports
    Gateways --> Ports
    DB --> Gateways
    UC1 --> Order

依赖规则:源代码依赖只能指向内侧。实体不了解用例。用例不了解控制器。控制器不了解数据库驱动程序。

各层包含的内容:

层内容示例
实体企业业务规则Order、Payment、领域事件、值对象
用例特定于应用的业务逻辑CreateOrderUseCase、RefundPaymentUseCase
接口适配器在用例与外部形式之间转换数据控制器、Presenter、Repository 实现
框架外部工具、数据库、UI 框架Express、Postgres、React

六边形架构(端口与适配器)

对许多团队来说,它比整洁架构更容易理解:所有内容都通过端口(接口)连接,适配器负责实现这些端口。

graph LR
    subgraph Domain ["Domain (Hexagon)"]
        Logic[Business Logic]
        DPorts[Driven Ports<br/>interfaces the domain calls]
    end

    subgraph Driving ["Driving Adapters<br/>(what drives the app)"]
        HTTP2[HTTP Controller]
        CLI2[CLI Command]
        Test[Test Harness]
    end

    subgraph Driven ["Driven Adapters<br/>(what the app drives)"]
        DB2[(PostgreSQL Adapter)]
        Email[SendGrid Adapter]
        Cache[Redis Adapter]
    end

    HTTP2 -->|calls| Logic
    CLI2 -->|calls| Logic
    Test -->|calls| Logic
    Logic -->|calls port| DPorts
    DPorts -.->|implemented by| DB2
    DPorts -.->|implemented by| Email
    DPorts -.->|implemented by| Cache

六边形架构优于整洁架构的场景:需要轻松替换基础设施时(测试使用内存实现,生产环境使用 Postgres),或业务逻辑足够丰富、值得隔离,但不需要整洁架构完整分层时。


按功能组织与按层组织

按层组织(传统 MVC)
src/
  controllers/
    user.controller.ts
    order.controller.ts
    payment.controller.ts
  services/
    user.service.ts
    order.service.ts
    payment.service.ts
  repositories/
    user.repository.ts
    order.repository.ts
  models/
    user.model.ts
    order.model.ts

问题:添加或修改“orders”功能,需要改动四个目录中的文件。理解 orders 领域,需要在不同文件夹之间来回切换上下文。

按功能组织(适用于不断扩展的应用)
src/
  features/
    orders/
      orders.controller.ts
      orders.service.ts
      orders.repository.ts
      orders.schema.ts
      orders.types.ts
      orders.test.ts
      index.ts           ← public API (barrel export)
    users/
      users.controller.ts
      users.service.ts
      ...
      index.ts
    payments/
      ...
  shared/
    database/
    logger/
    config/

优点:所有与订单相关的代码都集中在一起。删除某个功能只需删除一个文件夹。让开发者熟悉 orders 领域,只需查看一个目录。

规则:功能只能从 shared/ 和彼此的公开 index.ts 导入内容。绝不能访问彼此的内部实现。


反模式:将业务逻辑放在控制器中(臃肿的控制器)

新手:“请求是通过控制器进来的,所以我也把逻辑放在那里。这样很方便。”

// Fat controller — don't do this
app.post('/orders', async (req, res) => {
  const { userId, items } = req.body;

  // Business logic #1: calculate total
  const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);

  // Business logic #2: apply discount
  const user = await db.users.findById(userId);
  const discount = user.memberSince < oneYearAgo ? 0.1 : 0;
  const finalTotal = total * (1 - discount);

  // Business logic #3: check inventory
  for (const item of items) {
    const stock = await db.inventory.findById(item.productId);
    if (stock.quantity < item.qty) {
      return res.status(409).json({ error: 'Out of stock' });
    }
  }

  // Side effects mixed in
  const order = await db.orders.create({ userId, items, total: finalTotal });
  await emailService.send(user.email, 'order-confirmation', { order });
  await inventory.decrement(items);

  res.json(order);
});

专家:控制器是 HTTP 适配器。它们将 HTTP 转换为领域对象,调用用例或服务,然后将领域对象转换为 HTTP 响应。所有业务逻辑都应属于用例或领域服务,这样无需 HTTP 上下文也能进行测试。

// Lean controller
app.post('/orders', async (req, res) => {
  try {
    const command = CreateOrderCommand.fromRequest(req.body);  // Validate + map
    const order = await createOrderUseCase.execute(command);   // All logic here
    res.status(201).json(OrderPresenter.toJSON(order));        // Map to response
  } catch (error) {
    errorHandler(res, error);
  }
});

// Use case: testable, framework-agnostic
class CreateOrderUseCase {
  constructor(
    private readonly orderRepo: OrderRepository,
    private readonly inventoryService: InventoryService,
    private readonly discountPolicy: DiscountPolicy,
    private readonly notifications: NotificationPort,
  ) {}

  async execute(command: CreateOrderCommand): Promise<Order> {
    const user = await this.orderRepo.findUser(command.userId);
    const discount = this.discountPolicy.calculate(user);
    await this.inventoryService.reserveItems(command.items);
    const order = Order.create(command.items, discount);
    await this.orderRepo.save(order);
    await this.notifications.orderCreated(order, user);
    return order;
  }
}

识别方式:逻辑超过约 20 行的控制器、直接导入数据库模型的控制器、包含嵌套 if-else 业务条件的控制器。


反模式:架构太空人(为了抽象而抽象)

新手:“我要添加一个 Repository 接口、一个 Repository 实现、一个 Service、一个 ServiceInterface、一个用于创建 Service 的 Factory、一个 EventBus,以及一个 CQRS 命令处理器。这就是企业级架构。”

专家:每种抽象都有成本:更多文件、更多间接层、更难上手、更多维护工作。抽象是一种投资,只有在能够支持测试、替换或代码复用时才能带来回报。如果你添加了一个只有单一实现且永远不会改变的 UserServiceInterface,就等于付出了抽象成本,却没有获得收益。

不妨问问:“这种抽象能让我做到什么原本做不到的事?”

  • Repository 接口 → 在测试中用内存数据库替换真实数据库。立刻见效。
  • Service 接口 → 如果始终只有一个 Service,这只是在走形式。
  • Factory 模式 → 对象创建复杂或存在多种策略时才有回报。
  • Event bus → 许多组件需要响应领域事件,但彼此无需了解对方时才有回报。

检测:名为 *Interface.ts、*Abstract.ts、*Factory.ts 的文件只有一个实现者和一个调用方,并且该实现者从未改变。

时间线:企业 Java(2005-2015)将“凡事都抽象”变成了默认做法。Spring Framework 助长了这种做法。2015 年之后,Node.js 和 Go 社区以“无聊技术”原则作出反击。到了 2026 年,合适的抽象程度取决于具体情况——既不是零,也不是最大化。


反模式:循环依赖

新手:“Order 模块需要了解 Users,而 User 模块需要检查用户的订单。所以我要让它们互相导入。”

// orders/order.service.ts
import { UserService } from '../users/user.service';   // Order → User

// users/user.service.ts
import { OrderService } from '../orders/order.service'; // User → Order

// Node.js will silently give you `undefined` at runtime
// Jest will give you cryptic "Cannot access before initialization" errors

专家:循环依赖表明领域建模存在问题。两个确实需要彼此的模块,要么合并成一个模块,要么共用一个双方都依赖的第三模块,要么通过事件或接口通信。

解决策略:

  1. 合并:如果 Order 和 User 确实密不可分,就把它们放进 accounts/
  2. 提取共享模块:创建双方都可以导入的 order-summary/
  3. 通过接口反转依赖:User 模块定义 OrderSummaryPort 接口;Orders 实现该接口;User 永远不导入 Orders
  4. 事件驱动:User 响应 OrderCreated 事件,而不是直接调用 OrderService
# Detect circular deps
npx madge --circular src/
npx dpdm --circular src/index.ts

# ESLint rule (add to .eslintrc)
# "import/no-cycle": "error"

检测:启动时模块值为 undefined 的运行时错误、违反 ESLint import/no-cycle 规则的情况,以及 madge 输出的循环依赖。


实践中的依赖倒置

SOLID 中的 D:依赖抽象,而非具体实现。在模块层面上的应用:

// Bad: Use case is coupled to Postgres
class CreateOrderUseCase {
  constructor(private readonly db: PostgresConnection) {}

  async execute(cmd: CreateOrderCommand) {
    await this.db.query('INSERT INTO orders ...');
  }
}

// Good: Use case depends on an interface
interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: OrderId): Promise<Order | null>;
}

class CreateOrderUseCase {
  constructor(private readonly orderRepo: OrderRepository) {}

  async execute(cmd: CreateOrderCommand) {
    const order = Order.create(cmd);
    await this.orderRepo.save(order);  // No SQL, no Postgres, no coupling
  }
}

// Production: Postgres implements the interface
class PostgresOrderRepository implements OrderRepository {
  async save(order: Order): Promise<void> { /* Postgres SQL */ }
  async findById(id: OrderId): Promise<Order | null> { /* Postgres SQL */ }
}

// Tests: in-memory implements the same interface
class InMemoryOrderRepository implements OrderRepository {
  private orders = new Map<string, Order>();
  async save(order: Order) { this.orders.set(order.id, order); }
  async findById(id: OrderId) { return this.orders.get(id) ?? null; }
}

DI 容器适用的情况:依赖项很多,需要进行装配,且手动装配容易出错或重复时。NestJS、InversifyJS(TypeScript)、Python 的 dependency-injector、Spring(Java)。

DI 容器用力过猛的情况:简单脚本、依赖项较少的小型服务、无服务器函数、Go 项目(构造函数注入符合惯例且已足够)。


模块边界与 Barrel 导出

每个功能/模块都应通过 index.ts 暴露公共 API:

// features/orders/index.ts — public API
export { CreateOrderUseCase } from './create-order.use-case';
export { OrderRepository } from './order.repository.interface';
export type { Order, OrderStatus } from './order.entity';
// NOT exported: internal helpers, SQL queries, implementation details

// Other modules import from the public API only
import { CreateOrderUseCase } from '@/features/orders';
// NOT: import from '@/features/orders/create-order.use-case'

使用 ESLint 强制执行:

// .eslintrc
{
  "rules": {
    "import/no-internal-modules": ["error", {
      "allow": ["**/*.test.ts", "**/index.ts"]
    }]
  }
}

测试架构(测试金字塔中的位置)

每个架构层都有适合的测试类型:

层测试类型速度覆盖率
实体 / 领域单元测试即时90%+
用例 / 应用使用模拟对象的单元测试快80%+
接口适配器集成测试中等70%+
框架 / 外部E2E / 契约测试慢关键路径

规则:业务逻辑测试不应要求启动服务器、连接数据库或发起网络调用。如果你的用例测试需要真实数据库,说明基础设施关注点已经渗入架构中的领域。


参考资料

  • references/architecture-patterns.md — 查看详细模式:整洁架构分层、六边形架构的端口与适配器、垂直切片、基于功能的组织方式及示例
  • references/dependency-inversion.md — 查看 TypeScript 和 Python 中的 IoC 容器、构造函数注入模式、DI 何时属于过度设计,以及常见的 DI 反模式

评论

支持 Markdown