Clean Architecture(整洁架构)前端版:Entities、Use Cases 与 Presenters 的分层

Clean Architecture(整洁架构)前端版:Entities、Use Cases 与 Presenters 的分层实践

各位开发者朋友,大家好!今天我们来深入探讨一个在现代前端开发中越来越受重视的架构理念——Clean Architecture(整洁架构)。它最初由 Robert C. Martin(Uncle Bob)提出,主要应用于后端系统设计,但它的核心思想完全可以迁移到前端领域,尤其是当你开始构建复杂、可维护、可测试的单页应用(SPA)时。

本文将以讲座模式展开,目标是帮助你理解:

  • 什么是 Clean Architecture?
  • 前端如何实现“分层”?特别是 Entities、Use Cases 和 Presenters 这三个关键层。
  • 每一层的作用、职责边界以及它们之间的依赖关系。
  • 实战代码示例(基于 React + TypeScript)。
  • 最终你会获得一套清晰、易于扩展和测试的前端项目结构。

一、什么是 Clean Architecture?

Clean Architecture 是一种软件设计原则,强调关注点分离(Separation of Concerns),其核心理念是:

依赖必须指向内层(业务逻辑层),外层(UI、数据库、API 等)只能依赖内层。

换句话说,业务逻辑不能依赖技术细节,而技术细节可以依赖业务逻辑。

这听起来很抽象?我们用一个类比来理解:

想象你在做一道菜:

  • 内层是你家的食谱(Recipe)——这是你的核心业务逻辑,比如“怎么做红烧肉”;
  • 中间层是你厨房里的工具(Knife, Pot, Stove)——这些是 Use Cases,负责执行具体操作;
  • 外层是你餐厅的菜单、服务员、顾客界面(UI)——这就是 Presenter 或 View 层。

无论你怎么换刀具(React/Vue)、换灶台(Node.js/Express),只要食谱不变,你就永远能做出同一道红烧肉。

这就是 Clean Architecture 的精髓:让业务逻辑独立于技术栈变化。


二、前端 Clean Architecture 的三层模型

在前端场景下,我们可以将 Clean Architecture 映射为以下三部分:

层级 名称 职责 技术无关性
内层 Entities(实体) 定义业务规则、数据结构、领域模型 ✅ 高度独立,不依赖任何框架或平台
中层 Use Cases(用例) 封装业务流程、事务处理、交互逻辑 ✅ 不依赖 UI 框架,仅依赖 Entities
外层 Presenters / Views(展示器 / 视图) 渲染 UI、响应用户输入、调用 Use Cases ❌ 依赖 UI 框架(如 React、Vue)

✅ 表示该层不依赖外部技术;❌ 表示该层依赖外部技术(如 React、HTTP 请求等)

这种分层方式的好处是:

  • 所有业务逻辑都在中间层,便于单元测试;
  • UI 变更不影响核心逻辑;
  • 后续迁移框架(比如从 React 到 Vue)只需重写 Presenter,不用动 Use Case 和 Entity。

三、详细拆解每一层

1. Entities(实体)——业务的核心

Entities 是你应用程序中的“数据对象”,它们代表了你要解决的问题域中的核心概念。例如,在一个电商系统中,ProductOrderUser 都是 Entity。

示例:Product Entity

// entities/Product.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  description?: string;
}

// 业务规则也可以放在这里(比如价格校验)
export class Product {
  constructor(
    public id: string,
    public name: string,
    public price: number,
    public description?: string
  ) {}

  // 核心业务逻辑:判断是否打折
  isDiscounted(): boolean {
    return this.price < 50;
  }

  // 业务规则:商品名称不能为空
  validate(): boolean {
    return !!this.name.trim();
  }
}

💡 注意:

  • Entity 不包含任何框架代码(如 React hooks、Redux action types);
  • 不依赖 HTTP、LocalStorage、DOM API;
  • 如果未来要支持多种数据源(REST API / GraphQL / LocalStorage),Entity 保持不变。

2. Use Cases(用例)——业务流程编排者

Use Cases 是真正的“业务逻辑处理器”。它们接收输入参数,调用 Entity 方法,完成某个完整的业务任务,并返回结果。

示例:获取商品列表的 Use Case

// useCases/GetProductsUseCase.ts
import { Product } from '../entities/Product';

export interface GetProductsInput {
  userId: string;
}

export interface GetProductsOutput {
  products: Product[];
  error?: string;
}

export class GetProductsUseCase {
  private readonly productRepository: ProductRepository;

  constructor(productRepository: ProductRepository) {
    this.productRepository = productRepository;
  }

  async execute(input: GetProductsInput): Promise<GetProductsOutput> {
    try {
      const products = await this.productRepository.findAll();

      // 可以在这里添加额外业务逻辑(比如过滤掉已下架的商品)
      const validProducts = products.filter(p => p.validate());

      return { products: validProducts };
    } catch (error) {
      return { error: 'Failed to fetch products' };
    }
  }
}

// 抽象接口,供不同仓库实现(如 REST、Mock)
export interface ProductRepository {
  findAll(): Promise<Product[]>;
}

