普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月16日首页

整洁架构三连问:是什么,怎么做,为什么要用

作者 暖阳_
2026年4月15日 23:48

整洁架构问答

Q1:什么是整洁架构?

由 Robert C. Martin(Uncle Bob)提出,核心思想:业务逻辑独立于框架、UI、数据库等外部细节,依赖关系只能从外层指向内层。

四层结构

┌─────────────────────────────────┐
   Frameworks & Drivers             Web数据库UI(最易变)
   ┌─────────────────────────┐   
     Interface Adapters           控制器网关Presenter
     ┌───────────────────┐     
       Application              用例(Use Cases)
       Business Rules        
       ┌─────────────┐      
        Enterprise           实体(Entities)最稳定
        Business          
        Rules             
       └─────────────┘      
     └───────────────────┘     
   └─────────────────────────┘   
└─────────────────────────────────┘
  1. 实体层:核心业务规则,零依赖
  2. 用例层:编排实体完成应用逻辑
  3. 接口适配层:数据转换,连接内外
  4. 框架与驱动层:具体技术实现

关键收益

  • 可测试:业务逻辑不依赖外部,独立单元测试
  • 可替换:换数据库/框架不影响核心逻辑
  • 可维护:修改外层不影响内层

一句话:让业务逻辑成为核心,让数据库、框架等成为可插拔的细节。


Q2:整洁架构和普通架构有什么区别?

普通架构按技术类型分层,整洁架构按依赖方向分层。

直观对比

普通架构(传统分层)
src/
├── components/    ← UI 组件
├── api/           ← 接口调用
├── store/         ← 状态管理
├── utils/         ← 工具函数
└── views/         ← 页面

依赖方向:随意交叉

组件 ←→ store ←→ api
 ↕        ↕       ↕
utils ←─ views ←─ components
整洁架构
src/
├── domain/           ← 纯业务(零依赖)
├── application/      ← 用例编排
├── infrastructure/   ← 技术实现
└── ui/               ← 界面渲染

依赖方向:只从外向内

ui → application → domain
infrastructure → application → domain
         ↘          ↓          ↗
          任何外层都可以依赖内层
          内层永远不知道外层存在

具体差异对比

维度 普通架构 整洁架构
分层依据 按技术类型(组件、API、工具) 按依赖方向(外层依赖内层)
业务逻辑位置 散落在组件、store、utils 集中在 domain 层
框架耦合 业务逻辑依赖 Vue/React domain 层零框架依赖
改数据库 改 api/ + store/ + 组件 只改 infrastructure/
换框架 几乎重写 只重写 ui/ 层
单元测试 需要 mock 框架、mount 组件 直接测纯 TS 函数
API 字段变了 改多处 只改 Repository 转换逻辑

代码对比:同一个需求

普通架构写法
// store/cart.ts
import axios from 'axios'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])

  async function addItem(productId: string) {
    // 业务规则散落在 store 里
    if (items.value.length >= 20) {
      throw new Error('购物车已满')
    }
    // 直接耦合 axios
    const res = await axios.post('/api/cart', { productId })
    // 直接操作原始 JSON
    items.value.push(res.data)
  }

  return { items, addItem }
})
<!-- CartPage.vue -->
<script setup>
import { useCartStore } from '@/store/cart'

const cartStore = useCartStore()

// 组件里也有业务逻辑
const canAdd = computed(() => cartStore.items.length < 20)
</script>

问题:业务规则在 store 和组件里都有,换框架全废,换 axios 要改 store。

整洁架构写法
// domain/aggregates/Cart.ts — 纯 TS,零依赖
export class Cart {
  private items: CartItem[] = []

