第二部分:实战一 实战一(上)

需求分析

  • 文中举例,一个积分兑换系统的开发实战,技术人员应该更多地参与到产品设计中。
  • 作为技术人,我该怎么做产品设计呢?首先,一定不要自己一个人闷头想。一方面,这样做很难想全面。另一方面,从零开始设计也比较浪费时间。所以,我们要学会“借鉴”。
  • 除了“借鉴”的思路,还可以通过产品的线框图、用户用例(user case )或者叫用户故事(userstory)来细化业务流程,挖掘一些比较细节的、不容易想到的功能点。
  • 用户用例有点儿类似我们后面要讲的单元测试用例。它侧重情景化,其实就是模拟用户如何使用我们的产品,描述用户在一个特定的应用场景里的一个完整的业务操作流程。
  • 有关积分有效期的用户用例,我们可以进行如下的设计:
    • 用户在获取积分的时候,会告知积分的有效期
    • 用户在使用积分的时候,会优先使用快过期的积分
    • 用户在查询积分明细的时候,会显示积分的有效期和状态(是否过期)
    • 用户在查询总可用积分的时候,会排除掉过期的积分。
  • 通过上面讲的方法,积分系统的需求如下:
    • 积分赚取和兑换规则:
      • 积分的赚取渠道包括:下订单、每日签到、评论等。
      • 对于积分的有效期,我们可以根据不同渠道,设置不同的有效期。
    • 积分消费和兑换规则:
      • 积分的消费渠道包括:抵扣订单金额、兑换优惠券、积分换购、参与活动扣积分等。
      • 积分到期之后会作废;在消费积分的时候,优先使用快到期的积分。
    • 积分及其明细查询:查询用户的总积分,以及赚取积分和消费积分的历史记录。

系统设计

合理地将功能划分到不同模块

  • 第一种划分方式是:积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护(增删改查),不划分到积分系统中,而是放到更上层的营销系统中。
    • 这样积分系统就会变得非常简单,只需要负责增加积分、减少积分、查询积分、查询积分明细等这几个工作。
    • 比如,用户通过下订单赚取积分,营销系统根据拿到的订单信息,查询订单对应的积分兑换规则(兑换比例、有效期等),计算得到订单可兑换的积分数量,然后调用积分系统的接口给用户增加积分。
  • 第二种划分方式是:积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护,分散在各个相关业务系统中,比如订单系统、评论系统、签到系统、换购商城、优惠券系统等。
    • 用户下订单成功之后,订单系统根据商品对应的积分兑换比例,计算所能兑换的积分数量,然后直接调用积分系统给用户增加积分。
  • 第三种划分方式是:所有的功能都划分到积分系统中,包括积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护。
    • 用户下订单成功之后,订单系统直接告知积分系统订单交易成功,积分系统根据订单信息查询积分兑换规则,给用户增加积分。
  • 为了避免业务知识的耦合,让下层系统更加通用,一般来讲,我们不希望下层系统(也就是被调用的系统)包含太多上层系统(也就是调用系统)的业务信息,但是,可以接受上层系统包含下层系统的业务信息。
  • 综合考虑,我们更倾向于第一种和第二种模块划分方式。

设计模块与模块之间的交互关系

  • 比较常见的系统之间的交互方式有两种,一种是同步接口调用,另一种是利用消息中间件异步调用。第一种方式简单直接,第二种方式的解耦效果更好。
  • 上下层系统之间的调用倾向于通过同步接口,同层之间的调用倾向于异步消息调用。

设计模块的接口、数据库、业务模型

  • 下节课详细讲解。
实战一(下)

业务开发包括哪些工作?

  • 我们平时做业务系统的设计与开发,无外乎有这样三方面的工作要做:接口设计、数据库设计和业务模型设计(也就是业务逻辑)。
  • 数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。
    • 改动数据库表结构,需要涉及数据的迁移和适配
    • 改动接口,需要推动接口的使用者作相应的代码修改
  • 业务逻辑代码侧重内部实现,不涉及被外部依赖的接口,也不包含持久化的数据,所以对改动的容忍性更大。

针对积分系统,我们先来看,如何设计数据库。

  1. id:明细ID
  2. user_id:用户ID
  3. channel_id:赚取或消费渠道ID
  4. event_id:相关事件ID,比如订单、评论、优惠券换购交易等
  5. credit:积分
  6. create_time:积分赚取或消费时间
  7. expired_time:积分过期时间

第二部分:实战一_数据

接下来,我们再来看,如何设计积分系统的接口。

  • 接口设计要符合单一职责原则,粒度越小通用性就越好。但是接口粒度太小也会带来一些问题:
    • 多次调用小接口,增加网络开销
    • 原子操作拆成多个小接口,涉及分布式事务的数据一致性问题
  • 文中设计如下几个接口:赚取积分、消费积分、查询积分、查询总积分明细、查询赚取积分明细、查询消费积分明细。

第二部分:实战一_数据_02

