# 02. 分层架构规范 ## 决策 每个 `feature_*` 包内部采用简化版分层,`domain` 层**可选**,不强制每个 feature 都有: ``` feature_xxx/ lib/ feature_xxx.dart # 对外唯一导出文件 src/ presentation/ # widgets + Riverpod provider/notifier domain/ # 可选:entity + repository 接口 + use case data/ # repository 实现 + remote/local datasource test/ ``` ## 各层职责 - **presentation**:widgets、Riverpod `Notifier`/`Provider`。只处理 UI 状态和用户交互,不直接调用 `data` 层的具体实现类,通过依赖注入拿到抽象类型。 - **domain**(可选):`entity` 定义业务模型,`repository` 接口声明数据契约,`use case` 封装跨 repository 协调或多步骤业务规则。 - **data**:`repository` 接口的具体实现,内部再拆 `remote_datasource`(走 `core_network`)和 `local_datasource`(走 `core_storage`)。 ## 何时可以跳过 domain 层 判断标准: - **可以跳过**:功能是简单 CRUD、没有跨 repository 协调、没有多步骤业务规则——`presentation` 直接依赖 `data` 层定义的 repository 接口即可,`repository` 接口挪到 `data` 层里声明。 - **必须要有**:涉及多步骤业务规则(如支付的多步校验)、需要协调多个 repository、包含状态机或需要独立于 UI 单元测试的核心业务逻辑——`repository` 接口放在 `domain`,`data` 层依赖 `domain` 反向实现接口。 ## Repository 接口的位置规则 - 有 `domain` 层:接口定义在 `domain/repository/`,`data/repository_impl/` 实现它,`presentation` 只依赖 `domain` 里的抽象类型。 - 无 `domain` 层:接口直接定义在 `data/repository/`,同文件或同目录下给出实现类,`presentation` 依赖这个接口类型。 两种情况下,`presentation` 都不允许直接依赖 `data` 层的具体实现类(如 `XxxRepositoryImpl`),只依赖接口——这条不因为是否跳过 domain 层而改变。 ## 跨层依赖规则 ``` presentation → domain(或直接 → data 的接口,若跳过 domain) domain → 不依赖 presentation / data data → 依赖 domain 的接口(若有),依赖 core_network / core_storage ``` `domain` 层禁止 import 任何 Flutter SDK(`package:flutter/...`)——保持纯 Dart,可脱离 UI 单独做 unit test。 ## 附录:分层架构是什么,为什么要分层 给还没接触过这套分层习惯的同学看的入门说明。 ### 要解决的问题 如果 UI 代码里直接写网络请求、直接 new 一个 `Dio` 实例、直接操作数据库——短期能跑,但会导致两个问题: 1. **没法单独测试业务逻辑**:想验证"支付金额校验规则对不对",得连 widget 一起跑测试,跑得慢还容易因为 UI 变了导致业务逻辑测试跟着挂。 2. **换底层实现要动 UI 代码**:比如把网络库从 `dio` 换掉,或者把本地存储从 `shared_preferences` 换成 `Drift`,如果 UI 直接依赖具体实现类,改动会散落得到处都是。 **分层的本质**:把"业务规则"和"业务规则的具体实现方式(用什么网络库、存什么数据库)"分开,中间用抽象接口隔开。这就是 [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 这套思想的核心,我们只取最简化的三层版本,不套用它完整的同心圆规则。 ### 依赖方向是关键 三层最重要的不是"分了几层",而是**依赖只能单向流动**: ``` presentation ──依赖──> domain ──定义接口,不依赖任何人 ▲ │ 实现接口(依赖倒置) data ``` `domain` 不 import `data`,也不 import `presentation`——它甚至不知道 `data` 层是用 `dio` 还是别的什么网络库实现的,只定义"我需要一个能拿到 `PaymentOrder` 的东西"(接口),至于这个东西具体怎么实现,由 `data` 层负责,这就是[依赖倒置原则](https://en.wikipedia.org/wiki/Dependency_inversion_principle)。好处是:`domain` 层的业务规则可以完全脱离网络、脱离 UI 单独写单元测试。 ### 示例一:有 domain 层(`feature_payment`,支付确认——多步骤业务规则) ```dart // domain/entity/payment_order.dart class PaymentOrder { final String orderId; final int amountCents; final PaymentStatus status; const PaymentOrder({required this.orderId, required this.amountCents, required this.status}); } // domain/repository/payment_repository.dart abstract class PaymentRepository { Future fetchOrder(String orderId); Future confirmPayment(String orderId, String pinToken); } // domain/use_case/confirm_payment_use_case.dart class ConfirmPaymentUseCase { final PaymentRepository _repository; ConfirmPaymentUseCase(this._repository); Future call(String orderId, String pinToken) async { final order = await _repository.fetchOrder(orderId); if (order.status != PaymentStatus.pending) { throw StateError('订单状态不允许支付: ${order.status}'); } if (order.amountCents <= 0) { throw ArgumentError('订单金额非法'); } await _repository.confirmPayment(orderId, pinToken); } } // data/repository/payment_repository_impl.dart class PaymentRepositoryImpl implements PaymentRepository { final Dio _dio; // 来自 core_network PaymentRepositoryImpl(this._dio); @override Future fetchOrder(String orderId) async { final res = await _dio.get('/orders/$orderId'); return PaymentOrder( orderId: res.data['orderId'], amountCents: res.data['amountCents'], status: PaymentStatus.values.byName(res.data['status']), ); } @override Future confirmPayment(String orderId, String pinToken) => _dio.post('/orders/$orderId/confirm', data: {'pinToken': pinToken}); } ``` `ConfirmPaymentUseCase` 的多步校验规则可以直接用假的 `PaymentRepository` 实现来做单元测试,完全不需要启动 Flutter engine 或起一个 mock server。 ### 示例二:跳过 domain 层(`feature_store`,门店列表——简单 CRUD) ```dart // data/repository/store_repository.dart abstract class StoreRepository { Future> fetchNearbyStores(double lat, double lng); } class StoreRepositoryImpl implements StoreRepository { final Dio _dio; StoreRepositoryImpl(this._dio); @override Future> fetchNearbyStores(double lat, double lng) async { final res = await _dio.get('/stores', queryParameters: {'lat': lat, 'lng': lng}); return (res.data as List).map((e) => Store.fromJson(e)).toList(); } } ``` 没有多步骤规则、没有跨 repository 协调,接口和实现直接放在 `data` 层,`presentation` 直接依赖 `StoreRepository` 这个接口,省掉一层 `domain` 目录和 use case 模板代码。 ## 参考链接 - [Flutter 官方状态管理文档](https://docs.flutter.dev/data-and-backend/state-mgmt) - [The Clean Architecture(Uncle Bob 原文)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [依赖倒置原则(Dependency Inversion Principle)](https://en.wikipedia.org/wiki/Dependency_inversion_principle)