Flutter应用程序架构之领域模型

166 阅读8分钟

你是否曾经把你的用户界面业务逻辑网络代码混在一起,变成一捆混乱的意大利面条代码?

我知道我做过。✋

毕竟,现实世界的应用开发是很难的。

诸如领域驱动设计(DDD)之类的书籍被写出来,以帮助我们开发复杂的软件项目。

而DDD的核心在于模型,它捕获了解决手头问题所需的重要知识概念。而拥有一个好的领域模型可以使一个软件项目的成败完全不同。

模型非常重要,但它们不能孤立地存在。即使是最简单的应用程序也需要一些UI(用户看到的和与之互动的东西),并且需要与外部的API沟通,以显示一些有意义的信息。

Flutter的分层架构

在这种情况下,采用分层架构往往很有价值,在系统的不同部分之间引入明确的关注点分离。而这使得我们的代码更容易阅读维护测试

大体上说,通常会确定四个不同的层。

  • 表现层
  • 应用层
  • 领域层
  • 数据层

Flutter应用架构使用数据层、领域层、应用层和表现层。

数据层位于底部,包含用于与外部数据源对话的存储库。

就在它上面,我们发现领域应用层。这些层非常重要,因为它们包含了我们应用程序的所有模型业务逻辑。欲了解更多信息,请阅读本系列的其他文章。

在这篇文章中,我们将重点讨论领域层,以电子商务应用为实际例子。作为其中的一部分,我们将学习。

  • 什么是领域模型
  • 如何在Dart中定义实体并将其表现为数据类
  • 如何为我们的模型类添加业务逻辑
  • 如何为该业务逻辑编写单元测试

准备好了吗?我们开始吧

什么是领域模型?

维基百科对领域模型是这样定义的。

领域模型是一个包含了行为和数据的领域概念模型

数据可以由一组实体及其关系来表示,而行为则由一些操作这些实体的业务逻辑来编码。

以一个电子商务应用为例,我们可以确定以下实体。

  • 用户:ID和电子邮件
  • 产品。ID、图片URL、标题、价格、可用数量等。
  • 项目。产品ID和数量
  • 购物车:物品列表,总数
  • 订单。项目列表,支付的价格,状态,支付细节等。

电子商务应用:实体和它们的关系

在实践DDD时,实体和关系不是我们凭空产生的东西,而是一个(有时是漫长的)知识发现过程的最终结果。作为这个过程的一部分,领域词汇也被正式化,并被所有各方使用。

请注意,在这个阶段,我们并不关心这些实体来自哪里,或者它们是如何在系统中传递的。

重要的是,我们的实体是我们系统的核心,因为我们需要它们来为我们的用户解决领域相关的问题。

在DDD中,实体价值对象之间经常会有区别。欲了解更多信息,请参见StackOverflow上关于价值与实体对象的这个主题。

当然,一旦我们开始构建我们的应用程序,我们就需要实现这些实体,并决定它们在我们的架构中的位置。

而这就是领域层的作用。

今后,我们将把实体称为模型,可以在Dart中作为简单的类来实现。

领域层

让我们重新审视我们的架构图。

使用数据层、领域层、应用层和表现层的Flutter应用程序架构。

正如我们所见,模型属于领域层。它们由下面的数据层中的存储库检索,并可由上面的应用层中的服务修改。

那么,这些模型在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() {
    ...
  }
}

至少,这个类持有我们需要在用户界面中显示的所有属性。

产品卡使用了我们模型类的所有属性。

而且它还包含了用于序列化的fromMap()toMap() 方法。

在Dart中,有多种方法来定义模型类和它们的序列化逻辑。更多信息,请看我的Dart中JSON解析的基本指南和后续的关于使用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中更新一个键值对的地图

许多状态管理解决方案依赖于不可变的对象,以传播状态变化,并确保我们的小部件只在应该的时候重建。规则是,当我们需要改变模型中的状态时,我们应该通过制作一个新的、不可变的副本来实现。

测试我们模型中的业务逻辑

请注意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模型类
  • 将行为转化为工作代码(业务逻辑),对这些模型类进行操作
  • 添加单元测试,以验证行为的正确实现。

当你这样做时,想想你需要在用户界面中显示什么数据,以及用户将如何与之互动。

但先不要担心事情如何连接在一起。事实上,应用层服务的工作是通过在数据层的存储库和表现层的控制器之间进行调解来与模型合作。

而这将是未来文章的主题。