  addItem(item: CartItem): void { /* 规则在这里 */ }
  canAddMore(): boolean { return this.items.length < 20 }
}
// application/usecases/AddToCartUseCase.ts — 编排
export class AddToCartUseCase {
  constructor(private repo: CartRepository) {}
  async execute(id: string) {
    const cart = await this.repo.getCart()
    cart.addItem(item)           // 调领域方法
    await this.repo.save(cart)   // 持久化
  }
}
// infrastructure/repositories/ApiCartRepository.ts — 适配
export class ApiCartRepository implements CartRepository {
  async getCart() { /* axios → 领域对象 */ }
  async save(cart) { /* 领域对象 → axios */ }
}
<!-- ui/pages/CartPage.vue — 只渲染 -->
<script setup>
const { addItem } = useCart()  // composable 调用用例
</script>

好处:业务规则只在 Cart.ts,换框架/换 axios 都不影响它。

什么时候用哪个

场景 建议
简单 CRUD、几个页面 普通架构够用,别过度设计
中等复杂度、有业务规则 提取 domain 层,用 composable 做应用层
复杂业务(交易、审批、计费) 完整整洁架构 + DDD

一句话总结:普通架构按技术分文件夹,整洁架构按依赖方向分层。前者简单但业务逻辑散落各处,后者前期成本高但业务逻辑内聚、可替换、可测试。


Q3:目前 Vue CLI 是不是都是整洁架构?

不是。 Vue CLI 生成的项目默认不符合整洁架构

Vue CLI 默认结构

src/
├── components/    ← 组件(UI + 业务逻辑混在一起)
├── views/         ← 页面(UI + 业务逻辑混在一起)
├── router/        ← 路由
├── store/         ← Vuex/Pinia(状态管理)
├── api/           ← API 调用
├── utils/         ← 工具函数
└── App.vue

为什么不是整洁架构

整洁架构要求 Vue CLI 默认 问题
依赖从外指向内 各层互相 import,方向混乱 无依赖规则约束
领域层纯 TS,零框架依赖 业务逻辑写在 .vue 领域层耦合了 Vue
用例层编排领域对象 没有 use case 层 业务逻辑散落在组件和 store
Repository 接口在领域层定义 直接在组件里调 axios 数据获取和 UI 耦合

典型 Vue 组件的问题

<script setup>
// ❌ UI、业务规则、API 调用全混在一起
import axios from 'axios'

const cart = ref([])

async function addItem(id) {
  if (cart.value.length >= 20) {       // 业务规则在组件里
    alert('购物车已满')
    return
  }
  const res = await axios.post('/api/cart', { id })  // 直接调 API
  cart.value.push(res.data)
}
</script>

Vue CLI 能改成整洁架构吗

能,但需要手动改造,Vue CLI 不会自动帮你分层:

src/
├── domain/           ← 手动添加:纯 TS 领域层
│   ├── value-objects/
│   ├── entities/
│   ├── aggregates/
│   └── repositories/    ← 接口定义
├── application/      ← 手动添加:用例层
│   ├── usecases/
│   └── composables/
├── infrastructure/   ← 手动添加:适配层
│   ├── repositories/    ← Repository 实现
│   └── api/
├── ui/               ← 原 components/views,只负责渲染
│   ├── components/
│   └── pages/
├── router/
└── App.vue

一句话总结

Vue CLI 是脚手架工具,只管项目初始化和构建配置,不管架构分层。默认生成的项目是传统分层(按技术类型分),不是整洁架构(按依赖方向分)。整洁架构需要开发者自己设计和实施。

前端整洁架构详解

作者 暖阳_
2026年4月15日 22:52

前端整洁架构详解

以电商购物车系统为例,逐层讲解整洁架构在前端中的落地方式。


架构分层总览

┌──────────────────────────────────┐
│  UI 框架层(Vue/React 组件)      │  ← 最外层:页面、组件
│  ┌────────────────────────────┐  │
│  │  接口适配层                 │  │  ← Store/Composable、API 适配、Repository 实现
│  │  ┌────────────────────┐    │  │
│  │  │  应用层             │    │  │  ← 用例:编排领域逻辑
│  │  │  ┌────────────┐    │    │  │
│  │  │  │  领域层     │    │    │  │  ← 纯 JS/TS:实体、值对象、规则
│  │  │  └────────────┘    │    │  │
│  │  └────────────────────┘    │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

