你是否曾将用户界面、业务逻辑和网络代码混杂在一起,成为一捆乱七八糟的意大利面代码?
我知道我曾这样做过。 ✋
毕竟,真实世界的应用程序开发是很困难的。
领域驱动设计(DDD)等书籍就是为了帮助我们开发复杂的软件项目而编写的。 DDD 的核心在于模型,它捕捉了解决当前问题所需的重要知识和概念。 一个好的领域模型可以决定一个软件项目的成败。
模型非常重要,但不能孤立存在。 即使是最简单的应用程序也需要一些 UI(用户看到的和与之交互的内容),并需要与外部 API 通信,以显示一些有意义的信息。
Flutter分层架构
在这种情况下,采用分层架构通常是有价值的,它可以在系统的不同部分之间引入明确的关注点分离,从而使我们的代码更容易阅读、维护和测试。 概括地说,通常可以确定四个不同的层次:表现层 应用层 领域层 数据层。\
数据层位于底层,包含用于与外部数据源对话的Repository。
在数据层之上,是领域层和应用层。这些层非常重要,因为它们包含了应用程序的所有模型和业务逻辑。
在本文中,我们将以电子商务应用程序为例,重点介绍领域层。作为其中的一部分,我们将学习
- 什么是领域模型
- 如何在 Dart 中定义实体并将其表示为数据类
- 如何在模型类中添加业务逻辑
- 如何为业务逻辑编写单元测试
准备好了吗?开始吧。
什么是领域模型?
维基百科是这样定义领域模型的:
领域模型是包含行为和数据的领域概念模型。
数据可以用一组实体及其关系来表示,而行为则由一些操作这些实体的业务逻辑来编码。
以电子商务应用程序为例,我们可以确定以下实体:
- 用户:ID 和电子邮件
- 产品: ID、图片 URL、标题、价格、可用数量等。
- 项目: 产品 ID 和数量
- 购物车:项目列表、总数
- 订单: 项目列表、已付价格、状态、付款详情等。
在实践 DDD 时,实体和关系不是我们凭空产生的,而是知识发现过程(有时很漫长)的最终结果。作为该过程的一部分,领域词汇也会被正式化,并被各方使用。
请注意,在这个阶段,我们并不关心这些实体从何而来,也不关心它们在系统中是如何传递的。
重要的是,我们的实体是系统的核心,因为我们需要它们来为用户解决与领域相关的问题。
在 DDD 中,实体和值对象通常是有区别的。如需了解更多信息,请参阅 StackOverflow 上关于值对象与实体对象的主题。
当然,一旦开始构建应用程序,我们就需要实现这些实体,并决定它们在架构中的位置。
这就是领域层的作用所在。
接下来,我们将把实体称为模型,它们可以作为 Dart 中的简单类来实现。
领域层
让我们重温一下架构图:
我们可以看到,模型属于领域层。 它们由下面数据层中的存储库检索,并可由上面应用层中的Service进行修改使用。
那么,这些模型在 Dart 中是什么样子的呢? 好吧,让我们以产品模型类为例:
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 生成代码的后续文章。
请注意 Product 模型是一个简单的数据类,它不能访问Repository、Service或其他属于域层之外的对象。
模型类中的业务逻辑
然而,模型类可以包含一些业务逻辑,以表达如何修改它们。
为了说明这一点,让我们考虑一个购物车模型类:
dart
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 和数量。
由于我们可以添加和删除购物车中的商品,因此定义一个扩展来简化这项工作可能会很有用:
dart
/// 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()),修改其中的值,并返回一个新的不可变购物车对象,该对象可用于更新底层数据存储(通过相应的存储库)。
许多状态管理解决方案都依赖不可变对象来传播状态变化,并确保我们的Widget只在应该重建时才重建。规则是,当我们需要更改模型中的状态时,应通过创建一个新的、不可变的副本来实现。
测试模型中的业务逻辑
请注意 Cart 类及其 MutableCart 扩展并不依赖于域层之外的任何对象。
这使得它们非常容易测试。
为了证明这一点,我们可以编写一组单元测试来验证 addItem() 方法中的逻辑:
dart
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 模型类
- 将行为转化为在这些模型类上运行的工作代码(业务逻辑)
- 添加单元测试以验证行为的正确实现
在此过程中,请考虑需要在用户界面中显示哪些数据,以及用户将如何与之交互。
但先不要担心如何将它们连接在一起。事实上,应用层中的服务的工作是通过在数据层中的Repository和表现层中的Controller之间进行调用来使用模型。
这将是未来文章的主题。
本文翻译自:codewithandrea.com/articles/fl...
欢迎大家关注我的公众号------【群英传】,专注于「Android」「Flutter」「Kotlin」
我的语雀知识库------www.yuque.com/xuyisheng