skip to content
logo

Search

MVVM+DDD+CleanArchitecture

前端架构腐化:熵增定律与 MVVM 框架的双重困境

在项目开发中我们很少为前端去设计标准规范的代码架构,可能更多的去关注的是工程化、目录层级、以及业务代码的实现。随着前端业务复杂度的增长,代码维护成本、可扩展性、团队协作效率等问题日益凸显。

很多开发者都不得不面对的一个困境就是:很多时间被消耗在代码理解与导航上,而非实际编码。

个人认为,这种困境主要源于现实因素与框架缺陷两个方面。

  1. 架构层面的失控:业务逻辑与框架实现强耦合,导致复用性差、迁移成本高
  2. 代码组织模式的局限性:逻辑碎片化、层层嵌套、组件臃肿等

1. 不可避免的熵增:软件发展的自然规律

在热力学中,熵增定律告诉我们:封闭系统总是自发地从有序走向无序。这一规律同样适用于软件开发领域。前端项目不断增长的复杂性,主要源于两个不可抗拒的现实因素:

  1. 需求演变的必然性
    软件是对真实世界的模拟。正如人类认知世界的过程总是从简单到复杂,业务需求的演变也遵循同样的规律。我们永远无法在项目初期就预见所有可能性,项目初期的那些逻辑清晰的组件,随着需求的不断变更和迭代可能会变得越来越复杂。
  2. 开发模式的局限性

    当前主流的前端开发流程实际上在加速熵增过程:

  • 以界面为中心的开发思维:前端工程师在开发一个业务的时候,总是习惯先从界面出发,对着设计稿思考要怎么去拆分组件与还原页面,等把界面交互大致写出来之后,再把产品文档里面的业务逻辑作为一些判断条件加入到写好的交互代码中,最终交付。这种直线式开发,忽略了思考顶层设计。
  • 需求实现的路径依赖:在既有组件上不断打补丁式的迭代
  • 紧急需求的权宜之计:为赶工期牺牲架构合理性

2. MVVM 框架的架构陷阱

现代 MVVM 框架在提升开发效率的同时,其特性也在无形中诱导着架构腐化:

  • 业务逻辑与 UI 逻辑混杂

MVVM 的 ViewModel 层通常承担了 数据转换、状态管理、业务逻辑 等多重职责,导致业务规则散落在各个组件中,难以统一维护。结果就是:业务逻辑无法复用,相同规则在不同组件中重复实现。修改业务规则时,需多处调整,容易遗漏

  • 缺乏明确的业务领域边界

MVVM 框架的核心是数据绑定,是解决 UI 与状态的同步问题的技术分“层”,而非业务领域分层。
结果就是:不同业务模块的代码混杂,难以按领域拆分。新成员理解系统业务成本高。

  • 贫血模型问题

MVVM 中的 Model 通常是纯数据对象,仅仅是“数据容器”,而不具备行为(方法)。
结果就是:业务规则无法通过 Model 自身的方法表达。业务逻辑无处安放,然后分散到各处,无法体现领域模型的核心作用。

// 典型的贫血模型
interface Order {
  id: string
  status: string
  items: OrderItem[]
}

// 业务逻辑外溢到组件
const cancelOrder = (order: Order) => {
  if (order.status !== 'pending') {
    throw new Error('非法状态')
  }
  // ...取消逻辑
}

分层架构:把业务逻辑从交互代码中解救出来

“计算机领域的任何问题都可以通过增加一个间接的中间层来解决”。

当熵增遇到框架局限性,我们需要通过架构设计施加"外力"来实现熵减。

作为前端,我们的架构设计应达到以下目标:

  1. 视图层仅负责渲染:UI 组件只依赖稳定的 Entity 模型,不直接处理原始数据或业务规则,使视图层可以独立替换(如切换 UI 库)而不影响内层逻辑。
  2. 业务逻辑与视图层解耦:核心业务逻辑(Use Case & Entity)不应依赖 UI 框架及其生态,确保逻辑的可复用性和可测试性。
  3. 数据驱动但稳定:作为数据驱动的应用,应通过 Adapter 层隔离外部数据变化(如 API 响应结构调整、第三方服务格式变更),保证 Entity 层的数据模型和业务逻辑不受影响。