最后,我们来看业务模型的设计。

  • 文中对于我们要开发的积分系统来说,因为业务相对比较简单,所以,选择简单的基于贫血模型的传统开发模式就足够了。(这点我不太认同,首先积分系统并不是简单系统,其次设计上还是要有发展的思维)
  • 文中甚至认为积分系统小到可以和营销系统放在一个项目中开发部署,我也不能认同,前面的数据库字段设计也过于简单。

为什么要分 MVC 三层开发

分层能起到代码复用的作用

  • 同一个 Repository 可能会被多个 Service 来调用,同一个 Service 可能会被多个 Controller 调用。

分层能起到隔离变化的作用

  • 分层体现了一种抽象和封装的设计思想。比如,Repository 层封装了对数据库访问的操作,当我们需要替换数据库的时候,只需要改动 Repository 层的代码,Service 层的代码完全不需要修改。
  • 除此之外,Controller、Service、Repository 三层代码的稳定程度不同、引起变化的原因不同,所以分成三层来组织代码,能有效地隔离变化。

分层能起到隔离关注点的作用

  • Repository 层只关注数据的读写。
  • Service 层只关注业务逻辑,不关注数据的来源。
  • Controller 层只关注与外界打交道,数据校验、封装、格式转换,并不关心业务逻辑。

分层能提高代码的可测试性

  • Repsitory 层的代码通过依赖注入的方式供 Service 层使用,当要测试包含核心业务逻辑的 Service 层代码的时候,我们可以用 mock 的数据源替代真实的数据库,注入到 Service 层代码中。

分层能应对系统的复杂性

  • 所有的代码都放到一个类中,那这个类的代码就会因为需求的迭代而无限膨胀。
  • 拆分有垂直和水平两个方向。水平方向基于业务来做拆分,就是模块化;垂直方向基于流程来做拆分,就是这里说的分层。

BO、VO、Entity 存在的意义是什么

相对于每层定义各自的数据对象来说,是不是定义一个公共的数据对象更好些呢?

  • 实际上,我更加推荐每层都定义各自的数据对象这种设计思路,主要有以下 3 个方面的原因:
    1. VO、BO、Entity 并非完全一样。
    2. VO、BO、Entity 三个类虽然代码重复,但功能语义不重复,从职责上讲是不一样的。
    3. 为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,层与层之间通过接口交互。

既然 VO、BO、Entity 不能合并,那如何解决代码重复的问题呢?

  • 继承可以解决代码重复问题。
  • 组合也可以解决代码重复的问题,所以,这里我们还可以将公共的字段抽取到公共的类中,VO、BO、Entity 通过组合关系来复用这个类的代码。

代码重复问题解决了,那不同分层之间的数据对象该如何互相转化呢?

  • 最简单的转化方式是手动复制。
  • 也可以借鉴对象转化工具,文中举例的是JAVA,我感觉PHP的话没啥可转化的。

VO、BO、Entity 都是基于贫血模型的,而且为了兼容框架或开发库(比如 MyBatis、Dozer、BeanUtils),我们还需要定义每个字段的 set 方法。这些都违背 OOP 的封装特性,会导致数据被随意修改。那到底该怎么办好呢?

  • Entity 和 VO 的生命周期是有限的,都仅限在本层范围内。而对应的 Repository 层和 Controller 层也都不包含太多业务逻辑,所以也不会有太多代码随意修改数据,即便设计成贫血、定义每个字段的 set 方法,相对来说也是安全的。
  • Service 层包含比较多的业务逻辑代码,所以 BO 就存在被任意修改的风险了。但是,设计的问题本身就没有最优解,只有权衡。为了使用方便,我们只能做一些妥协,放弃 BO 的封装特性,由程序员自己来负责这些数据对象的不被错误使用。

总结用到的设计原则和思想

  1. 高内聚、松耦合:上节课中,我们将不同的功能划分到不同的模块,遵从的划分原则就是尽量让模块本身高内聚,让模块之间松耦合。
  2. 单一职责原则:上节课中,我们讲到模块的设计要尽量职责单一,符合单一职责原则。这节课中,分层的一个目的也是为了更加符合单一职责原则。
  3. 依赖注入:在MVC三层结构的代码实现中,下一层的类通过依赖注入的方式注入到上一层代码中。
  4. 依赖反转原则:在业务系统开发中,如果我们通过类似Spring IOC这样的容器来管理对象的创建、生命周期。那就用到了依赖反转原则。
  5. 基于接口而非实现编程:在MVC三层结构的代码实现中,Service 层使用 Repository 层提供的接口,并不关心其底层是依赖的那种具体的数据库,遵从基于接口而非实现编程的设计思想。
  6. 封装、抽象:分层体现了抽象和封装的设计思想,能够隔离变化,隔离关注点。
  7. DRY与继承和组合:尽管VO、BO、Entity存在代码重复,但功能语义不同,并不违反DRY原则。为了解决三者之间的代码重复问题,我们还用到了继承或组合。
  8. 面向对象设计:系统设计的过程可以参照面向对象设计的步骤来做。面向对象设计本质是将合适的代码放到合适的类中。系统设计是将合适的功能放到合适的模块中。

第二部分:实战一_封装_03