编程思维:商场系统互斥关系的设计思路

目录

引言

1、设计思路

2、实现步骤

        2.1 封装不同促销类型的二进制表示的枚举类

         2.2 给商品类添加判断是否可以使用促销的方法

        2.3 数据库中存储促销配置的二进制值:​编辑

        2.4 使用方法

每日一笑😀

引言

        最近公司要对商场系统进行升级重构,其中有一点就是会增加很多种促销活动,每个促销活动可以配置成对其他促销的一个互斥关系

场景一如下:

        假设现在有三种促销类型:满减促销、换购促销、折扣促销

        这三种促销类型有很多种配置有的可以一起使用,有的可以使用满减但是不能使用换购促销等等,有很多种的组合方式

现在客户下单了一批商品:A、B、C、D不同的商品可能命中不同的促销,在匹配下一个促销的时候如何判断是否能够使用该促销呢?

        现在有一个满减促销:可以和换购促销、折扣一起使用

                          换购促销:不能和折扣促销使用、可以和满减促销使用

        现在商品A命中了满减促销,下一步可以匹配换购促销,命中之后则不能匹配折扣促销

       

在匹配促销的时候如何判断是否能够使用促销类型呢?

方案一:

        给商品属性中加上三个字段分别表示是否可以使用满减促销、换购促销、折扣促销

        优点:便于理解、直观

        缺点:不利于重构、扩展、在商品数据流转过程中数据状态难以维护

方案一是绝大多数能够想到的但是缺点也很明显。并且如果场景在复杂一点:

        场景二:多个促销之间是一个并行的,换购促销类型也可以设置不可以使用满减促销生效那么匹配的时候不仅要判断商品是否能够使用换购促销既要判断促销上的属性是否符合当前商品已经匹配的促销,如果换购促销设置的不能够与满减促销使用则当前已经匹配的商品则不能匹配该促销,应该去匹配同类型下可以用使用满减促销的换购促销。在这种场景下方案一就更加难以实现了,实现出来的代码也是很多if-else不利于后期的迭代和优化。

方案二:借助于二进制本文就详细说一下

1、设计思路

        从商品角度来说对于任何促销只有两种状态即生效/不生效,而二进制中的0和1就能够表达该意思且也不会有其他数据,那么商品对于每个促销都可能有不同的状态,则可以用不同位置的0/1来表示不同促销的状态。

满减促销                换购促销                折扣促销                 直减促销

0001                        0010                       0100                       1000

我定义二进制中的四个位置来表示不同的位置,如果该位置为1则表示可以使用该促销。

比如我在配置满减促销A的时候设置:可以换购、不能使用折扣,可以使用满减促销则用二进制表示则是:1010

如果有一个商品命中了该促销A则只需要将该值复制给商品属性上即可。

        那么如何判断一个商品是否可以使用该促销呢?

可以结合二进制的与/或的特点

二进制的或运算:遇1得1,全0得0

二进制的与运算:遇0得0,全1得1

        场景一:如果我要判断一个商品是否可以换购促销则需要:将商品的属性 & 促销二进制表示 判断结果是否等于换购促销的二进制表示

        比如1010 & 0010 = 0010  发现结果0010等于我上诉定义的换购促销0100,则表示该商品可以使用换购促销。

        场景二

        满减促销:可以换购促销、不可以折扣促销、可以使用直减促销:1010

        换购促销:不可以满减、可以折扣、不可以直减:0100

商品A命中了满减促销:1111(商品默认属性:可以使用所有促销) & 1010 = 1010

     商品已经命中的促销类型:0000 | 0001 = 0001

之后来匹配换购促销:

        第一步:判断商品是否可以使用换购促销 1010 & 0010 = 0010  == 0010可以使用换购促销

        第二部:判断商品是否符合该换购促销 0001 & 0100 = 0000  != 0001 则该促销不符合商品(因为促销不能使用满减,而商品已经匹配上了满减促销所以不满足)

针对于场景二需要比场景1多一个属性:商品已经匹配上的促销类型。

本文针对场景一详细讲解

2、实现步骤

        2.1 封装不同促销类型的二进制表示的枚举类