想要达成目标,我们需要有一些要必须实现的设计:

  1. 通过 DDD 划分明确的业务边界,将业务规则封装在富领域模型中,建立领域护城河

    // 富领域模型示例
    class Order {
      constructor(private readonly status: OrderStatus) {}
      
      cancel() {
        if (this.status !== OrderStatus.PENDING) {
          throw new BusinessRuleViolation('仅能取消待支付订单')
        }
        // ...领域逻辑
      }
    }
  2. 采用整洁架构的分层原则,构建单向依赖体系 UI → Application → Domain ← Infrastructure

    • 领域层保持纯净业务逻辑
    • 依赖关系严格单向流动
    • 各层通过接口解耦
  3. 框架归框架,业务归业务

    在保留 MVVM 开发效率的同时建立隔离层:

    // 在Pinia store中引入分层
    const useOrderStore = defineStore('order', () => {
      // 依赖注入
      const repository = new OrderRepository()
      const cancelUseCase = new CancelOrderUseCase(repository)
    
      // ViewModel职责
      const orders = ref<Order[]>([])
      
      return {
        orders,
        cancel: async (id) => {
          await cancelUseCase.execute(id)
          // 更新视图状态...
        }
      }
    })

这种架构演进不是对 MVVM 的否定,而是对其局限性的补充。优秀的前端架构应该让框架各司其职,而不是被框架绑定架构。

特性MVC/MVVM分层架构(如 Clean Architecture)
职责分离关注点分离(View/Logic)严格分层(UI/App/Domain/Infra)
依赖方向可能双向依赖(如 View ↔ Model)单向依赖(上层 → 下层)
业务逻辑存放位置可能分散在 Controller/ViewModel集中在领域层
可测试性较差(需模拟 UI 环境)高(领域层可独立测试)
适合场景简单到中等复杂度应用复杂业务系统(如金融、电商)

代码实践

下面通过一个 Vue 3 + Composition API (Hooks) + Pinia 的示例,演示如何结合 MVVM(保留开发效率) 和 DDD + 整洁架构(治理业务复杂度),实现分层清晰、可维护性高的前端架构。

示例场景:电商订单管理系统

核心功能:

  • 用户提交订单
  • 订单状态管理(待支付、已支付、已取消)
  • 支付流程

分层架构设计

项目文件结构采用 Clean Architecture + DDD 的分层方式:(只是示意,具体要根据项目设计)

src/
├── domain/            # 领域层(纯业务逻辑)
├── application/       # 应用层(用例协调)
├── infrastructure/    # 基础设施层(API、存储)
└── ui/                # UI层(MVVM:View + ViewModel)
  • 领域层(Domain Layer)定义核心业务模型和规则,不依赖任何框架或技术
// src/domain/order.ts
export enum OrderStatus {
  PENDING = "PENDING",
  PAID = "PAID",
  CANCELLED = "CANCELLED",
}

// 领域实体(充血模型,包含业务逻辑)
export class Order {
  constructor(
    public readonly id: string,
    public status: OrderStatus,
    public readonly amount: number
  ) {}

  // 业务方法:取消订单
  cancel() {
    if (this.status !== OrderStatus.PENDING) {
      throw new Error("Only pending orders can be cancelled");
    }
    this.status = OrderStatus.CANCELLED;
  }

  // 业务方法:支付订单
  pay() {
    if (this.status !== OrderStatus.PENDING) {
      throw new Error("Only pending orders can be paid");
    }
    this.status = OrderStatus.PAID;
  }
}
  • 应用层(Application Layer):定义 用例(Use Case),协调领域逻辑和基础设施。
// src/application/useCases/cancelOrder.ts
import type { OrderRepository } from "../../infrastructure/repositories/orderRepository";
import { Order } from "../../domain/order";

export class CancelOrderUseCase {
  constructor(private orderRepository: OrderRepository) {}

  async execute(orderId: string): Promise<void> {
    const order = await this.orderRepository.findById(orderId);
    order.cancel(); // 调用领域逻辑
    await this.orderRepository.save(order);
  }
}
  • 基础设施层(Infrastructure Layer):实现技术细节(API 调用、本地存储等),依赖领域层接口
