设计模式:积分兑换系统的设计与实现
积分是一种常见的营销手段,很多产品都会用它来促进消费、增加用户粘性。那应该怎么才能实现一个积分系统呢?也就是怎么做产品设计呢?
(1)首先,一定不要自己一个人闷头想。一方面,这样做很难想全面。另一方面,从零开始设计也比较浪费时间。
- 我们可以找几个类似的产品,比如淘宝,看看它们是如何设计积分系统的,然后借鉴到我们的产品中。
- 笼统地来讲,积分系统无外乎就两个大的功能点,一个是赚取积分,另一个是消费积分。赚取积分功能包括积分赚取渠道,比如下订单、每日签到、评论等;还包括积分兑换规则,比如订单金额与积分的兑换比例,每日签到赠送多少积分等。消费积分功能包括积分消费渠道,比如抵扣订单金额、兑换优惠券、积分换购、参与活动扣积分等;还包括积分兑换规则,比如多少积分可以换算成抵扣订单的多少金额,一张优惠券需要多少积分来兑换等等。
- 上面只是一些非常笼统、粗糙的功能需求。在实际情况中,肯定还有一些业务细节需要考虑,比如积分的有效期问题。对于这些业务细节,还是那句话,闷头拍脑袋想是想不全面的。以防遗漏,我们还是要有方法可寻。那除了刚刚讲的“借鉴”的思路之外,还可以通过产品的**线框图、用户用例(user case)**或者叫做用户故事(user story)来细化业务流程,挖掘一些比较细节的、不容易想到的功能点。
- 比如用户用例。用户用例有点类型单元测试用例。它侧重情景化,其实就是模拟用户如何使用产品,描述用户在一个特定的应用场景里的一个完整的业务操作流程。所以,它包含更多的细节,且更加容易被人理解。比如,有关积分有效期的用户用例,我们可以进行如下的设计:
- 用户在获取积分的时候,会告知积分的有效期;
- 用户在使用积分的时候,会优先使用快过期的积分;
- 用户在查询积分明细的时候,会显示积分的有效期和状态(是否过期);
- 用户在查询总可用积分的时候,会排除掉过期的积分。
(2)通过上面讲的方法,我们就可以将功能需求大致弄清楚了。积分系统的需求总结如下:
- 积分赚取和兑换规则
- 积分的赚取渠道包括:下订单、每日签到、评论等。
- 积分兑换规则可以是比较通用的。比如,签到送 10 积分。再比如,按照订单总金额的10% 兑换成积分,也就是 100 块钱的订单可以积累 10 积分。除此之外,积分兑换规则也可以是比较细化的。比如,不同的店铺、不同的商品,可以设置不同的积分兑换比例。
- 对于积分的有效期,我们可以根据不同渠道,设置不同的有效期。积分到期之后会作废;在消费积分的时候,优先使用快到期的积分。
- 积分消费和兑换规则
- 积分的消费渠道包括:抵抗订单金额、兑换优惠券、积分换购、参与活动扣积分等。
- 我们可以根据不同的消费渠道,设置不同的积分兑换规则。比如,积分换算成消费抵扣金额的比例是 10%,也就是 10 积分可以抵扣 1 块钱;100 积分可以兑换 15 块钱的优惠券等。
- 积分及其明细查询:查询用户的总积分,以及赚取积分和消费积分的历史记录。
设计模式、原则和思想,都是为了应对复杂系统,应对系统的复杂性。对于简单系统来说,其实是发挥不了作用的,就是俗话说的“杀鸡焉用牛刀”。
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 的封装特性,由程序员自己来负责这些数据对象的不被错误使用。
-
总结