/**
 * @author light pwd
 * @description 促销活动生效值枚举(每个促销生效标识)
 * @date 2025/03/25
 */
public enum PromotionEffectiveTypeEnum {

    /**
     * 满减促销生效值
     */
    FULL_EFFECTIVE(PromotionTypeEnum.EXCHANGE.name(), 0b0001),
    /**
     * 换购促销生效值
     */
    EXCHANGE_EFFECTIVE(PromotionTypeEnum.CONTINUE.name(), 0b0010),
    /**
     * 折扣促销生效值
     */
    DISCOUNT_EFFECTIVE(PromotionTypeEnum.EXPAND.name(), 0b0100),
    /**
     * 直减促销生效值
     */
    REDUCE_EFFECTIVE(PromotionTypeEnum.COMMON.name(), 0b1000);

    private String name;

    private Integer binaryValue;

    private static final Map<String, PromotionEffectiveTypeEnum> ENUM_NAME_MAP = Arrays.stream(PromotionEffectiveTypeEnum.values()).collect(Collectors.toMap(PromotionEffectiveTypeEnum::getNameStr, e -> e));

    private static final String DEFAULT_VALUE_STR = "0000";

    PromotionEffectiveTypeEnum(String name, Integer binaryValue) {
        this.name = name;
        this.binaryValue = binaryValue;
    }

    /**
     * 通过枚举名称获取枚举
     * @param name 枚举名称(PromotionTypeEnum的name)
     * @return
     */
    public static PromotionEffectiveTypeEnum getEnumByPromotionTypeName(String name) {
        return ENUM_NAME_MAP.get(name);
    }

    /**
     * 通过多个名称获取最终二进制值
     * @param names
     * @return
     */
    public static String getBinaryValueStr(List<String> names) {
        if (CollectionUtils.isEmpty(names)) {
            return DEFAULT_VALUE_STR;
        }
        Integer binaryValue = 0b0000;
        for (String name : names) {
            PromotionEffectiveTypeEnum effectiveTypeEnum = ENUM_NAME_MAP.get(name);
            if (Objects.isNull(effectiveTypeEnum)) {
                throw BusinessException.of(CommonErrorCodes.PARAM_IS_INVALID, "没有该促销类型");
            }
            binaryValue = binaryValue | effectiveTypeEnum.getBinaryValue();
        }
        if (binaryValue == 0) {
            return DEFAULT_VALUE_STR;
        }
        return Integer.toBinaryString(binaryValue);
    }

    /**
     * 通过二进制值获取枚举名称
     * @param binaryStr
     * @return
     */
    public static List<String> getBinaryNames(String binaryStr) {
        if (StringUtils.isBlank(binaryStr)) {
            return new ArrayList<>();
        }
        Integer binaryValue = Integer.valueOf(binaryStr, 2);
        if (binaryValue == 0b0000) {
            return new ArrayList<>();
        }
        List<String> names = new ArrayList<>();
        ENUM_NAME_MAP.forEach((k, v) -> {
            if ((binaryValue & v.getBinaryValue()) == v.getBinaryValue()) {
                names.add(v.getNameStr());
            }
        });
        return names;
    }

    public String getNameStr() {
        return name;
    }

    public Integer getBinaryValue() {
        return binaryValue;
    }

}

         2.2 给商品类添加判断是否可以使用促销的方法

public class GoodsInfo {

    private static final Logger LOGGER = LoggerFactory.getLogger(GoodsInfo.class);

    /**
     * 主键id
     */
    private Long id;

    /**
     * 商品名称
     */
    private String name;

    /**
     * 商品价格
     */
    private BigDecimal price;

    /**
     * 商品原价
     */
    private BigDecimal originalPrice;

    /**
     * 默认每个促销都能够生效 (后期迭代:比如增加促销需要在这里增加一位)
     */
    private Integer goodBinaryValue = 0b1111;

    /**
     * 是否为福利商品 false:不是,true:是
     */
    private boolean isWelfareGood = false;

