Files
conti-docs/02-layering.md
T

158 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<PaymentOrder> fetchOrder(String orderId);
Future<void> confirmPayment(String orderId, String pinToken);
}
// domain/use_case/confirm_payment_use_case.dart
class ConfirmPaymentUseCase {
final PaymentRepository _repository;
ConfirmPaymentUseCase(this._repository);
Future<void> 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<PaymentOrder> 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<void> 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<List<Store>> fetchNearbyStores(double lat, double lng);
}
class StoreRepositoryImpl implements StoreRepository {
final Dio _dio;
StoreRepositoryImpl(this._dio);
@override
Future<List<Store>> 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 ArchitectureUncle 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)