设计模式:积分兑换系统的设计与实现
作者:互联网
积分是一种常见的营销手段,很多产品都会用它来促进消费、增加用户粘性。那应该怎么才能实现一个积分系统呢?也就是怎么做产品设计呢?
(1)首先,一定不要自己一个人闷头想。一方面,这样做很难想全面。另一方面,从零开始设计也比较浪费时间。
- 我们可以找几个类似的产品,比如淘宝,看看它们是如何设计积分系统的,然后借鉴到我们的产品中。
- 笼统地来讲,积分系统无外乎就两个大的功能点,一个是赚取积分,另一个是消费积分。赚取积分功能包括积分赚取渠道,比如下订单、每日签到、评论等;还包括积分兑换规则,比如订单金额与积分的兑换比例,每日签到赠送多少积分等。消费积分功能包括积分消费渠道,比如抵扣订单金额、兑换优惠券、积分换购、参与活动扣积分等;还包括积分兑换规则,比如多少积分可以换算成抵扣订单的多少金额,一张优惠券需要多少积分来兑换等等。
- 上面只是一些非常笼统、粗糙的功能需求。在实际情况中,肯定还有一些业务细节需要考虑,比如积分的有效期问题。对于这些业务细节,还是那句话,闷头拍脑袋想是想不全面的。以防遗漏,我们还是要有方法可寻。那除了刚刚讲的“借鉴”的思路之外,还可以通过产品的**线框图、用户用例(user case)**或者叫做用户故事(user story)来细化业务流程,挖掘一些比较细节的、不容易想到的功能点。
- 比如用户用例。用户用例有点类型单元测试用例。它侧重情景化,其实就是模拟用户如何使用产品,描述用户在一个特定的应用场景里的一个完整的业务操作流程。所以,它包含更多的细节,且更加容易被人理解。比如,有关积分有效期的用户用例,我们可以进行如下的设计:
- 用户在获取积分的时候,会告知积分的有效期;
- 用户在使用积分的时候,会优先使用快过期的积分;
- 用户在查询积分明细的时候,会显示积分的有效期和状态(是否过期);
- 用户在查询总可用积分的时候,会排除掉过期的积分。
(2)通过上面讲的方法,我们就可以将功能需求大致弄清楚了。积分系统的需求总结如下:
- 积分赚取和兑换规则
- 积分的赚取渠道包括:下订单、每日签到、评论等。
- 积分兑换规则可以是比较通用的。比如,签到送 10 积分。再比如,按照订单总金额的10% 兑换成积分,也就是 100 块钱的订单可以积累 10 积分。除此之外,积分兑换规则也可以是比较细化的。比如,不同的店铺、不同的商品,可以设置不同的积分兑换比例。
- 对于积分的有效期,我们可以根据不同渠道,设置不同的有效期。积分到期之后会作废;在消费积分的时候,优先使用快到期的积分。
- 积分消费和兑换规则
- 积分的消费渠道包括:抵抗订单金额、兑换优惠券、积分换购、参与活动扣积分等。
- 我们可以根据不同的消费渠道,设置不同的积分兑换规则。比如,积分换算成消费抵扣金额的比例是 10%,也就是 10 积分可以抵扣 1 块钱;100 积分可以兑换 15 块钱的优惠券等。
- 积分及其明细查询:查询用户的总积分,以及赚取积分和消费积分的历史记录。
系统设计
面向对象设计聚集在代码层面(主要是针对类),那系统设计就是聚集在架构层面(主要是针对模块)。针对业务系统,其设计步骤如下
第一步:合理的将功能划分到不同模块
面向对象设计的本质就是把合适的代码放在合适的类中。合理的划分代码可以实现代码的高内聚、低耦合,类与类之间的交互简单清晰,代码整体结构一目了然,那代码的质量不会差到哪里去。类比面向对象设计,系统设计实际上就是把合适的功能放到合适的模块中。合理的划分模块也可以做到模块层面的高内聚、低耦合,架构整洁清晰
对于前面罗列的所有功能点,我们有下面三种模块划分方法。
(1)积分赚取渠道及兑换规则、消费渠道以及兑换规则的管理和维护,不划分到积分系统中,而是放到更上层的营销系统中,这样积分系统就会变得非常简单,只需要负责增加积分、减少积分,查询积分明细这几个工作。
举个例子解释一下。比如,用户通过下订单赚取积分。订单系统通过异步发送消息或者同步调用接口的方式,告知营销系统订单交易成功。营销系统根据拿到的订单信息,查询订单对应的积分兑换规则(兑换比例、有效期等),计算得到订单可兑换的积分数量,然后调用积分系统的接口给用户增加积分。
(2)积分赚取渠道以及兑换规则、消费渠道以及兑换规则的管理和维护,分散在各个相关业务系统中,比如订单系统、评论系统、签到系统、换购商城、优惠券系统等。还是刚刚那个下订单赚取积分的例子,在这种情况下,用户下订单成功之后,订单系统根据商品对应的积分兑换比例,计算所能兑换的积分数量,然后直接调用积分系统给用户增加积
分。
(3)所有的功能都划分到积分系统中,包括积分赚取渠道及兑换规则、消费渠道及兑换规则的管理和维护。还是同样的例子,用户下订单成功之后,订单系统直接告知积分系统订单交易成功,积分系统根据订单信息查询积分兑换规则,给用户增加积分。
怎么判断哪种模块划分合理呢?
- 实际上,我们可以反过来通过看它是否符号高内聚、低耦合特性来判断。如果一个功能的修改或者添加,经常要跨团队、跨项目、跨系统才能完成,那说明模块划分的不够合理,职责不够清晰,耦合过于严重。
- 除此之外,为了避免业务知识的耦合,让下层系统更加通用,一般来讲,我们不希望下层系统(也就是被调用的系统)包含太多上层系统(也就是调用系统)的业务信息,但是,可以接受上层系统包含下层系统的业务信息。比如,订单系统、优惠券系统、换购商城等作为调用积分系统的上层系统,可以包含一些积分相关的业务信息。但是,反过来,积分系统中最好不要包含太多跟订单、优惠券、换购等相关的信息。
- 所以,综合考虑,我们更倾向于第一种和第二种模块划分方式。但是,不管选择这两种的的哪一种,积分系统所负责的工作是一样的,只包含积分的增减查询以及积分明细的记录和查询。
第二步:设计模块和模块之间的交互关系
在面向对象设计中,类设计好之后,我们需要设计类之间的交互关系。类比到系统设计,系统职责划分好之后,接下来就是设计系统之间的交互,也就是确定有哪些系统跟积分系统之间有交互以及如何进行交互。
比较常见的系统之间的交互关系有两种,一种是同步接口调用,另一种是利用消息中间件异步调用。第一种方式简单直接,第二种方式解耦效果更好。
比如,用户下订单成功之后,订单系统推送一条消息到消息中间件,营销系统订阅订单成功消息,触发执行相应的积分兑换逻辑。这样订单系统就跟营销系统完全解耦,订单系统不需要知道任何跟积分有关的逻辑,而影响系统也不需要直接跟订单系统交互。
除此之外,上下层系统之间的调用倾向同步接口,同层之间的调用倾向于异步消息调用。比如,比如,营销系统和积分系统是上下层关系,它们之间就比较推荐使用同步接口调用。
第三步:设计模块的接口、数据库、业务模型
接下来就是模块本身如何设计了。实际上,业务系统本身的设计无外乎有这样三方面的工作要做:接口设计、数据库设计和业务模型设计。也就是说,这个系统怎么实现
如何实现一个遵从设计原则的积分兑换系统?
业务开发包括哪些工作
实际上,我们平时做业务系统的设计与开发,无外乎有这样三方面的工作要做:接口设计、数据库设计和业务模型设计(也就是业务逻辑)。
数据库和接口的设计非常重要,一旦设计好并投入使用之后,这两部分都不能轻易改动。改动数据库表结构,需要涉及数据的迁移和适配。改动接口,需要推动接口的使用者作相应的代码修改。这两种情况,即便是微小的改动,执行起来都会非常麻烦。因此,我们在设计接口和数据库的时候,一定要多花点心思和时间,切不可过于随意。相反,业务逻辑代码侧重内部实现,不涉及被外部依赖的接口,也不包含持久化的数据,所以对改动的容忍性更大。
针对积分系统,我们先来看,如何设计数据库。
数据库的设计比较简单。实际上,我们只需要一张记录积分流水明细的表就可以了。表中记录积分的赚取和消费流水。用户积分的各种统计数据,比如总积分、总可用积分等,都可以通过这张表来计算得到。
接下来,我们再来看,如何设计积分系统的接口。
接口设计要符合单一职责原则,粒度越小通用性越好。但是,接口粒度太小也会带来一些问题。比如,一个功能的实现要调用多个小接口,一方面如果接口调用走网络(特别是公网),多次远程接口调用会影响性能;另一方面,本来应该在一个接口中完成的原子操作,现在分拆成多个小接口来完成,就可能会涉及到分布式事务的数据一致性问题(一个接口执行成功了,但另一个接口执行失败了)。所以,为了兼顾易用性和性能,我们可以借鉴facade(外观)模式,在职责单一的细粒度接口之上,再封装一层粗粒度的接口给外部使用。
对于积分系统来说,我们需要设计如下这样几个接口。
最后,我们来看业务模型的设计。
从代码实现的角度来说,大部分业务系统的开发都可以分为Controller、Service、Repository三层。Controller层负责接口暴露,Repositorty负责数据读写,Service负责核心业务逻辑,也就是这里说的业务模型。
另外,还有两种开发模式,基于贫血模型的传统开发模型和基于充血模型的DDD开发模式。前者是一种面向过程的编程分格,后者是一种面向对象的编程风格。不管是DDD还是iOOP,高级开发模式的存在一般都是为了应对复杂系统,应对系统的复杂性。对于我们要开发的积分系统来说,因为业务相对比较简单,所以,选择简单的基于贫血模型的传统开发模式就足够了。
从开发的角度来说,我们可以把积分系统作为一个独立的项目,来独立开发,也可以跟其他业务代码(比如营销系统)放到同一个项目中进行开发。从运维的角度来说,我们可以将它们跟其他业务一块部署,也可以作为一个微服务独立部署。具体选择哪种开发和部署方式,我们可以参考公司当前的技术架构来决定。
实际上,积分系统业务比较简单,代码量也不多,建议将它更营销系统放到一个项目中开发部署。只要我们做好代码的模块化和解耦,让积分相关的业务代码跟其他业务代码之间边界清晰,没有太多耦合,后期如果我们要将它拆分为独立的项目来开发部署,那也并不困难。
为什么要分 MVC 三层开发?
大部分业务系统的开发都可以分为三层:Contoller 层、Service 层、Repository 层。那为什么要这么做呢??很多业务都比较简单,一层代码搞定所有的数据读取、业务逻辑、接口暴露不好吗。原因如下:
(1)分层能够起到代码复用的作用
- 同一个Repository可能会被多个Service来调用,同一个Service可能会被多个Controller调用。
- 比如,UserService 中的 getUserById() 接口封装了通过 ID 获取用户信息的逻辑,这部分逻辑可能会被UserController 和 AdminController 等多个 Controller使用。如果没有 Service 层,每个 Controller 都要重复实现这部分逻辑,显然会违反 DRY原则
(2)分层能起到隔离变化的作用
- 分层体现了一种抽象和封装的设计思想。比如,Repository 层封装了对数据库访问的操作,提供了抽象的数据访问接口。基于接口而非实现编程的设计思想,Service 层使用Repository 层提供的接口,并不关心其底层依赖的是哪种具体的数据库。当我们需要替换数据库的时候,比如从 MySQL 到 Oracle,从 Oracle 到 Redis,只需要改动 Repository层的代码,Service 层的代码完全不需要修改
- 除此之外,Controller、Service、Repository 三层代码的稳定程度不同、引起变化的原因不同,所以分成三层来组织代码,能有效地隔离变化。比如,Repository 层基于数据库表,而数据库表改动的可能性很小,所以 Repository 层的代码最稳定,而 Controller 层提供适配给外部使用的接口,代码经常会变动。分层之后,Controller 层中代码的频繁改动并不会影响到稳定的 Repository 层
(3)分层能起到隔离关注点的作用:Repository 层只关注数据的读写。Service 层只关注业务逻辑,不关注数据的来源。Controller 层只关注与外界打交道,数据校验、封装、格式转换,并不关心业务逻辑。三层之间的关注点不同,分层之后,职责分明,更加符合单一职责原则,代码的内聚性更好。
(4)分层能提高代码的可测试性。单元测试不依赖不可控的外部组件,比如数据库。分层之后,Repsitory层的代码通过依赖注入的方式供Service层使用,当要测试包含核心业务逻辑的Service层的时候,我们可以用mock的数据源替代真实的数据库,注入到Service层代码中
(5)分层能应对系统的复杂性。所有的代码都放在一个类中,那这个类的代码就会因为需要的迭代而无限膨胀。我们知道,当一个类或者一个函数的代码过多之后,可读性、可维护性就会变差。那我们就要想办法拆分。拆分有水平拆分和垂直拆分两个方向。水平方向基于业务来做拆分,就是模块化;垂直方向基于流程来做拆分,就是这里说的分层
还是那句话,不管是分层、模块化,还是 OOP、DDD,以及各种设计模式、原则和思想,都是为了应对复杂系统,应对系统的复杂性。对于简单系统来说,其实是发挥不了作用的,就是俗话说的“杀鸡焉用牛刀”。
BO、VO、Entity 存在的意义是什么?
我们提到,针对 Controller、Service、Repository 三层,每层都会定义相应的数据对象,它们分别是 VO(View Object)、BO(Business Object)、Entity,例如 UserVo、UserBo、UserEntity。在实际的开发中,VO、BO、Entity可能存在大量的重复字段,甚至三者包含的字段完全一样。在开发的过程中,我们经常需要重复定义三个几乎一样的类,显然是一种重复劳动
相对于每层定义各自的数据对象来说,是不是定义一个公共的数据对象会更好些呢?
不,更推荐每层都定义各自的数据对象。原因如下:
- VO、BO、Entity 并非完全一样。比如,我们可以在 UserEntity、UserBo 中定义Password 字段,但显然不能在 UserVo 中定义 Password 字段,否则就会将用户的密码暴露出去。
- VO、BO、Entity三个类虽然代码重复,但功能语义不重复,从职责上讲是不一样的。所以,也不能算违背DRY原则。如果合并为同一个类,那也会存在后期因为需求的变化而需要再拆分的问题。
- 为了尽量减少每层之间的耦合,把职责边界划分明确,每层都会维护自己的数据对象,层与层之间通过接口交互。数据从下一层传递到上一层的时候,将下一层的数据对象转换成上一层的数据对象,再继续处理。虽然这样的设计稍微有点繁琐,每层都需要定义各自的数据对象,需要做数据对象之间的转换,但是分层清晰。对于非常大的项目来说,结构清晰是第一位的
既然VO、BO、Entity不能合并,那如何解决代码重复的问题呢?
从设计的角度来说,VO、BO、Entity的设计思路并不违反DRY原则,为了分层清晰、减少耦合,多维护几个类的成本也并不是不能接受的。但是,如果你真的有代码洁癖,对于代码重复的问题,我们也有办法来解决。
- 我们知道,继承可以解决代码重复的问题。我们可以将公共的字段定义在类中,让VO、BO、Entity都继承这个父类,各自只定义特有的字段。因为这里的继承层次很浅,也不复杂,所以使用继承并不会影响代码的可读性和可维护性。后期如果因为业务的需要,有些字段需要从父类移动到组合,或者从子类提取到父类,代码改起来也不复杂
- 第二个方法就是,组合。组合也可以拒绝代码重复的问题,所以这里我们还可以将公共的字段抽取出来到公共的类中,VO、BO、Entity通过组合关系来复用这个类的代码
代码重复问题解决了,那不同分层之间的数据对象该如何相互转换呢?
当下一层的数据通过接口调用传递到上一层之后,我们需要将它转换成上一层对应的数据对象类型。比如,Service层从Repository层获取到Entity之后,将其转换为BO,再继续业务逻辑的处理,所以,这个开发的过程会涉及到“Entity到BO”和“BO到VO”这两种转化。
- 最简单的转化方式是手动复制。自己写代码在两个对象之间,一个字段一个字段的赋值。但这样的做法显然是没有技术含量的低级劳动。Java 中提供了多种数据对象转化工具,比如BeanUtils、Dozer 等,可以大大简化繁琐的对象转化工作。
- 如果你是用其他编程语言来做开发,也可以借鉴 Java 这些工具类的设计思路,自己在项目中实现对象转化工具类。
VO、BO、Entity 都是基于贫血模型的,而且为了兼容框架或开发库(比如 MyBatis、Dozer、BeanUtils),我们还需要定义每个字段的 set 方法。这些都违背 OOP 的封装特性,会导致数据被随意修改。那到底该怎么办好呢?
- Entity 和 VO 的生命周期是有限的,都仅限在本层范围内。而对应的Repository 层和 Controller 层也都不包含太多业务逻辑,所以也不会有太多代码随意修改数据,即便设计成贫血、定义每个字段的 set 方法,相对来说也是安全的。
- 不过,Service 层包含比较多的业务逻辑代码,所以 BO 就存在被任意修改的风险了。但是,设计的问题本身就没有最优解,只有权衡。为了使用方便,我们只能做一些妥协,放弃BO 的封装特性,由程序员自己来负责这些数据对象的不被错误使用。
-
总结
标签:积分,代码,系统,接口,兑换,设计,设计模式 来源: https://www.cnblogs.com/itdabao/p/16121251.html