Flutter/Riverpod中实体无逻辑、逻辑放应用层是否正确?
在Flutter/Riverpod中实体与业务逻辑的设计规范
核心结论
在Flutter/Riverpod生态中,将实体设计为无逻辑(或仅含极少量核心属性验证逻辑)的不可变类,把业务逻辑抽离到应用层(服务/UseCase)是常规且推荐的做法。
术语混淆的根源
不同架构体系(如DDD、Clean Architecture)对model/entity/state的定义存在差异,再加上不同作者的理解偏差,才导致你看到的教程内容不一致。以下是这三个术语在Flutter/Riverpod场景下的常见定位:
1. Model/DTO
- 定位:可变的数据传输对象,仅作为数据容器,负责在不同数据源(API、本地数据库)和应用层之间传递原始数据。
- 特点:无业务逻辑,仅包含序列化/反序列化相关代码。
- 示例:
class PersonModel { String name = ''; int age = 0; PersonModel.fromJson(Map<String, dynamic> json) { name = json['name']; age = json['age']; } }
2. Entity
- 定位:不可变的核心业务对象,代表业务领域中的核心概念(如用户、订单),是跨层传递的标准数据格式(从Repository到状态层再到UI层)。
- 特点:仅包含与自身属性强相关的极简逻辑(如判断是否成年的
isAdult()),绝不包含修改自身状态的可变方法——所有状态变更都通过生成新实例实现(类似copyWith方法)。 - 示例:
class PersonEntity { final int id; final String name; final int age; const PersonEntity({required this.id, required this.name, required this.age}); // 仅保留与属性强相关的极简逻辑 bool get isAdult => age >= 18; // 通过copyWith生成新实例实现状态变更,保证不可变性 PersonEntity copyWith({String? name, int? age}) { return PersonEntity( id: id, name: name ?? this.name, age: age ?? this.age, ); } }
3. State
- 定位:UI或业务流程的快照对象,不仅包含实体数据,还附带UI相关的状态标记(如加载中、错误、空状态)。
- 特点:必须不可变,方便Riverpod通过对象引用变化触发UI更新。
- 示例:
sealed class PersonState {} class PersonLoading extends PersonState {} class PersonLoaded extends PersonState { final PersonEntity person; PersonLoaded(this.person); } class PersonError extends PersonState { final String message; PersonError(this.message); }
关于业务逻辑的放置
将业务逻辑(如修改用户信息、权限验证、数据处理)放到应用层的服务/UseCase中,而非实体内部,有以下优势:
- 保持实体的纯净性:实体仅专注于业务对象的定义,不承担额外逻辑,更易维护和理解。
- 逻辑复用:服务层的逻辑可以被多个组件或业务流程复用。
- 易于测试:业务逻辑集中在服务层,无需依赖实体实例,单元测试更简单。
比如修改用户姓名的逻辑,正确的做法是放在服务中:
class UserService { PersonEntity updateUserName(PersonEntity person, String newName) { // 这里可以添加验证逻辑(如名字长度检查) return person.copyWith(name: newName); } }
内容的提问来源于stack exchange,提问作者Maciej Pszczolinski
相关产品推荐
相关产品推荐