依赖规则:依赖关系只能从外层指向内层,内层完全不知道外层的存在。


1. 领域层(纯 JS/TS,零依赖)

最内层,不引入任何框架,不 import Vue/React,不 import axios,纯业务逻辑。

1.1 值对象 — 封装校验和不可变概念

// domain/value-objects/Money.ts
export class Money {
  constructor(private amount: number, private currency: string = 'CNY') {
    if (amount < 0) throw new Error('金额不能为负数');
  }

  add(other: Money): Money {
    if (this.currency !== other.currency) throw new Error('币种不同');
    return new Money(this.amount + other.amount, this.currency);
  }

  multiply(factor: number): Money {
    return new Money(this.amount * factor, this.currency);
  }

  get value(): number { return this.amount; }
  toString(): string { return `${this.currency} ${this.amount.toFixed(2)}`; }
}
// domain/value-objects/Email.ts
export class Email {
  constructor(private value: string) {
    if (!/^[^\s@]+@[^\s@]+.[^\s@]+$/.test(value)) {
      throw new Error('邮箱格式不合法');
    }
  }

  get domain(): string { return this.value.split('@')[1]; }
  toString(): string { return this.value; }
}

为什么用值对象? 校验逻辑内聚,不会散落在各个组件里。任何地方拿到一个 Money,就保证它是合法的。

1.2 实体 — 有标识,有生命周期

// domain/entities/CartItem.ts
import { Money } from '../value-objects/Money';

export class CartItem {
  constructor(
    public readonly productId: string,  // 唯一标识
    public readonly name: string,
    private unitPrice: Money,
    private quantity: number,
  ) {
    if (quantity <= 0) throw new Error('数量必须大于0');
  }

  get totalPrice(): Money {
    return this.unitPrice.multiply(this.quantity);
  }

  changeQuantity(newQty: number): CartItem {
    return new CartItem(this.productId, this.name, this.unitPrice, newQty);
  }
}

1.3 聚合根 — 保证一致性边界

// domain/aggregates/Cart.ts
import { Money } from '../value-objects/Money';
import { CartItem } from '../entities/CartItem';

export class Cart {
  private items: CartItem[] = [];

  addItem(item: CartItem): void {
    const existing = this.items.find(i => i.productId === item.productId);
    if (existing) {
      // 已存在则合并数量
      this.items = this.items.map(i =>
        i.productId === item.productId
          ? i.changeQuantity(i.quantity + item.quantity)
          : i
      );
    } else {
      this.items.push(item);
    }
  }

  removeItem(productId: string): void {
    this.items = this.items.filter(i => i.productId !== productId);
  }

  get total(): Money {
    return this.items.reduce(
      (sum, item) => sum.add(item.totalPrice),
      new Money(0),
    );
  }

  get itemCount(): number {
    return this.items.length;
  }

  // 业务规则:购物车最多 20 件商品
  canAddMore(): boolean {
    return this.items.length < 20;
  }
}

关键点:所有业务规则都在这里——最多 20 件、合并同类商品、金额计算。组件不需要知道这些规则。

1.4 Repository 接口(领域层定义)

// domain/repositories/CartRepository.ts
import { Cart } from '../aggregates/Cart';

export interface CartRepository {
  getCart(): Promise<Cart>;
  save(cart: Cart): Promise<void>;
  clear(): Promise<void>;
}

接口在领域层定义,实现在适配层——这是依赖反转的核心,让领域层不依赖具体的数据获取方式。


2. 应用层(用例 — 编排领域对象)

这一层只做编排,不包含业务规则本身。它协调领域对象完成一个完整操作。

2.1 添加商品到购物车

