抽奖概率算法_策略模式在抽奖组件中的实践应用

背景介绍

全网活动主要是在微信、支付宝、10086APP等多个渠道开展H5承载的营销活动,通过发放优惠券、和微币、流量等奖励来拉新促活互联网渠道用户。抽奖组件正是其中一个至关重要的基础组件,包括但不限于活动开展时间、黑白名单、抽奖机会、奖品库存及概率等的控制。奖品发放是其中的关键一步,奖品类型也从最初的只支持优惠券、折扣券,逐步增加了和微币、电商产品,此后定会有更多奖品类型的支持。

全网活动不生产奖品,只是奖品的搬运工,大多数奖品都需要与外部系统交互,优惠券依赖卡券中心、和微币依赖微门户、电商产品依赖电商平台,这造就了每一种奖品的发放过程相似但又不同。 


整个抽奖的过程用伪代码可以简单的表示如下:

1075fa624ad368b1d4415b88994d23bd.png

今天就跟大家来详细聊一下其中的奖品发放,即:Boolean result = deliverPrize(Request,Prize);
这一步的实现,需要经历从可运行代码,到遵循一定的编码原则,再到应用策略模式的演进过程。

可运行的代码

2e5ec5b3401d68a667db47f02c1f87e3.png

根据背景介绍里的描述,抛开编码原则、设计模式等不谈,发奖过程最朴素的实现方式如下:

1af49acc8af6e6cdfc903bf24bb981d8.png

代码这么写,功能肯定是能实现的,乍看上去也没有什么问题。但是我们一直在说,软件设计是一门关注长期变化的学问。软件存续时间越长,软件设计的重要性越突显,要面向可预见的未来编程,特别是基础能力系统。

奖品发放作为全网活动的命脉,所有活动都以用户为中心,以奖品为发力点,故而奖品功能的变化是必然的,奖品发放就是系统设计的关键点。

果不其然,随着奖品类型在不断增加,发放逻辑也在变化(话费券、流量券等营销资源需要增加审计规则限制),这正在发生,并且定会持续发生。从初始的优惠券、折扣券,已经增加了和微币、电商产品,其他(离线发放),还有ON THE WAY 的支付宝卡券类型(直接发放到支付宝卡包里)。deliverPrize 方法中ifelse分支将越来越多,单个分支内的逻辑也在膨胀,随着需求变更、时间积累,每次一点点儿变更,最后都会积重难返,导致无可读性可言,其修改必将如履薄冰,难以维护,这就是项目缺乏设计守护的结果。

可维护的代码

4cd7f64a252af34d2c9c6a74c6db43bf.png

保持代码可维护性的最简单原则就是,单个方法不要超过一屏,单个类尽量不要超过200行。这是一个比较合理的量化结果,在应用过程中也不必过于刻板。接下来就围绕这个原则来让代码变得可维护。

方法长怎么办?拆!

1个方法拆成4个!把不同类型奖品的发放过程各抽取为一个私有方法,在deliverPrize方法中调用,拆解后如下:

1281cb6aa45f6c0d1cfd109b11d3e0f5.png

经过抽取,分离出了更多更小的方法,deliverPrize方法变得简短,业务逻辑一目了然。此时已经满足了单个方法不超过一屏。但是这些代码还在同一个类里面,类文件内行数不仅没有减少,甚至还多了几行,接下来就是改造类文件。

类文件大怎么办?拆!

再回过头仔细查看一下刚才重构后的代码,会发现这些代码从功能上来说极为相似,都是调用外部系统发放奖品,仅是具体交互细节稍有不同,最终都是返回发放成功还是失败。这不就是面向对象四大特性之一的多态吗?拥有相同的能力(发放奖品),具体行为(交互细节)有所不同。说到多态首先就该想到接口,此外面向接口编程,也应该形成肌肉记忆。顺利成章把他们相同的能力抽象为一个接口,就叫做CallOutside,里面有一个call方法用来实现具体的细节:

2ba686938be9c6b5ee38569dbe8afd83.png

发放优惠券的实现类

76d1f79ec879ca796a3475ca9b2fafbf.png

发放和微币的实现类

ead31a823b24145d383682835d75abc9.png

发放电商产品的实现类

868a5d84372821c9dcc72cf3e38d8f1a.png

具体的交互细节现在都已经抽取到单独的类文件中了,调用它们的地方也要稍作修改。将每个类实例化出一个对象,在运行时根据奖品类型调用相应对象的方法,但是既然已经抽象出了接口,就可以上转型为父接口类型来调用,具体代码如下:

f80f0151d9972317b17bdb4805869879.png

