设计模式

55 阅读6分钟

模板模式

属于行为型模式之一,在含有继承结构的代码中是非常常用的。

模板方法只负责定义第一步应该要做什么,第二步应该做什么,第三步应该做什么,至于怎么做,由子类来实现。

子类必须实现它的抽象方法。

基于模板设计模式,规范化抽奖执行流程。包括:提取抽象类、编排模板流程、定义抽象方法、执行抽奖策略、扣减中奖库存、包装返回结果等。

  • 主要以抽象类 AbstractDrawBase 编排定义流程,定义抽象方法由类 DrawExecImpl 做具体实现;
  • 在抽象类的 doDrawExec 方法中,处理整个抽奖流程:根据入参策略ID获取抽奖策略配置 --> 校验和处理抽奖策略的数据初始化到内存 --> 获取那些被排除掉的抽奖列表,包括:奖品库存为空、风控策略、临时调整等 --> 执行抽奖算法 --> 包装中奖结果;
  • 抽象方法的具体实现类 DrawExecImpl,分别实现了 queryExcludeAwardIds、drawAlgorithm 两个 proctect 修饰的抽象方法,因为这2个方法可能随着实现方有不同的方式变化,不适合定义成通用的方法。
    • queryExcludeAwardIds:排除奖品ID,可以包含无库存奖品,也可能是业务逻辑限定的风控策略排除奖品等,所以交给业务实现类做具体处理。
    • drawAlgorithm:是算法抽奖的具体调用处理,因为这里还需要对策略库存进行处理,扣减库存,所以需要单独包装。

基于模板模式开发领取活动领域,因为在领取活动中需要进行活动的日期、库存、状态等校验,并处理扣减库存、添加用户领取信息、封装结果等一系列流程操作,因此使用抽象类定义模板模式更为妥当。

  • 在领取活动 doPartake 方法中,先是通过父类提供的数据服务,获取到活动账单,再定义三个抽象方法:活动信息校验处理、扣减活动库存、领取活动,依次顺序解决活动的领取操作。

简单工厂模式

属于创建型模式之一,强调职责单一原则,一个类只提供一种功能。

定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

一个工厂类 XxxFactory,里面有一个静态方法,根据我们不同的参数,返回不同的派生自同一个父类(或实现同一接口)的实例对象。

把四种奖品(优惠券、兑换码、实物、描述性商品)的发奖,放到一个统一的配置文件类(/** 奖品发放策略组 */ConcurrentHashMap 中,便于通过 AwardType 获取相应的对象,减少 if...else 的使用。 image.png

状态模式

属于行为型模式之一,类的行为是基于它的状态改变的,描述的是一个行为下的多种状态变更。

活动状态的审核,1编辑、2提审、3撤审、4通过、5运行(审核通过后worker扫描状态)、6拒绝、7关闭、8开启。

优化掉原本需要在各个流程节点中的转换使用 ifelse 的场景,也更方便后续进行扩展。

策略模式

属于行为型模式之一,一个类的行为或算法可以在运行时进行更改。

需要生成一个ID,可选的策略就是雪花算法、阿帕奇工具包 RandomStringUtils、日期拼接,分别用在订单号、策略ID、活动号的生成上。外部的调用方会需要根据不同的场景来选择出适合的ID生成策略

  • 订单号:唯一、大量、订单创建时使用、分库分表

  • 活动号:唯一、少量、活动创建时使用、单库单表

  • 策略号:唯一、少量、活动创建时使用、单库单表

自增ID通常容易被外界知晓你的运营数据,以及后续需要做数据迁移到分库分表中都会有些麻烦。

雪花算法:用于生成分布式系统中的唯一 ID,它具有高效、简单和分布式特性的优点。

@Component
public class SnowFlake implements IIdGenerator {
    private Snowflake snowflake;
    @PostConstruct
    public void init(){
        /**
         * 雪花算法由3部分組成
         *
         */
        // 0~31位,可以采用配置的方式使用
        long workerId;
        try{
            /**
             * 機器ID
             */
            workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
        }catch (Exception e){
            /**
             * 如果转换过程中发生异常(例如无法获取本地主机地址),则使用本地主机名的哈希码作为机器ID
             */
            workerId = NetUtil.getLocalhostStr().hashCode();
        }
        /**
         * 将机器ID右移16位,然后与31(即二进制的11111)进行按位与运算,确保机器ID在0到31之间。
         * 这样做是为了确保机器ID符合雪花算法的要求(通常是5位二进制数)。
         */
        workerId = workerId >> 16 & 31;

        long dataCenterId = 1L;
        snowflake = IdUtil.createSnowflake(workerId,dataCenterId);
    }
    @Override
    public long nextId() {
        return snowflake.nextId();
    }
}

雪花算法生成的 ID 通常是一个 64 位的长整型数,这 64 位可以划分为以下几个部分:

  1. 符号位: 1 位,总是 0,不参与实际值计算。

  2. 时间戳: 41 位,用于表示从某个时间点(通常是定义的纪元)开始经过的毫秒数。

  3. 数据中心 ID: 5 位,用于标识数据中心。

  4. 机器 ID: 5 位,用于标识机器或进程。

  5. 序列号: 12 位,用于同一毫秒内生成的不同 ID 的顺序号。

  6. 计算 workerId

    • 使用 NetUtil.ipv4ToLong(NetUtil.getLocalhostStr()) 方法,把本地 IPv4 地址转换为长整型数。
    • 如果转换失败(比如无法获取本地主机地址),则使用主机名的哈希码作为 workerId
    • 为了保证 workerId 在 0 到 31 之间,利用 >> 16 & 31 操作,这样可以确保其仅占 5 位。
  7. 数据中心 ID

    • 数据中心 ID 固定为 1(可以根据实际情况调整)。

组合模式

属于结构型模式之一,用于表示具有层次结构的数据,使得我们对单个对象和组合对象的访问具有一致性。

这种类通常需要定义 add(node)、remove(node)、getChildren() 这些方法。

搭建用于量化人群的规则引擎,用于用户参与活动之前,通过规则引擎过滤性别、年龄、首单消费、消费金额、忠实用户等各类身份来量化出具体可参与的抽奖活动。通过这样的方式控制运营成本和精细化运营。

组合模式的特点就像是搭建出一棵二叉树,而库表中则需要把这样一颗二叉树存放进去,那么这里就需要包括:树根、树茎、子叶、果实。在具体的逻辑实现中则需要通过子叶判断走哪个树茎以及最终筛选出一个果实来。

门面模式

属于结构型模式之一,比如 slf4j 就可以理解为是门面模式的应用。

门面模式使得客户端不需要关注实例化时应该使用哪个实现类,直接调用门面提供的方法就可以了,因为门面类提供的方法的方法名对于客户端来说已经很友好了。

门面模式通过为子系统中的一组接口提供一个统一的接口,使得子系统更容易使用。同时,它也降低了客户端与子系统之间的耦合性,提高了系统的可维护性和扩展性。