现代化Flutter架构-Riverpod领域层

你是否曾将用户界面、业务逻辑和网络代码混杂在一起,成为一捆乱七八糟的意大利面代码?

我知道我曾这样做过。 ✋

毕竟,真实世界的应用程序开发是很困难的。

领域驱动设计(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

相关推荐
coder_pig1 小时前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
捡芝麻丢西瓜3 小时前
flutter自学笔记5- dart 编码规范
flutter·dart
恋猫de小郭3 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
sunly_1 天前
Flutter:导航,tab切换,顶部固定,列表分页滚动
开发语言·javascript·flutter
敲代码的小强1 天前
Flutter项目兼容鸿蒙Next系统
flutter·华为·harmonyos
Zh-jie2 天前
flutter 快速实现侧边栏
前端·javascript·flutter
truemi.732 天前
flutter --no-color pub get 超时解决方法
android·flutter
王家视频教程图书馆2 天前
flutter 使用dio 请求go语言后台数据接口展示瀑布流图片
flutter
迷雾漫步者2 天前
Flutter组件————AppBar
flutter·跨平台·dart
AiFlutter2 天前
Flutter 开关属性
flutter