此时所有的代码都有了自己正确的归宿,与不同系统交互的细节通过类文件相互隔离,方法之间互不干扰,可读性增加了,易于理解自然而然可维护性就增加了。完美遵循了单个方法不要超过一屏,单个类尽量不要超过200行原则。深究下也可以发现,此时也遵循了KISS(Keep It Simple,Stupid)和 SRP(Single Responsibility Principle)原则,即保持单个方法、类都要简单、愚蠢(认为它愚蠢说明容易理解),并且一个类只干一件事。

代码写到这里已经很好了,但这是基于需求不会变更的情况下。然而唯一不变的就是变化,前面也提到了,需求时刻在变化着 , 接下来就看如何更好的应对变化。

可扩展的代码

3e688a7ecefd9f2cbc2fa4501c1bb3f9.png

该来的总会来的,甚至比预期的来的更早、更频繁。现在就需要支持一种新的奖品类型——支付宝卡券,直接发放到用户的支付宝卡包里面。该如何实现呢? 


基于以上重构后的代码,很显然,需要新建一个类AlipayOutside,实现CallOutside接口,把与支付宝交互的相关细节隔离到AlipayOutside的call方法里,然后在deliverPrize方法中增加一个else if 分支,调用AlipayOutside的call方法。 


大家有没有注意一个问题,每新增一个奖品就要在deliverPrize方法中添加一个分支,deliverPrize是系统核心代码,不应该被轻易修改,应当遵循OCP(Open Closed Principle)即对扩展开放,对修改关闭的原则。那有没有一个好的方法来解决这个问题呢? 


我们再来仔细观察下deliverPrize这个方法,其主要逻辑就是实例化对象,根据奖品类型找到对应的对象,并且这些对象具有相同的父类型CallOutside。这个功能在天天用的Spring框架中,依托于Spring提供的强大IOC/DI能力,deliverPrize方法可以进一步简化。 

第一步:利用Spring 的IOC能力,把外部接口类交给Spring管理。 


首先,为了能够查找奖品类型对应的外部接口类,需要把外部接口类与奖品类型建立某种映射关系。当然可以通过硬编码或者配置文件的方式逐一将外部接口类与奖品类型建立映射关系。但是约定优于配置也是一个最佳实践,这里我们约定,奖品类型对应的外部接口类在Spring容器中的Bean名称为:{奖品类型}Outside, 这里{奖品类型}是占位符,即当奖品类型为coupon时,对应接口类的Bean名称为:couponOutSide. 有了这个约定,仅需在实现类上添加一个注解并指定Bean名称即可,具体如下:

15d53dba9e58ada798eb38880eb6325e.png

(说明:其实Spring中Bean名称默认就是类名称首字母小写,由于这里恰巧实现类的名称也遵循了奖品类型加Outside这个规则,以上注解中指定名称是非必须的,即仅添加@Service注解也是可以的。)

第二步:利用spring 强大的DI能力,直接把所有的外部接口类注入到Map中,key即为Bean名称,value就是具体的接口类对象(注意,这是一个知识点哦,可以直接注入到Map中)

35fa3bf715d02ee6f77c18f32d1a5a01.png

至此,所有的代码改造工作已经完成,这比改造之前好在哪里呢?回到新增支付宝卡券类型的问题上,现在实现这个需求非常容易,只需要新增一个AlipayOutside类实现CallOutside接口,把具体与支付宝交互的细节封装在call方法内,并添加注解交给Spring管理即可,其他无需任何修改。

5e816853c84700f1828e2b8942f80bb0.png

到这里,通过新增类文件即可完成新奖品类型发放的功能,核心逻辑无需任何修改,这就完美遵循了开闭原则,提高了代码的可扩展性。把最终的代码以UML类图的形式表示下就是:

74369fded13d9e82a6352e94d61d9f57.png

策略模式

知道策略模式的同学,可能已经发现了,特别是看到上面的UML类图之后应该更加笃定,经过遵循多种编码规则一步步精心改造后的代码就是标准的策略模式的应用方式。
来看一下策略模式的官方定义,在GoF(Gang of Four)中是这样描述的: 

Strategy Pattern is used when there is a family of interchangeable algorithms of which one algorithm is to be used depending on the program execution context. Strategy Pattern achieves this by clearly defining the family of algorithms, encapsulating each one and finally making them interchangeable. Each of the algorithms in the family can vary independently from the clients that use it.Interchangeability is achieved by making a parent interface/base strategy class and keeping individual algorithms as children of this base class. The client class holds a reference to the base strategy and at runtime receives the correct algorithm child instance depending on the execution context.  


主旨大意有几点:

  1. 有一组算法(这里指与外部系统交互),将每个算法封装起来,让他们可以相互替换。

  2. 这些算法可以独立于使用它们的客户端(这里指deliverPrize方法)变化,增删算法、修改算法等,客户端无感知。

  3. 具体使用哪个算法是根据运行时的上下文决定的(这里指根据奖品类型动态获取实现类对象)

  4. 定义一个父接口,单个算法实现父接口,使用时根据上下文获取对应的算法(这里指定义了CallOutside接口,与外部交互类都实现了此接口)

