王争《设计模式之美》学习笔记
业务开发包括哪些工作?
- 我们平时做业务系统的设计与开发,无外乎有这样三方面的工作要做:接口设计、数据库设计和业务模型设计(也就是业务逻辑)。
- 数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。
- 改动数据库表结构,需要涉及数据的迁移和适配
- 改动接口,需要推动接口的使用者作相应的代码修改
- 业务逻辑代码侧重内部实现,不涉及被外部依赖的接口,也不包含持久化的数据,所以对改动的容忍性更大。
针对积分系统,我们先来看,如何设计数据库。
- id:明细ID
- user_id:用户ID
- channel_id:赚取或消费渠道ID
- event_id:相关事件ID,比如订单、评论、优惠券换购交易等
- credit:积分
- create_time:积分赚取或消费时间
- expired_time:积分过期时间
接下来,我们再来看,如何设计积分系统的接口。
- 接口设计要符合单一职责原则,粒度越小通用性就越好。但是接口粒度太小也会带来一些问题:
- 多次调用小接口,增加网络开销
- 原子操作拆成多个小接口,涉及分布式事务的数据一致性问题
- 文中设计如下几个接口:赚取积分、消费积分、查询积分、查询总积分明细、查询赚取积分明细、查询消费积分明细。
最后,我们来看业务模型的设计。
- 文中对于我们要开发的积分系统来说,因为业务相对比较简单,所以,选择简单的基于贫血模型的传统开发模式就足够了。(这点我不太认同,首先积分系统并不是简单系统,其次设计上还是要有发展的思维)
- 文中甚至认为积分系统小到可以和营销系统放在一个项目中开发部署,我也不能认同,前面的数据库字段设计也过于简单。
为什么要分 MVC 三层开发
分层能起到代码复用的作用
- 同一个 Repository 可能会被多个 Service 来调用,同一个 Service 可能会被多个 Controller 调用。
分层能起到隔离变化的作用
- 分层体现了一种抽象和封装的设计思想。比如,Repository 层封装了对数据库访问的操作,当我们需要替换数据库的时候,只需要改动 Repository 层的代码,Service 层的代码完全不需要修改。
- 除此之外,Controller、Service、Repository 三层代码的稳定程度不同、引起变化的原因不同,所以分成三层来组织代码,能有效地隔离变化。
分层能起到隔离关注点的作用
- Repository 层只关注数据的读写。
- Service 层只关注业务逻辑,不关注数据的来源。
- Controller 层只关注与外界打交道,数据校验、封装、格式转换,并不关心业务逻辑。
分层能提高代码的可测试性
- Repsitory 层的代码通过依赖注入的方式供 Service 层使用,当要测试包含核心业务逻辑的 Service 层代码的时候,我们可以用 mock 的数据源替代真实的数据库,注入到 Service 层代码中。
分层能应对系统的复杂性
- 所有的代码都放到一个类中,那这个类的代码就会因为需求的迭代而无限膨胀。
- 拆分有垂直和水平两个方向。水平方向基于业务来做拆分,就是模块化;垂直方向基于流程来做拆分,就是这里说的分层。
BO、VO、Entity 存在的意义是什么
相对于每层定义各自的数据对象来说,是不是定义一个公共的数据对象更好些呢?
- 实际上,我更加推荐每层都定义各自的数据对象这种设计思路,主要有以下 3 个方面的原因:
- VO、BO、Entity 并非完全一样。
- VO、BO、Entity 三个类虽然代码重复,但功能语义不重复,从职责上讲是不一样的。
- 为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,层与层之间通过接口交互。
既然 VO、BO、Entity 不能合并,那如何解决代码重复的问题呢?
- 继承可以解决代码重复问题。
- 组合也可以解决代码重复的问题,所以,这里我们还可以将公共的字段抽取到公共的类中,VO、BO、Entity 通过组合关系来复用这个类的代码。
代码重复问题解决了,那不同分层之间的数据对象该如何互相转化呢?
- 最简单的转化方式是手动复制。
- 也可以借鉴对象转化工具,文中举例的是JAVA,我感觉PHP的话没啥可转化的。
VO、BO、Entity 都是基于贫血模型的,而且为了兼容框架或开发库(比如 MyBatis、Dozer、BeanUtils),我们还需要定义每个字段的 set 方法。这些都违背 OOP 的封装特性,会导致数据被随意修改。那到底该怎么办好呢?
- Entity 和 VO 的生命周期是有限的,都仅限在本层范围内。而对应的 Repository 层和 Controller 层也都不包含太多业务逻辑,所以也不会有太多代码随意修改数据,即便设计成贫血、定义每个字段的 set 方法,相对来说也是安全的。
- Service 层包含比较多的业务逻辑代码,所以 BO 就存在被任意修改的风险了。但是,设计的问题本身就没有最优解,只有权衡。为了使用方便,我们只能做一些妥协,放弃 BO 的封装特性,由程序员自己来负责这些数据对象的不被错误使用。
总结用到的设计原则和思想
- 高内聚、松耦合:上节课中,我们将不同的功能划分到不同的模块,遵从的划分原则就是尽量让模块本身高内聚,让模块之间松耦合。
- 单一职责原则:上节课中,我们讲到模块的设计要尽量职责单一,符合单一职责原则。这节课中,分层的一个目的也是为了更加符合单一职责原则。
- 依赖注入:在MVC三层结构的代码实现中,下一层的类通过依赖注入的方式注入到上一层代码中。
- 依赖反转原则:在业务系统开发中,如果我们通过类似Spring IOC这样的容器来管理对象的创建、生命周期。那就用到了依赖反转原则。
- 基于接口而非实现编程:在MVC三层结构的代码实现中,Service 层使用 Repository 层提供的接口,并不关心其底层是依赖的那种具体的数据库,遵从基于接口而非实现编程的设计思想。
- 封装、抽象:分层体现了抽象和封装的设计思想,能够隔离变化,隔离关注点。
- DRY与继承和组合:尽管VO、BO、Entity存在代码重复,但功能语义不同,并不违反DRY原则。为了解决三者之间的代码重复问题,我们还用到了继承或组合。
- 面向对象设计:系统设计的过程可以参照面向对象设计的步骤来做。面向对象设计本质是将合适的代码放到合适的类中。系统设计是将合适的功能放到合适的模块中。
版权声明:本文为linglongwunv原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。