Flutter App架构:领域模型

2022-09-20 16:59:28 浏览数 (1)

你是不是曾经在代码里把UI、业务逻辑、网络请求混在一个类里,看起来像一锅大杂烩?我也这样做过 ✋。总而言之,APP开发是困难的。像领域驱动设计Domain-Driven Design (DDD) 之类的书可以帮助我们开发复杂的软件工程项目。DDD的核心是model,是我们要解决的问题需要掌握的重要知识和概念。一个好的领域模型是决定一个项目成功或失败的重要因素。模型很重要,但也不会脱离系统。最简单的app也需要一些UI(就是用户所看到好)和与服务端的接口交互,用来获取有意义的信息。

flutter的分层结构

在app开发中,引入分层结构通常是有价值的,这样就可以在系统的不同部分之间有着明确的关注点分离。也能够使得我们的代码更加容易阅读、维护和测试。一般来说,我们通常可以把APP设计分为4层:

  • 「presentation layer」
  • 「application layer」
  • 「domain layer」
  • 「data layer」

Data Layer是最底层,用来和外部数据交互,详细可见我之前的文章。

❝Flutter App架构:Repository 设计模式 ❞

在Data Layer之上是「Domain」「application」 Layer,这两层是业务逻辑和模型的关键部分。

本文,我们将聚焦在「domain layer」,使用一个购物APP作为练习。在本文你将学到以下内容:

  • 什么是领域模型?
  • 在Dart中怎样定义实体类和展示它们。
  • 在model 类中添加业务逻辑
  • 为业务逻辑编写单元测试

什么是领域模型?

维基百科有如下的定义:

❝The domain model is a conceptual model of the domain that incorporates both behavior and data. ❞

数据能够被一系列的实体和实体间的关系所表示,它们的行为能够通过实体类体现出业务逻辑并且能够被操作。

一个购物APP我们能够定义出如下实体:

  • 「User」: ID 和 email
  • 「Product」: ID, image URL, title, price, available quantity etc.
  • 「Item」: Product ID 和 quantity
  • 「Cart」: List of items, total
  • 「Order」: List of items, price paid, status, payment details etc

❝当我们实践DDD时,实体和关系不是无中生有,而是一个知识发现过程的最终结果(有时很长时间)。作为该过程的一部分,领域词汇表也被形式化,供各部分使用。 ❞

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

实体类是我们app的关键部分,因为它为用户解决了领域关系的难题。

❝在 DDD中, 经常会比较实体类和实体对象的区别,详细可以查看:Value vs Entity objects on StackOverflow(https://stackoverflow.com/questions/75446/value-vs-entity-objects-domain-driven-design) ❞

当我们构建APP,就需要实现这些实体类,并决定它们在App架构中的位置。所以我们需要在架构中引入domain layer。

Domain Layer

我们再看看我们的app架构图:

如图所示,models正是在domain layer,它所处的位置是承上启下,能从下层 data layer获取数据,并且被上层的services layer进行数据处理。

下面我们来看看这些实体在dart中长什么样。

我们以Product这个实体为例:

代码语言:javascript复制
/// 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() 帮助我们进行序列化。

请记住 Product模型是一个简单的数据类,不需要访问repositories, services和其他领域层外的对象。

Model class中的业务逻辑

Model classes也能包含一些业务逻辑,也就意味着它可以被修改。

我们下面来看看一个购物车模型类的实现:

代码语言:javascript复制
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() { ... }
}

这里我们使用map来存储加入购物车的产品ID和对应的数量。我们还需要为购物车添加一个加入购物车和移出购物车的功能,我们使用extension方法来实现:

代码语言:javascript复制
/// 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);
  }
}

上面的方法先复制了一份购物车的列表,然后修改对应的值,最后返回新的immutable的Cart对象。

❝许多状态管理的实现依赖于 **immutable objects,**这样能够正确的传递状态,并且是我们的widget能够在被正确的刷新 所以这里有一条规则,无论何时我们都不要使用mutate state,而是创建新的 「immutable copy。」

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

现在我们 Cart 类和 MutableCart extension 没有依赖任何领域层外的任何对象,所有对他们的测试相对容易。

下面我们实现一个针对addItem方法的单元测试:

代码语言:javascript复制
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});
    });
  });
}

虽然单元测试不好写,但是能够保证我们APP的健壮性,所以大家还是多谢单测,少写bug。

总结

本文讨论了好的领域模型对我们系统的重要性。也展示了如何定义实体类,以及使用immutable data方式处理我们的业务逻辑。最后也学习了如何为业务逻辑表现单元测试,领域层的单测比较简单,不会有复杂的mock和其他设置。


下面有一些设计和开发APP的小提示:

  • 理解领域模型,找出哪些概念和行为是你需要在代码里表示出来的
  • 将行为转换为操作那些模型类的代码(业务逻辑)
  • 实现相应的Dart模型类
  • 将这些概念及其关系表示为实体类
  • 增加单元测试验证业务逻辑

当你做到以上内容,并且思考哪些内容是和用户有关系并且需要在页面上展示的,你的App可能就是一个好用的app了。

目前不需要担心这些models是怎样在ui展示的,这些都是services和展示层的工作,下一篇文章讲详细的讲解。

少年别走,交个朋友~

0 人点赞