设计模式简记-设计符合设计原则的业务系统之实现
3.10 实战一:如何开发实现一个遵从设计原则的积分兑换系统?
3.10.1业务开发包含的工作
- 无外乎三方面的工作要做:接口设计、数据库设计和业务模型设计。
- 数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。
- 改动数据库表结构,需要涉及数据的迁移和适配;
- 改动接口,需要推动接口的使用者作相应的代码修改。
3.10.1.1 积分系统的数据库设计
-
只需要一张记录积分流水明细的表
积分明细表 creadit_transaction id 明细id user_id 用户id channel_id 赚取或消费渠道 event_id 相关事件ID,如订单id,评论id,优惠券换购交易id credit 积分(赚取为正,消费为负) create_time 积分赚取或消费时间 expired_time 积分过期时间
3.10.1.2 积分系统的接口设计
-
接口设计要符合单一职责原则,粒度越小通用性就越好
-
接口粒度太小也会带来一些问题:通讯开销;原子操作被分开,涉及分布式事务数据一致性问题。
-
借鉴 facade(外观)设计模式,在职责单一的细粒度接口之上,再封装一层粗粒度的接口给外部使用。
接口 参数 返回 赚取积分 userId,channelId,eventId,credit,expiredTime 积分明细ID 消费积分 userId,channelId,eventId,credit,expiredTime 积分明细ID 查询积分 userId 总可用积分 查询总积分明细 userId+分页参数 id,userId,channelId,eventId,
credit,createTime,expiredTime查询赚取积分明细 userId+分页参数 id,userId,channelId,eventId,
credit,createTime,expiredTime查询消费积分明细 userId+分页参数 id,userId,channelId,eventId,
credit,createTime,expiredTime
3.10.1.3 业务模型设计
-
三层架构的Service
^: 大部分业务系统的开发都可以分为 Controller、Service、Repository 三层。Controller 层负责接口暴露,Repository 层负责数据读写,Service 层负责核心业务逻辑,也就是这里说的业务模型
-
简单系统选择贫血开发模式
^: 基于贫血模型的传统开发模式和基于充血模型的 DDD 开发模式,前者是一种面向过程的编程风格,后者是一种面向对象的编程风格,无论是DDD还是OOP,高级开发模式应对复杂系统;积分系统业务相对比较简单,选择简单的基于贫血模型的传统开发模式就足够了。
-
简单选择跟其他业务系统一块部署
3.10.2 为什么分MVC三层开发?
3.10.2.1 分层能起到代码复用的作用
- 同一个 Repository 可能会被多个 Service 来调用,同一个 Service 可能会被多个 Controller 调用
- 满足DRY原则
3.10.2.2 分层能起到隔离变化的作用
-
分层体现了抽象和封装:Repository 层封装了对数据库访问的操作,提供了抽象的数据访问接口
-
基于接口而非实现编程的设计思想,Service 层使用 Repository 层提供的接口,并不关心其底层依赖的是哪种具体的数据库。方便替换数据库的时候:只需要改动 Repository 层的代码
-
Controller、Service、Repository 三层代码的稳定程度不同、引起变化的原因不同,分成三层来组织代码,能有效地隔离变化:Controller可能经常变化,分层后Controller变化并不影响其他层的稳定。
3.10.2.3 分层能起到隔离关注点的作用
- Repository 层只关注数据的读写。
- Service 层只关注业务逻辑,不关注数据的来源。
- Controller 层只关注与外界打交道,数据校验、封装、格式转换,并不关心业务逻辑。
- 三层之间的关注点不同,分层之后,职责分明,更加符合单一职责原则,代码的内聚性更好
3.10.2.4 分层能提高代码的可测试性
- 代码都放到一个类中,这个类的代码会因为需求的迭代而无限膨胀。
- 代码过多之后,可读性、可维护性就会变差。
- 拆分代码:拆分有垂直和水平两个方向。
- 水平方向基于业务来做拆分,就是模块化;
- 垂直方向基于流程来做拆分,就是这里说的分层。
- 还是那句话,不管是分层、模块化,还是 OOP、DDD,以及各种设计模式、原则和思想,都是为了应对复杂系统,应对系统的复杂性。对于简单系统来说,其实是发挥不了作用的,就是俗话说的“杀鸡焉用牛刀”。
3.10.3 BO、VO、Entity 存在的意义是什么?
^ : 针对 Controller、Service、Repository 三层,每层都会定义相应的数据对象,它们分别是 VO(View Object)、BO(Business Object)、Entity,例如 UserVo、UserBo、UserEntity。在实际的开发中,VO、BO、Entity 可能存在大量的重复字段,甚至三者包含的字段完全一样。在开发的过程中,经常需要重复定义三个几乎一样的类,显然是一种重复劳动。
3.10.3.1 更加推荐每层都定义各自的数据对象这种设计思路
- VO、BO、Entity 并非完全一样。比如,我们可以在 UserEntity、UserBo 中定义 Password 字段,但显然不能在 UserVo 中定义 Password 字段,否则就会将用户的密码暴露出去。
- VO、BO、Entity 三个类虽然代码重复,但功能语义不重复,从职责上讲是不一样的,是符合DRY原则的。
- 为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,层与层之间通过接口交互。对于非常大的项目来说,结构清晰是第一位的!
3.10.3.2 既然 VO、BO、Entity 不能合并,那如何解决代码重复的问题呢?
-
继承可以解决代码重复问题
可以将公共的字段定义在父类中,让 VO、BO、Entity 都继承这个父类,各自只定义特有的字段。因为这里的继承层次很浅,也不复杂,并不会影响代码的可读性和可维护性。后期如果因为业务的需要,有些字段需要从父类移动到子类,或者从子类提取到父类,代码改起来也并不复杂。
-
组合也可以解决代码重复的问题
可以将公共的字段抽取到公共的类中,VO、BO、Entity 通过组合关系来复用这个类的代码。
注意:组合可能导致数据层次问题:对象转换json会分成多个平行层次,对前端不友好
3.10.3.3 不同分层之间的数据对象该如何互相转化?
整个开发的过程会涉及“Entity 到 BO”和“BO 到 VO”这两种转化
- Java 中提供了多种数据对象转化工具,比如 BeanUtils、Dozer 等,可以大大简化繁琐的对象转化工作。
3.10.3.4 贫血模型违背封装特性如何解决?
- VO、BO、Entity 都是基于贫血模型的,而且为了兼容框架或开发库(比如 MyBatis、Dozer、BeanUtils),我们还需要定义每个字段的 set 方法。这些都违背 OOP 的封装特性,会导致数据被随意修改。
- Entity 和 VO 的生命周期是有限的,都仅限在本层范围内,即便设计成贫血、定义每个字段的 set 方法,相对来说也是安全的
- Service 层包含比较多的业务逻辑代码,所以 BO 就存在被任意修改的风险了。但是,设计的问题本身就没有最优解,只有权衡。为了使用方便,我们只能做一些妥协,放弃 BO 的封装特性,由程序员自己来负责这些数据对象的不被错误使用。
3.10.4 总结用到的设计原则和思想
原则和思想 | 说明 |
---|---|
高内聚,松耦合 | 不同功能划分到不同的模块,让模块高内聚,松耦合 |
单一职责原则 | 模块设计尽量职责单一;分层设计也是为了职责单一 |
依赖注入 | MVC:下层的类通过依赖注入的方式注入到上层的代码 |
依赖反转原则 | 通过类似spring IOC这样的容器来管理对象的创建、生命周期 |
基于接口而非实现编程 | MVC:Service层使Respository层提供的接口,不关心底层数据库类型 |
封装、抽象 | 分层体现了抽象和封装的思想,隔离关注点和变化 |
DRY与继承、组合 | VO、BO、Entity代码重复,但语义不重复,符合DRY; 解决重复问题可用到继承和组合的方法 |
面向对象设计 | 合适的功能放到合适的模块中,体现了面向对象设计(合适的代码放到合适的类) |