您是否曾将UI、业务逻辑和网络代码混合成一团乱麻般的代码? 我知道我做到了。✋ 毕竟,现实世界的应用程序开发很难。 已经有一些诸如领域驱动设计(DDD)的书籍来帮助我们开发复杂的软件项目。 DDD 的核心是模型,它捕获了解决手头问题所需的重要知识和概念。拥有一个好的领域模型可以决定软件项目的成败。 模型非常重要,但它们不能孤立存在。 即使是最简单的应用程序也需要一些 UI(用户看到并与之交互的内容),并且需要与外部 API 通信以显示一些有意义的信息。
Flutter 分层架构
在 CODE WITH ANDREA:使用 Riverpod 的 Flutter 应用架构介绍 文章中,介绍了基于 Riverpod 包的应用程序架构,明确了该架构包含四层:数据层、 领域层、 应用层、展示层。
数据层位于底部,包含用于与外部数据源通信的Repository。 在其上方,我们找到了领域层和应用层。这些层非常重要,因为它们包含我们应用程序的所有模型和业务逻辑。
在本文中,我们将重点介绍域层,并使用电子商务应用程序作为实际示例。作为其中的一部分,我们将学习:
- 什么是领域模型
- 如何在 Dart 中定义实体并将其表示为数据类
- 如何向我们的模型类添加业务逻辑
- 如何为该业务逻辑编写单元测试
准备好了吗?出发!
什么是领域模型?
维基百科对领域模型的定义如下:
领域模型是融合行为和数据的领域概念模型。
数据可以由一组实体及其关系来表示,而行为则由用于操纵这些实体的某些业务逻辑进行编码。 以电子商务应用程序为例,我们可以识别以下实体:
- 用户:ID 和电子邮件
- 产品:ID、图片URL、标题、价格、可用数量等。
- 商品:产品编号和数量
- 购物车:商品清单,总计
- 订单:物品清单、付款价格、状态、付款详情等。

