前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter App架构:领域模型

Flutter App架构:领域模型

作者头像
用户1974410
发布2022-09-20 16:59:28
1.3K0
发布2022-09-20 16:59:28
举报
文章被收录于专栏:flutter开发精选flutter开发精选

你是不是曾经在代码里把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和展示层的工作,下一篇文章讲详细的讲解。

少年别走,交个朋友~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-02-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 flutter开发精选 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • flutter的分层结构
  • 什么是领域模型?
  • Domain Layer
  • Model class中的业务逻辑
  • 在我们的模型中测试业务逻辑
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档