本文由 简悦SimpRead 转码,原文地址 codewithandrea.com
介绍领域模型及其在定义实体和业务逻辑方面的作用,以便于管理......。
你是否曾经把你的UI、业务逻辑和网络的代码混在一起,变成一捆混乱的意大利面条代码?
我知道我做过。✋
毕竟,现实世界的应用开发是很难的。
诸如领域驱动设计(DDD)之类的书籍已经被写出来,以帮助我们开发复杂的软件项目。
而DDD的核心在于模型,它捕捉了解决手头问题所需的重要的知识和概念。而拥有一个好的域模型可以使软件项目的成败完全不同。
模型非常重要,但它们不能孤立地存在。即使是最简单的应用程序也需要一些用户界面(用户看到的和与之互动的东西),并需要与外部的API进行通信,以显示一些有意义的信息。
Flutter分层架构
在这种情况下,采用分层架构往往是有价值的,在系统的不同部分之间引入明确的关注点分离。而这使得我们的代码更容易被阅读、维护和测试**。
概括地说,通常有四个不同的层次。
- 表现层
- 应用层
- 领域层
- 数据层
使用数据层、领域层、应用层和表现层的Flutter应用程序架构。
数据层位于底部,包含用于与外部数据源对话的存储库。
要了解更多关于数据层和资源库模式的信息,请阅读这篇文章。Flutter应用程序架构:存储库模式。
就在它上面,我们发现了域和应用层。这些层非常重要,因为它们容纳了我们应用程序的所有模型和业务逻辑。
在这篇文章中,我们将专注于域层,使用一个电子商务应用作为实际例子。作为其中的一部分,我们将学习。
- 什么是领域模型
- 如何在Dart中定义实体并将其表现为数据类
- 如何为我们的模型类添加业务逻辑
- 如何为该业务逻辑编写单元测试
准备好了吗?我们开始吧!
什么是领域模型?
维基百科对领域模型是这样定义的。
领域模型是一个领域的概念模型,包含了行为和数据。
数据可以由一组实体以及它们的关系来表示,而行为则由一些操作这些实体的业务逻辑来进行编码。
以一个电子商务应用为例,我们可以确定以下实体。
- 用户。ID和电子邮件
- 产品。ID、图片URL、标题、价格、可用数量等。
- 项目。产品ID和数量
- 购物车。项目的清单,总数
- 订单。项目列表,支付的价格,状态,支付细节等。
电子商务应用:实体和它们的关系。
在实践DDD时,实体和关系不是我们凭空产生的东西,而是一个(有时是漫长的)知识发现过程的最终结果。作为这个过程的一部分,领域的词汇也被正式化,并被所有各方使用。
请注意,在这个阶段,我们并不关心这些实体来自哪里,或者它们是如何在系统中传递的。
重要的是,我们的实体是我们系统的核心,因为我们需要它们来为我们的用户解决领域相关的问题。
在DDD中,人们经常对实体和值对象进行区分。欲了解更多信息,请参见这个关于StackOverflow上的价值与实体对象的主题。
当然,一旦我们开始构建我们的应用程序,我们就需要实现这些实体并决定它们在我们的架构中的位置。
而这就是领域层的作用。
今后,我们将把实体称为模型,可以在Dart中作为简单的类来实现。
领域层
让我们重新审视一下我们的架构图。
使用数据层、领域层、应用层和表现层的Flutter应用程序架构。
正如我们所见,模型属于领域层。它们被下面数据层的存储库检索,并可被上面应用层的服务修改。
那么,这些模型在Dart中是什么样子的呢?
好吧,让我们考虑一个产品模型类为例。
/// 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() {
...
}
}
至少,这个类持有我们需要在用户界面中显示的所有属性。
产品卡使用我们模型类的所有属性。
而且它还包含fromMap()和toMap()方法,用于序列化。
在Dart中,有多种方法来定义模型类和它们的序列化逻辑。欲了解更多信息,请参见我的Dart中JSON解析的基本指南和关于使用Freezed生成代码的后续文章。
请注意,"产品 "模型是一个简单的数据类,它不能访问存储库、服务或其他属于**领域层之外的对象。
模型类中的业务逻辑
然而,模型类可以包括一些业务逻辑来表达它们是如何被修改的。
为了说明这一点,让我们考虑一个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);
if (copy.containsKey(productId)) {
copy[productId] = quantity + copy[productId]!;
} else {
copy[productId] = quantity;
}
return Cart(copy);
}
Cart removeItemById(ProductID productId) {
final copy = Map<ProductID, int>.from(items);
copy.remove(productId);
return Cart(copy);
}
}
上面的方法复制了购物车中的物品(使用Map.from()),修改了里面的值,并返回一个新的不可变的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模型类
- 将行为转化为工作代码(业务逻辑),对这些模型类进行操作
- 添加单元测试以验证行为的正确实现。
当你这样做时,想想你需要在用户界面中显示什么数据,以及用户将如何与之互动。
但先不要担心事情如何连接在一起。事实上,应用层中的服务的工作是通过在数据层的存储库和表现层的控制器之间进行调解来与模型合作。
而这将是未来文章的主题。
即将推出:完整的Flutter课程
我正在准备一个全新的课程,它将非常深入地介绍Flutter应用程序的架构,以及其他重要的主题,如状态管理、导航和路由、测试等等。
如果您对此感兴趣,您可以查看课程页面或在此报名。