    /**
     * 判断是否可以使用换购促销
     *
     * @return true: 可以 false:不可以
     */
    public boolean canUseExchange() {
        Integer exchangeBinaryValue = PromotionEffectiveTypeEnum.EXCHANGE_EFFECTIVE.getBinaryValue();
        return (goodBinaryValue & exchangeBinaryValue) == exchangeBinaryValue;
    }

    /**
     * 判断是否可以使用满减促销
     *
     * @return true: 可以 false:不可以
     */
    public boolean canUseFull() {
        Integer continueBinaryValue = PromotionEffectiveTypeEnum.FULL_EFFECTIVE.getBinaryValue();
        return (goodBinaryValue & continueBinaryValue) == continueBinaryValue;
    }

    /**
     * 判断是否可以使用折扣促销
     *
     * @return true: 可以 false:不可以
     */
    public boolean canUseDiscount() {
        Integer expandBinaryValue = PromotionEffectiveTypeEnum.DISCOUNT_EFFECTIVE.getBinaryValue();
        return (goodBinaryValue & expandBinaryValue) == expandBinaryValue;
    }

    /**
     * 判断是否可以使用直减促销
     *
     * @return true: 可以 false:不可以
     */
    public boolean canUseReduce() {
        Integer commonBinaryValue = PromotionEffectiveTypeEnum.REDUCE_EFFECTIVE.getBinaryValue();
        return (goodBinaryValue & commonBinaryValue) == commonBinaryValue;
    }

    /**
     * 给商品打上促销的生效促销标签
     *
     * @param promotionMutexType 促销互斥类型值,以二进制字符串形式传入(数据库中促销配置的值)
     */
    public void tagGoodPromotionMutexType(String promotionMutexType) {
        Integer defaultValue = 0b0000;
        if (StringUtils.isBlank(promotionMutexType)) {
            // 如果为空,则将商品的二进制值与默认值进行按位与操作,相当于清空促销标签(后续促销则不会生效)
            this.goodBinaryValue = defaultValue & this.goodBinaryValue;
        } else {
            try {
                // 尝试将传入的二进制字符串转换为整数
                this.goodBinaryValue = this.goodBinaryValue & Integer.parseInt(promotionMutexType, 2);
            } catch (Exception e) {
                LOGGER.error("促销给商品打标签:字符串转二进制失败:", e);
                throw BusinessException.of(CommonErrorCodes.PARAM_IS_INVALID, "促销配置有误请联系管理员!");
            }
        }
    }

}

        2.3 数据库中存储促销配置的二进制值:

该字段的默认值为0000

        2.4 使用方法

        如果商品根据定义的规则命中了促销则调用商品类中的tagGoodPromotionMutexType(String promotionMutexType)方法既可,参数为促销的配置值。

        如果要判断该商品是否可以使用当前促销只需要调用对应的方法根据返回的boolean值来判断既可,如:我要判断该商品是否可以使用直减促销则只需要调用canUseReduce()方法即可

3. 总结

        优点一:该方案利于后续功能的迭代,比如再额外增加一个跨店促销,则只需在枚举类中添加一个跨店促销的枚举值,然后在商品类中将商品的goodBinaryValue属性默认值改为0b11111(0b是二进制的前缀,好比16进制的前缀是0x一样)。然后再提供一个判断是否可以使用跨店促销的方法即可(逻辑参照其他方法)。

        优点二:本来需要4个字段来分别表示是否可以各个促销的状态,包括数据库也需要4个字段值,而上诉方案只需要一个字段,而且后续增加促销也不需要改动数据库。

        优点三:参考充血模型将判断是否可以促销的逻辑放到实体类中便于后续维护,而且判断逻辑并不复杂。减少了很多if-else的判断语句提高了代码的可读性

        如果非要说缺点的话可能就是最开始不了解该设计概念或者二进制的同学可能有点儿难以理解

欢迎大家在评论区讨论,分享自己的观点和设计思路

每日一笑😀:

        某猿退休后决定练习书法,于是重金购买文房四宝。

        一日,饭后突生雅兴,一番研墨拟纸,并点上上好檀香。

        定神片刻,泼墨挥毫,郑重地写下一行字:hello world!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值