// application/usecases/AddToCartUseCase.ts
import { Cart } from '../../domain/aggregates/Cart';
import { CartItem } from '../../domain/entities/CartItem';
import { Money } from '../../domain/value-objects/Money';
import { CartRepository } from '../../domain/repositories/CartRepository';

export class AddToCartUseCase {
  constructor(private cartRepo: CartRepository) {}

  async execute(productId: string, name: string, price: number, qty: number): Promise<Cart> {
    // 1. 从仓库获取当前购物车
    const cart = await this.cartRepo.getCart();

    // 2. 业务规则检查(规则在领域对象里,用例只调用)
    if (!cart.canAddMore()) {
      throw new Error('购物车已满,最多 20 件商品');
    }

    // 3. 创建领域对象
    const item = new CartItem(productId, name, new Money(price), qty);

    // 4. 执行领域操作
    cart.addItem(item);

    // 5. 持久化
    await this.cartRepo.save(cart);

    return cart;
  }
}

2.2 结算下单

// application/usecases/CheckoutUseCase.ts
import { CartRepository } from '../../domain/repositories/CartRepository';
import { OrderRepository } from '../../domain/repositories/OrderRepository';
import { Order } from '../../domain/aggregates/Order';

export class CheckoutUseCase {
  constructor(
    private cartRepo: CartRepository,
    private orderRepo: OrderRepository,
  ) {}

  async execute(userId: string): Promise<Order> {
    const cart = await this.cartRepo.getCart();

    if (cart.itemCount === 0) {
      throw new Error('购物车为空,无法下单');
    }

    // 领域逻辑:创建订单
    const order = Order.createFromCart(cart, userId);

    await this.orderRepo.save(order);
    await this.cartRepo.clear();

    return order;
  }
}

用例层的特点:读数据 → 调领域方法 → 存数据。像导演一样编排,但不自己写业务规则。


3. 接口适配层(Store/Composable + API 适配)

3.1 Repository 实现(JSON ↔ 领域对象转换)

// infrastructure/repositories/ApiCartRepository.ts
import { CartRepository } from '../../domain/repositories/CartRepository';
import { Cart } from '../../domain/aggregates/Cart';
import { CartItem } from '../../domain/entities/CartItem';
import { Money } from '../../domain/value-objects/Money';
import { cartApi } from '../api/CartApi';

export class ApiCartRepository implements CartRepository {
  async getCart(): Promise<Cart> {
    // API 返回的是原始 JSON,需要转换成领域对象
    const raw = await cartApi.fetchCart();
    const cart = new Cart();
    for (const item of raw.items) {
      cart.addItem(new CartItem(item.productId, item.name, new Money(item.price), item.qty));
    }
    return cart;
  }

  async save(cart: Cart): Promise<void> {
    // 领域对象 → 原始 JSON,给 API
    const payload = {
      items: cart.items.map(i => ({
        productId: i.productId,
        name: i.name,
        price: i.unitPrice.value,
        qty: i.quantity,
      })),
    };
    await cartApi.updateCart(payload);
  }

  async clear(): Promise<void> {
    await cartApi.clearCart();
  }
}

关键:API 返回的 JSON → 领域对象的转换在这里完成。领域层永远不碰原始 JSON。

3.2 API 层(最外层细节)

// infrastructure/api/CartApi.ts
import axios from 'axios';

export const cartApi = {
  fetchCart: () => axios.get('/api/cart').then(r => r.data),
  updateCart: (data: any) => axios.put('/api/cart', data).then(r => r.data),
  clearCart: () => axios.delete('/api/cart').then(r => r.data),
};

3.3 Composable(连接用例和 UI)

// application/composables/useCart.ts
import { ref } from 'vue';
import { AddToCartUseCase } from '../usecases/AddToCartUseCase';
import { ApiCartRepository } from '../../infrastructure/repositories/ApiCartRepository';