在实践 DDD 时,实体和关系不是我们凭空产生的,而是(有时很长的)知识发现过程的最终结果。作为该过程的一部分,领域词汇表也被形式化并供各方使用。
请注意,在此阶段我们并不关心这些实体来自哪里或它们如何在系统中传递。
重要的是,我们的实体是我们系统的核心,因为我们需要它们为我们的用户解决与领域相关的问题。
在 DDD 中,通常会区分实体和值对象。有关更多信息,请参阅StackOverflow 上有关值对象与实体对象的线程。
当然,一旦我们开始构建我们的应用程序,我们就需要实现这些实体并决定它们在我们的架构中的位置。
这就是领域层发挥作用的地方。
展望未来,我们将把实体称为可以在 Dart 中作为简单类实现的模型。
领域层
模型属于领域层。它们由下面数据层的Repository检索,并可以由上面应用层的服务修改。
那么这些模型在 Dart 中是什么样的?
好吧,让我们考虑一个 Product 模型类作为例子:
/// The ProductID is an important concept in our domain
/// so it deserves a type of its own
typedef ProductID = String;
class Product {
Product({
required this.id,
required this.imageUrl,
required this.title,
required this.price,
required this.availableQuantity,
});
final ProductID id;
final String imageUrl;
final String title;
final double price;
final int availableQuantity;
// serialization code
factory Product.fromMap(Map<String, dynamic> map, ProductID id) {
...
}
Map<String, dynamic> toMap() {
...
}
}
至少,此类包含我们需要在 UI 中显示的所有属性: 
它还包含 fromMap() 用于 toMap() 序列化的方法。
在 Dart 中,有多种方法可以定义模型类及其序列化逻辑。有关更多信息,请参阅我的Dart JSON 解析基本指南 以及有关使用 Freezed 进行代码生成的后续文章。
请注意,Product 模型是一个 简单的数据类,它无权访问Repository、服务或属于域层之外的其他对象。
模型类中的业务逻辑
然而,模型类可以包含一些业务逻辑来表达如何修改它们。
为了说明这一点,我们来考虑一个Cart模型类:
class Cart {
const Cart([this.items = const {}]);
/// All the items in the shopping cart, where:
/// - key: product ID
/// - value: quantity
final Map<ProductID, int> items;
factory Cart.fromMap(Map<String, dynamic> map) { ... }
Map<String, dynamic> toMap() { ... }
}
它以键值对的映射形式实现,代表我们已添加到购物车中的商品的 ID 和数量。
而且由于我们可以在购物车中添加和删除商品,定义一个扩展来简化此任务可能会很有用:
/// Helper extension used to update the items in the shopping cart.
extension MutableCart on Cart {
Cart addItem({required ProductID productId, required int quantity}) {
final copy = Map<ProductID, int>.from(items);
// * update item quantity. Read this for more details:
// * https://codewithandrea.com/tips/dart-map-update-method/
copy[productId] = quantity + (copy[productId] ?? 0);
return Cart(copy);
}
Cart removeItemById(ProductID productId) {
final copy = Map<ProductID, int>.from(items);
copy.remove(productId);
return Cart(copy);
}
}
上述方法复制购物车中的商品(使用 Map.from() ),修改里面的值,并返回一个新的不可变 Cart 对象,可用于更新底层数据存储(通过相应的Repository)。
如果您不熟悉上述语法,请阅读:如何在 Dart 中更新键值对的 Map。
许多状态管理解决方案都依赖于不可变对象来传播状态更改并确保我们的Widget仅在需要时重建。规则是,当我们需要改变模型中的状态时,我们应该通过制作一个新的不可变副本来实现。
测试模型中的业务逻辑
请注意,Cart 类及其 MutableCart 扩展不依赖于域层之外的任何对象。
这使得它们非常容易测试。
为了证明这一点,我们可以编写一组单元测试来验证 addItem() 方法中的逻辑:
void main() {
group('add item', () {
test('empty cart - add item', () {
final cart = const Cart()
.addItem(productId: '1', quantity: 1);
expect(cart.items, {'1': 1});
});
test('empty cart - add two items', () {
final cart = const Cart()
.addItem(productId: '1', quantity: 1)
.addItem(productId: '2', quantity: 1);
expect(cart.items, {
'1': 1,
'2': 1,
});
});
test('empty cart - add same item twice', () {
final cart = const Cart()
.addItem(productId: '1', quantity: 1)
.addItem(productId: '1', quantity: 1);
expect(cart.items, {'1': 2});
});
});
}
为我们的业务逻辑编写单元测试不仅容易,而且增加了很多价值。
如果我们的业务逻辑不正确,我们的应用中肯定会有错误。因此,我们完全有动力通过确保我们的模型类没有任何依赖关系来使其易于测试。
结论
我们已经讨论过拥有良好的系统思维模型的重要性。
我们还了解了如何在 Dart 中将我们的模型/实体表示为不可变的数据类,以及我们可能需要修改它们的任何业务逻辑。
我们已经了解了如何为该业务逻辑编写一些简单的单元测试,而无需借助模拟或任何复杂的测试设置。
以下是您在设计和构建应用程序时可以使用的一些技巧:
- 探索领域模型,找出需要表示的概念和行为
- 将这些概念连同它们的关系一起表达为实体
- 实现相应的 Dart 模型类
- 将行为转换为可对这些模型类进行操作的工作代码(业务逻辑)
- 添加单元测试来验证行为是否正确实现
当你这样做时,请考虑需要在 UI 中显示什么数据以及用户如何与之交互。
但目前还不必担心如何将事物连接在一起。事实上,应用层中的服务的工作是通过在数据层中的Repository和展示层中的Controller之间进行调解来处理模型。
这将是未来文章的主题。
参考资料
文档信息
- 本文作者:Bob.Zhu
- 本文链接:https://home.mytool.group/2024/08/17/03-flutter-app-architecture-domain-model/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
