需求背景
设计一个简单的电商平台积分系统
需求分析
借鉴竞品
先去借鉴一下其他电商平台的积分系统是怎么设计的,可以去使用一下淘宝的积分系统或者百度搜一下"淘宝积分兑换规则",就能摸清积分系统大致的功能。
积分系统的功能简单分成两类:积分的赚取渠道和兑换规则,赚取渠道比如有下单、签到、答题等,兑换规则比如有签到送10积分,答题全对送10积分,下单后返送某个下单金额比例的积分等;积分的消费渠道和兑换规则,消费渠道比如有下单抵钱、换优惠券等,兑换规则比如下单时按照某个比例抵扣用户拥有的积分、10积分换一张特定的优惠券等
挖掘细节
上面只是积分系统的简单功能,我们还需要根据产品的线框图、用户用例来细化流程,挖掘一些细节的、不容易想到的功能点
用户用例比如侧重情景化,描述了用户怎么使用我们的产品,在特定场景下完成一个功能的所有流程,包含更多细节让人更容易理解。比如积分有效期的用户用例,可以进行如下的设计:
1)用户赚取积分的时候,告知用户有效期
2)用户消费积分的时候,优先消费快过期的积分
3)用户查询积分明细的时候,显示积分的有效期和状态(是否过期)
4)用户查询总可用积分的时候,去掉已过期的积分
分析后,系统整体的功能如下:
1)积分的赚取渠道和兑换规则
2)积分的消费渠道和兑换规则
3)可用积分查询、积分明细查询
系统设计
划分模块,设计每个模块的接口、数据库、业务模型
合理地把功能划分到不同的模块
把相似的功能划到一个模块,不相似的功能划到不同的模块,使得整个模块设计"高内聚、低耦合",模块之间的关系简单、清晰
那怎么看模块划分的是否合理呢?
实现一个功能的时候,如果需要跨团队、跨部门、跨系统沟通,那么就说明模块设计的不合理,功能不够内聚
为了避免业务知识耦合,让下层模块一般比较通用,所以下层模块不要感知上层模块的业务逻辑,上层模块可以感知下层模块的一些业务逻辑
划分方式:
1)把积分赚取渠道和兑换规则、积分消费渠道和兑换规则的管理单独划分成一个营销系统,积分系统只负责积分的增删改查
2)把积分赚取渠道和兑换规则、积分消费渠道和兑换规则的管理划分到各个上层业务的系统,比如订单系统、优惠券系统等,订单系统只负责积分的增删改查
3)积分系统负责积分赚取渠道和兑换规则、积分消费渠道和兑换规则的管理以及积分的增删改查
根据我们开头提到划分思想,可以采用第一种、第二种划分方式,因为第三种划分方式积分系统需要感知订单、优惠券的各种兑换规则,下层系统感知到了上层系统的业务知识
这里我个人偏向第一种方式,因为这种方式将积分赚取、消费的管理内聚在一个营销系统模块中,修改的时候可以更集中
设计模块之间的交互关系
模块之间的交互关系分为两种:接口同步调用、消息中间件异步调用。同步调用的好处是简单直接、异步调用的好处是解耦
上层模块调用下层模块使用同步调用(因为上层模块、下层模块本身就不怎么耦合,所以可以简单直接一点),同层模块之间的调用使用异步调用进行解耦(避免同层模块耦合)
设计模块的接口、数据库、业务模型
数据库设计:
这里只需要一张积分流水明细表即可,记录用户每一笔积分的赚取和消费。
表设计
表名 | credit_transaction |
---|---|
id | 主键ID |
chan_id | 操作渠道ID,比如下单、评论等 |
event_id | 事件ID,比如订单ID、评论ID等 |
credit | 积分操作,正数代表赚取,负数代表消费 |
op_user | 操作人 |
expire_time | 积分的过期时间 |
create_time | 创建时间即操作时间 |
接口设计
接口设计要满足单一职责原则,粒度越小越通用,但是粒度小会带来一些问题
1)实现一个功能可能需要调用多个小的接口,涉及多次网络传输,性能差并且调用繁琐
2)多个接口调用如果要保证所有接口调用操作的原子性,那么需要使用分布式事务,成本很大
所以,我们可以采用facade门面模式在职责单一的细粒度接口的基础上封装一层粗粒度接口提供给外部使用
对于积分系统,我们需要提供如下几个接口:
1)积分新增 CreditIncrease(userID int, credit int)
2)积分扣减 CreditDecrease(userID int, credit int)
3)可用积分查询 CreditFind(userID int)
业务模型
采用MVC三层架构,Controller、Service、Repository
为什么要采用分层架构呢?
- 代码复用
一个Service可以被多个Controller重复使用 - 隔离变化
Repository层就将数据库的操作封装起来,只对外暴露数据操作的接口,Service层只需要使用数据操作接口不需要关注底层数据的操作逻辑,即使底层数据源由Mysql切换为Redis、由Mysql切换为ES也不会影响到Service层
Controller、Service、Repository这三层的稳定程度是不一样的,Repository是面向数据库的,数据库的结构变更频率是很小的,所以Repository层代码最稳定
而Controller层是面向用户需求的,用户需求的变更频率是很大的,所以Controller层代码是最不稳定的。分层后,Controller层的代码变更不会影响到最稳定的Repository层 - 隔离关注点
每一层只需要关注自己负责的事情,层与层之间通过接口进行数据传输
Repository层只负责数据的操作
Service层只负责处理业务逻辑
Controller层只负责接收用户请求、校验参数、编排多个Service层接口、组装VO对象 - 可测试性好
Repository层通过依赖注入的方式注入到Service层,这样我们就可以Mock Repository层的接口,将Mock的接口注入到Service,使用我们自己Mock的数据 - 应对系统复杂性
如果不分层,所有代码都在一个类里面,随着业务需求的迭代这个类的代码会非常多,就会导致可读性、可维护性急剧下降,应对复杂度最有效的办法就是拆分。
拆分分为水平拆分、垂直拆分,水平拆分就是按照业务功能拆分即划分模块,垂直拆分就是分层
BO、VO、Entity存在的意义是什么?
VO、BO、Entity分别是Controller层、Service层、Repository层定义的数据对象,实际开发中,它们可能存在大量重复的字段,甚至包含的字段一模一样
但是它们的功能语义是不一样的,所以不违反DRY原则。每层维护自己的数据对象,上下层之间通过接口进行通信,下层将自己的数据对象传给上层后,上层转换成自己的数据对象进行使用,这样每层之间都不耦合,结构更加清晰
如何解决数据对象代码重复的问题?
可以采用继承,将多个数据对象公共的字段抽到父类里面,多个数据对象都继承这个父类,因为继承关系也就一层所以继承结构也不复杂
如何解决数据对象转换问题?
可以使用一些工具包比如copier来进行数据对象转换,这样就不需要手动复制字段了
一些框架的规范是要求对象的字段都是大写,这样这些框架才可以对对象进行操作,所以为了便捷性,BO、VO、Entity的字段都需要大写,这样会破坏对象原有的封装性,只能靠程序员自己来保证对象不被错误使用