Flutter应用架构:域模型
如果您需要帮助为您的 Flutter 应用程序选择最适合的项目结构,可以查看:
如果您想探索其他流行架构(如 MVP、MVVM 或 Clean Architecture)并了解它们如何与此处提出的架构相比较,可以阅读以下内容:
要了解 Riverpod 架构中每个层的更多信息,请阅读本系列中的其他文章:
- Flutter riverpod应用程序架构介绍
- Flutter 应用程序架构:存储库模式(Repository Pattern)
- Flutter 应用程序架构:域模型(Domain Mode)
- Flutter 应用程序架构:演示层(Presentation Layer)
- Flutter 应用程序架构:应用层(Application Layer)
- 如何使用 Riverpod 架构获取数据并执行数据变更
- 案例: 使用 Riverpod (TMDB API) 的 Flutter 电影应用程序 使用 Flutter 和 Firebase 的时间跟踪应用程序 flutter 电商
你是否曾经将UI、业务逻辑和网络代码混在一团乱麻中?
我知道我曾经这样做过。✋
毕竟,实际应用开发很困难。
像《领域驱动设计》(DDD)这样的书籍已经写出来,帮助我们开发复杂的软件项目。
而DDD的核心在于模型,它捕捉了解决手头问题所需的重要知识和概念。拥有一个良好的域模型可能是一个软件项目成功与失败之间的关键因素。
模型非常重要,但它们不能独立存在。即使是最简单的应用程序也需要一些UI(用户看到和与之交互的内容),并且需要与外部API进行通信以显示有意义的信息。
Flutter分层架构
在这个背景下,通常有价值采用分层架构,明确地分隔系统的不同部分的关注点。这使得我们的代码更容易阅读、维护和测试。
广义上来说,通常可以识别四个不同的层次:
- 演示层
- 应用层
- 领域层
- 数据层 Flutter App 架构使用数据、领域、应用和展示层。数据层位于底部,包含用于与外部数据源交互的存储库。
在数据层的上方,我们找到了领域和应用层。这些层非常重要,因为它们包含了应用程序的所有模型和业务逻辑。
在本文中,我们将重点关注领域层,以电子商务应用程序作为实际示例。作为这个过程的一部分,我们将学习:
- 什么是域模型
- 如何定义实体并将它们表示为Dart中的数据类
- 如何为我们的模型类添加业务逻辑
- 如何编写针对该业务逻辑的单元测试
准备好了吗?让我们开始吧!
什么是域模型?(What is a Domain Model?)
维基百科这样定义域模型:
域模型是包括行为和数据的领域的概念模型。
数据可以由一组实体以及它们的关系表示,而行为是由一些用于操作这些实体的业务逻辑编码而成。
以电子商务应用程序为例,我们可以识别以下实体:
- 用户:ID和电子邮件
- 产品:ID、图像URL、标题、价格、可用数量等
- 项目:产品ID和数量
- 购物车:项目列表、总价
- 订单:项目列表、支付的价格、状态、付款细节等。 很抱歉,我似乎没有看到你提供的Markdown内容。如果你能提供具体的文本,我将尽力为你翻译成口语化和专业化的中文。 电子商务应用:实体及其关系
在实践领域驱动设计(DDD)时,实体和关系不是凭空产生的,而是一个(有时较长的)知识发现过程的最终结果。作为该过程的一部分,领域词汇也被正式化并由所有相关方使用。
请注意,此阶段我们不关心这些实体是从何而来,或者它们在系统中是如何传递的。
重要的是,我们的实体位于系统的核心,因为我们需要它们来解决与领域相关的用户问题。
在DDD中,通常会区分实体和值对象。有关更多信息,请参阅StackOverflow上的值对象与实体对象。
当然,一旦我们开始构建应用程序,我们需要实现这些实体并决定它们在架构中的位置。
这就是领域层发挥作用的地方。
在接下来的内容中,我们将把实体称为可以在Dart中实现为简单类的模型。
领域层
让我们重新查看我们的架构图:
在Flutter应用程序中,通常会使用数据(data)、领域(domain)、应用(application)和表示(presentation)等不同层次的架构。正如我们所看到的,模型通常属于领域层。它们通过位于下方的数据层的存储库进行检索,可以通过位于上方的应用层的服务进行修改。
那么在Dart中,这些模型是什么样的呢?
嗯,让我们以Product
模型类为例:
dart
class Product {
final String id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
}
上面是一个示例的Product
模型类定义。在这里,我们定义了一个包含id
(标识)、name
(名称)和price
(价格)等属性的类。这是一个非常基本的模型类,通常用于领域层,用于表示产品数据。
你可以根据你的应用程序需求和数据结构定义不同的模型类,以便在领域层中使用它们。这有助于将数据分离到领域层,以实现良好的应用程序架构。
/// 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中定义模型类及其序列化逻辑有多种方法。更多信息,请参阅我的JSON解析Dart基本指南以及关于使用Freezed进行代码生成的后续文章。
请注意,Product
模型是一个简单的数据类,没有访问存储库、服务或其他属于领域层之外的对象。
模型类中的业务逻辑
但是,模型类可以包括一些业务逻辑,以表达它们的修改方式。
为了说明这一点,让我们考虑一个 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` 对象,该对象可用于更新底层的数据存储(通过相应的存储库)。 > 如果您对上述语法不熟悉,请阅读:[如何在Dart中更新键值对的Map](https://codewithandrea.com/tips/dart-map-update-method/)。 > 许多状态管理解决方案依赖于不可变对象,以传播状态更改,并确保我们的小部件仅在必要时重新构建。规则是,当我们需要在模型中更改状态时,应通过创建新的不可变副本来执行。
测试模型内部的业务逻辑
请注意,`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模型类
将行为转化为在这些模型类上运行的工作代码(业务逻辑)
添加单元测试以验证行为是否正确实现
在执行这些操作时,请考虑需要在用户界面中显示哪些数据以及用户将如何与其交互。
但暂时不必担心如何连接各个部分。事实上,应用层中的服务的任务是通过在数据层的存储库和演示层的控制器之间进行调解来处理模型。
这将是未来文章的主题。