💡 关键点:

  • Use Case 接收输入(Input),输出结果(Output);
  • 使用 Dependency Injection(依赖注入)来解耦数据访问;
  • 不直接操作 DOM 或发起网络请求,而是通过 Repository 接口委托给具体实现;
  • 可以轻松用 Jest 测试这个类,因为它完全脱离 UI 和网络。

3. Presenters / Views(展示器 / 视图)——用户交互入口

这是最外层,负责与用户交互,调用 Use Case,并将结果渲染到页面上。

示例:React 组件作为 Presenter

// presenters/ProductListPresenter.tsx
import React, { useState, useEffect } from 'react';
import { GetProductsUseCase } from '../useCases/GetProductsUseCase';
import { Product } from '../entities/Product';

interface Props {
  useCase: GetProductsUseCase;
}

const ProductListPresenter: React.FC<Props> = ({ useCase }) => {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadProducts = async () => {
      const result = await useCase.execute({ userId: 'user-123' });

      if (result.error) {
        setError(result.error);
      } else {
        setProducts(result.products);
      }
      setLoading(false);
    };

    loadProducts();
  }, [useCase]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name} - ${product.price}
          {product.isDiscounted() && <span style={{ color: 'red' }}> Discount!</span>}
        </li>
      ))}
    </ul>
  );
};

export default ProductListPresenter;

💡 亮点:

  • Presenter 只关心“如何显示”和“如何触发 Use Case”,不关心内部逻辑;
  • 即使以后换成 Vue、Angular,只要 Use Case 不变,Presenter 只需重写;
  • 可以单独测试 Presenter(使用 React Testing Library),验证渲染是否正确。

四、完整项目结构建议(推荐)

为了更好地组织代码,建议如下目录结构:

src/
├── entities/
│   └── Product.ts
├── useCases/
│   ├── GetProductsUseCase.ts
│   └── ProductRepository.ts (interface)
├── presenters/
│   └── ProductListPresenter.tsx
├── adapters/
│   └── HttpProductRepository.ts (implements ProductRepository)
└── main.ts (入口文件,负责 DI)

其中 adapters/ 是用来连接外部世界的适配器层,比如:

  • HttpProductRepository 实现 ProductRepository 接口,通过 HTTP 请求获取数据;
  • LocalStorageProductRepository 实现相同接口,用于本地缓存。

这样即使将来切换数据源(比如从 REST 改成 GraphQL),也不需要改 Use Case!


五、为什么这样做更好?——对比传统做法

让我们对比一下传统前端开发 vs Clean Architecture:

方面 传统做法(无分层) Clean Architecture
业务逻辑位置 混合在组件中(如 useEffect + fetch) 集中在 Use Cases
测试难度 难测(需模拟 DOM、网络) 易测(纯函数 + Mock Repository)
UI 更换成本 高(需重构整个组件) 低(只改 Presenter)
数据源变更成本 高(修改所有地方) 低(只改 Adapter)
可读性 差(逻辑分散) 好(职责分明)

举个例子:如果你现在想把 ProductListPresenter 从 React 移植到 Vue,只需要写一个新的 Vue 组件,调用同一个 GetProductsUseCase,根本不需要改动任何业务逻辑!


六、进阶技巧:依赖注入(DI)与工厂模式

为了让 Use Case 和 Presenter 解耦,我们需要一种机制来动态注入依赖,比如 ProductRepository 的具体实现。

你可以使用简单的工厂模式或依赖注入容器(DI Container):

工厂模式示例(简单场景)

// adapters/factory.ts
import { ProductRepository } from '../useCases/ProductRepository';
import { HttpProductRepository } from './HttpProductRepository';
import { LocalStorageProductRepository } from './LocalStorageProductRepository';

export function createProductRepository(type: 'http' | 'local'): ProductRepository {
  switch (type) {
    case 'http':
      return new HttpProductRepository();
    case 'local':
      return new LocalStorageProductRepository();
    default:
      throw new Error('Unknown repository type');
  }
}

然后在主入口中初始化:

// main.ts
import { GetProductsUseCase } from './useCases/GetProductsUseCase';
import { createProductRepository } from './adapters/factory';

const repo = createProductRepository('http');
const useCase = new GetProductsUseCase(repo);

// 在 React App 中传入 useCase
ReactDOM.render(<ProductListPresenter useCase={useCase} />, document.getElementById('root'));

这样就实现了“配置驱动”的架构,方便后续根据环境切换数据源(如 dev/test/prod)。


七、总结:为什么你应该采用 Clean Architecture 前端版?

长期价值高:随着项目增长,混乱的代码会拖垮团队效率,而 Clean Architecture 让代码清晰有序。

提升可测试性:每个模块都可以独立测试,减少集成测试负担。

降低技术债务:UI 框架更换、数据源迁移不再痛苦。

利于协作:前后端分工明确,前端专注 UI,后端专注业务逻辑。

符合行业趋势:越来越多公司开始在前端引入 Clean Architecture(如 Airbnb、Netflix 的工程实践)。


最后送一句话给你:

“好的架构不是让你立刻写出更快的代码,而是让你在未来三年里依然能优雅地扩展它。”

希望这篇文章能帮你建立起对 Clean Architecture 前端版的深刻理解。如果你正在做一个复杂的前端项目,不妨试试这套分层结构 —— 它会让你的代码变得更有生命力。

谢谢大家!欢迎提问交流 😊

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注