const cartRepo = new ApiCartRepository();
const addToCartUseCase = new AddToCartUseCase(cartRepo);

export function useCart() {
  const cart = ref<Cart | null>(null);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function addItem(productId: string, name: string, price: number, qty: number) {
    loading.value = true;
    error.value = null;
    try {
      cart.value = await addToCartUseCase.execute(productId, name, price, qty);
    } catch (e) {
      error.value = e.message;
    } finally {
      loading.value = false;
    }
  }

  return { cart, loading, error, addItem };
}

4. UI 框架层(组件 — 只负责渲染)

<!-- ui/components/ProductCard.vue -->
<script setup lang="ts">
import { useCart } from '@/application/composables/useCart';

const { addItem, loading, error } = useCart();

function handleAdd() {
  addItem(product.id, product.name, product.price, 1);
}
</script>

<template>
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>¥{{ product.price }}</p>
    <button :disabled="loading" @click="handleAdd">
      加入购物车
    </button>
    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

组件只做三件事:展示数据、捕获用户操作、调用用例。零业务逻辑。


依赖关系总览

ProductCard.vue          ← 只渲染,零业务逻辑
  ↓ 调用
useCart()                ← 状态管理 + 调用用例
  ↓ 调用
AddToCartUseCase         ← 编排领域对象
  ↓ 使用
Cart / CartItem / Money  ← 纯业务规则
  ↓ 通过接口
CartRepository (接口)    ← 领域层定义接口
  ↓ 实现
ApiCartRepository        ← 适配层实现,转换 JSON ↔ 领域对象
  ↓ 调用
cartApi (axios)          ← 最外层:HTTP 请求细节

箭头方向 = 依赖方向,全部从外指向内。内层完全不知道外层存在。


项目目录结构

src/
├── domain/                          # 领域层(纯 TS,零框架依赖)
│   ├── value-objects/
│   │   ├── Money.ts
│   │   └── Email.ts
│   ├── entities/
│   │   └── CartItem.ts
│   ├── aggregates/
│   │   ├── Cart.ts
│   │   └── Order.ts
│   └── repositories/
│       ├── CartRepository.ts        # 接口定义
│       └── OrderRepository.ts
│
├── application/                     # 应用层(用例编排)
│   ├── usecases/
│   │   ├── AddToCartUseCase.ts
│   │   └── CheckoutUseCase.ts
│   └── composables/
│       └── useCart.ts
│
├── infrastructure/                  # 接口适配层
│   ├── repositories/
│   │   └── ApiCartRepository.ts     # Repository 实现
│   └── api/
│       └── CartApi.ts               # axios 调用
│
├── ui/                              # UI 框架层
│   ├── components/
│   │   └── ProductCard.vue
│   └── pages/
│       └── CartPage.vue

这样做的好处

场景 传统做法 整洁架构
换框架 Vue→React 重写所有业务逻辑 只重写 UI 层,领域层直接复用
API 字段名变了 改几十个组件 只改 ApiCartRepository 的转换逻辑
加新业务规则 在组件里到处加 if 在领域对象里加,组件无感知
单元测试 mock axios、mount 组件 直接测 Cart.addItem(),纯函数测试
后端复用 不可能 领域层是纯 TS,Node 端可直接引用

实际项目中的权衡

不是所有前端项目都需要完整分层

项目规模 建议做法
简单页面/CRUD 分离 API 调用和 UI 即可,不必过度设计
中等复杂度 提取领域层(纯 TS),用 Composable 做应用层
复杂业务系统 完整分层 + DDD 建模(如交易系统、审批流)

核心原则

领域层必须是纯 JS/TS,不依赖任何框架。 这样你的业务逻辑可以:

  • 在 Vue 和 React 之间迁移
  • 在 Node 后端复用(同构)
  • 独立写单元测试,不需要 mock 组件

框架是细节,业务逻辑才是核心。

❌
❌