// src/infrastructure/repositories/orderRepository.ts
import type { Order } from "../../domain/order";

// 仓库接口(领域层定义)
export interface OrderRepository {
  findById(id: string): Promise<Order>;
  save(order: Order): Promise<void>;
}

// 具体实现(基础设施层)
export class ApiOrderRepository implements OrderRepository {
  async findById(id: string): Promise<Order> {
    const response = await fetch(`/api/orders/${id}`);
    const data = await response.json();
    return new Order(data.id, data.status, data.amount);
  }

  async save(order: Order): Promise<void> {
    await fetch(`/api/orders/${order.id}`, {
      method: "PUT",
      body: JSON.stringify(order),
    });
  }
}
  • UI 层(MVVM:View + ViewModel):用 Vue 3 + Pinia 实现 ViewModel,仅依赖应用层
// src/ui/stores/orderStore.ts (Pinia Store)
import { defineStore } from "pinia";
import { CancelOrderUseCase } from "../../application/useCases/cancelOrder";
import { ApiOrderRepository } from "../../infrastructure/repositories/orderRepository";

export const useOrderStore = defineStore("order", () => {
  // 依赖注入(DIP:依赖接口,而非具体实现)
  const orderRepository = new ApiOrderRepository();
  const cancelOrderUseCase = new CancelOrderUseCase(orderRepository);

  // ViewModel 状态(Pinia 的 reactive 数据)
  const currentOrder = ref<Order | null>(null);

  // ViewModel 方法(调用 Use Case)
  const cancelOrder = async () => {
    if (!currentOrder.value) return;
    await cancelOrderUseCase.execute(currentOrder.value.id);
  };

  return { currentOrder, cancelOrder };
});
<!-- src/ui/views/OrderView.vue (Vue Component) -->
<template><div><h2>Order: {{ orderStore.currentOrder?.id }}</h2><button @click="orderStore.cancelOrder">Cancel Order</button></div></template><script setup>
import { useOrderStore } from "../stores/orderStore";
const orderStore = useOrderStore();

// 初始化订单数据(模拟)
onMounted(async () => {
  orderStore.currentOrder = await orderRepository.findById("123");
});
</script>

关键设计原则

单向依赖

UI → Application → Domain ← Infrastructure

  • UI 层 只依赖 应用层(如 CancelOrderUseCase
  • 领域层 不依赖任何其他层(纯业务逻辑)
  • 基础设施层 实现领域层定义的接口(如 OrderRepository

依赖倒置(DIP)

  • 应用层 依赖抽象的 OrderRepository(接口),而非具体的 ApiOrderRepository
  • 可轻松替换实现(如换成 LocalStorageOrderRepository

领域模型驱动

  • 业务规则(如“只有待支付订单可取消”)集中在 Order 类中
  • 避免贫血模型,确保数据和行为封装在一起

可测试性

  • 领域层 可单独测试(不依赖 UI 或 API)
  • 应用层 可通过 Mock 仓库测试用例逻辑

对比传统 MVVM 的改进

传统 MVVM 问题本方案改进点
业务逻辑散落在 ViewModel逻辑集中在领域层(Order.cancel()
难以替换数据源通过 OrderRepository 接口解耦
状态管理混乱Pinia 仅管理 UI 状态,业务状态在领域层
可测试性差领域层和用例层可独立测试

总结

如何平衡 MVVM 效率与分层架构?

  1. 保留 MVVM 的 UI 开发效率

    • 使用 Vue 3 + Pinia 管理 UI 状态(如 currentOrder
    • ViewModel 调用 Use Case 而非直接操作业务逻辑
  2. 通过 DDD + 整洁架构治理复杂度

    • 领域层:封装核心业务规则,设计领域模型
    • 应用层:依赖倒置,协调用例,避免业务逻辑泄漏到 UI
    • 基础设施层:技术细节可插拔
  3. 依赖方向严格管控

    • 确保 UI → Application → Domain ← Infrastructure 的单向依赖

最终效果

  • 业务逻辑 高内聚、可复用
  • 技术细节(如 API、存储)可灵活替换
  • 长期维护性 显著提升,适合复杂前端项目