ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

设计模式:积分兑换系统的设计与实现

2022-04-09 11:32:36  阅读:162  来源: 互联网

标签:积分 代码 系统 接口 兑换 设计 设计模式


积分是一种常见的营销手段,很多产品都会用它来促进消费、增加用户粘性。那应该怎么才能实现一个积分系统呢?也就是怎么做产品设计呢?

(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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有