通过上面奖品发放的演进过程,及策略模式的定义,可以得出策略模式: 

主要解决:使用 if...else 所带来的复杂性和难以维护。应用场景如果在一个系统里面有许多相似算法,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地从许多行为中选择一种行为。如果不用恰当的模式,这些行为就只好使用多重ifelse/switch条件选择语句来实现。如何解决:将这些算法封装成一个个独立的类,实现同一个接口,使其可以相互替换。 

优点:
1、算法可以自由切换。
2、避免使用多重条件判断。
3、扩展性良好。

缺点:
1、策略类会增多。
2、所有策略类都需要对外暴露。

总结

策略模式是一种设计模式,何为设计模式?就是前人从开发经验中总结出的最佳实践,所有的设计模式理论都来源于实践。

本文基于全网活动中的关键能力——发放奖品,围绕可运行代码 -> 可维护代码 -> 可扩展代码的演进过程,通过遵循单个方法不要超过一屏,单个类尽量不要超过200行,面向接口编程,约定优于配置,KISS(keep it simple,stupid),SRP(single responsibility principle)等原则,一步步推导出了策略模式的应用过程,并且Spring 提供的强大IOC/DI能力,让策略模式的应用变得非常容易。

以上是策略模式的逆推过程,即在不了解策略模式的情况下,通过遵循一些基本的编码规则,也可写出符合某种设计模式的代码。设计模式正确的应用方式应该是,先掌握设计模式,对其应用场景了如指掌,在实际应用过程中,以终为始,使用设计模式理论指导系统设计,使其可维护、可扩展。针对策略模式总结的通俗、极端一点,策略模式就是为了解决多个 if...else 所带来的复杂性和难以维护,凡是有多个if..else的地方就可以考虑有没有必要、是不是可以通过策略模式来解决。

后记

在日常开发过程中,应时刻谨记编码规则和设计模式,融入到潜意识中,形成肌肉记忆,构建自己的知识图谱,应用过程便可由点及面,这样才能写出可扩展、可维护的高质量代码,才能经受住人员更替,历史变迁。

熟知的设计模式有23种,但是在日常开发中有些并不常见,还有些已经作为系统能力提供。常见的编码原则有SOLID(5种原则缩写)、KISS、YAGNI、LOD、基于接口而非实现编程等,编码规则看似简单,但是每个规则好像又是那么的虚幻,很难进行量化。

就拿单一职责原则(SRP)来说,单一职责可以指某个方法、某个类、某个模块,甚至某个系统的职责,根据视角不同、场景不同,应用过程中需要进行适当的权衡取舍,这就需要在开发过程中多应用,多实践,才能得心应手。

编辑:李杰  审校:武六七

f586dd5ef7b957d0ea274f6e3f326bf5.png

29d09d05cc75743bfee5eef86698f027.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
此资源包括抽奖相关所有的配置,概率配置、奖品表、抽奖记录表和通用存储过程算法。 可以指定抽多少次以后在按照正常概率来计算,奖个数,如果奖品全部被抽完就永远抽不奖率和奖最大范围有关,数字越大概率越低,反正越高。由于涉及到一些敏感表数据,只提供主要的奖表,如用户流水账号信息这些表不提供。 规则: --奖率公式:奖率 = 奖项数字范围 ÷ 摇奖数字范围 ÷ 奖数字范围 --1、摇奖数字范围最小值和最大值定义了产生奖项数字的范围。 --2、奖数字范围是从1到奖范围的最大值,此范围内产生奖号码。 /* 比如说,现在有三个选择一等、二等、三等。 可以设置“摇奖数字选项”为 1-30 一等 奖项数字范围1-10 二等 奖项数字范围 11-20 三等 奖项数字范围 21-30 所设置的奖项数字范围必须在 “摇奖数字选项”范围内,而且不得交叉、重叠。 如果我其一个设置1-12 另外一个设置10-20 那么就重叠了。 而且为了方便调整奖率,建议把所有的 奖项数字范围全部设置等距离。如现在有十二个选项,那么依次可以设置成为以下: 1-10、11-20、21-30、...、111-120 那么自然 “摇奖数字范围”就是1-120 现在需要调整奖率的大小,奖号码不用改了,全部设置成为1. 然后去调整奖数字范围。 比如说 一等 奖数字范围 1-30 二等 奖数字范围 1-20 三等 奖数字范围 1-10 那么具体这样设置下来,奖率会有多高呢?算一下便知道。 一等:10/(30*30)=1/90 九十分之一。 二等:10/(30*20)=1/60 六十分之一。 三等:10/(30*10)=1/30 三十分之一。 所以设置的时候就把奖号码都设置成1,只需要调整奖数字范围便可。 */

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值