前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用策略模式写了个需求,感觉还行,分享一下

使用策略模式写了个需求,感觉还行,分享一下

作者头像
烟雨平生
发布2024-07-31 17:12:56
710
发布2024-07-31 17:12:56
举报
文章被收录于专栏:数字化之路

如果说研发团队是一个可以随意替换的工具,那么所有代码都会来自需求。

先交待下需求: 在之前渠道1退款的基础上,再增加一个退款渠道。

需求分析:

退款是一个高可靠性操作。收到退款请求后,需要进行如下 操作:

  1. 校验数据合法性
  2. 校验业务合法性
  3. 执行退款

其中,校验业务合法性是个性化逻辑,不同的渠道校验的办法不同。

做过支付的同学都知道,支付操作会分为发起支付、支付中、支付完成 这三个状态。

退款也一样,执行退款后,并不知道结果。具体的结果需要由支付来回调来更新最终的退款状态。

技术分析:

操作流程固定,只是不同场景时,“校验业务合法性”的具体逻辑不同。

看到这的粉丝朋友,是不是也想到了相同部分提取到抽象父类,不同的部分各自实现。

是的。是这个场景,虽然"组合大于继承",就流程执行的连贯性、可读性、可理解性,还是使用模板+抽象类更合适一些。

上类图

退款渠道1:

退款渠道2:

上代码

要干的事:退款

代码语言:javascript
复制
import java.math.BigDecimal;

public interface AfterSalesRefundService {

    void refund(String soNo, String afterSaleNo, BigDecimal amount);

}

退款的动作拆解:

代码语言:javascript
复制

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Service
@Slf4j
public abstract class AbstractAfterSalesRefundService implements AfterSalesRefundService {


    @Override
    public void refund(String soNo, String afterSaleNo, BigDecimal amount) {
        // TODO: 2024/7/22 数据合法性检查
        /**
         * 具体渠道业务校验
         */
        doubleCheckValid(soNo, afterSaleNo, amount);
        // TODO: 2024/7/22 执行具体的退款操作 
    }

    /**
     * 退款时,不同渠道传不同的标识,方便后期业务梳理,也方便研发排查问题
     *
     * @return
     */
    abstract String getFastRefundPrefix();

    abstract void doubleCheckValid(String soNo, String afterSaleNo, BigDecimal amount);

}

渠道1中的个性化校验逻辑:

代码语言:javascript
复制

import com.alibaba.fastjson.JSON;
import com.payment.core.domain.dto.ResultDTO;
import com.payment.core.exception.GlobalCode;
import com.payment.payment.api.manager.order.OrderFeignClient;
import com.payment.business.refund.enums.RefundServiceNameConstant;
import com.payment.business.refund.exception.PaymentAfterSaleException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

@Service(RefundServiceNameConstant.SELF_RUN_SERVICE)
@Slf4j
public class AfterSalesSelfRunRefundServiceImpl extends AbstractAfterSalesRefundService {

    public static final String SELF_RUN_FAST_REFUND_PREFIX = "自营售后快速退款";

    @Autowired
    private OrderFeignClient orderFeignClient;

    @Override
    String getFastRefundPrefix() {
        return SELF_RUN_FAST_REFUND_PREFIX;
    }

    @Override
    void doubleCheckValid(String soNo, String afterSaleNo, BigDecimal amount) {
        ResultDTO<String> resultDTO = orderFeignClient.selfRunRefundCheck(soNo, afterSaleNo, amount);
        if (!resultDTO.getSuccess()) {
            log.info(" 自营售后单退款 soNo {} afterSaleNo {} 售后单校验失败 {} ", soNo, afterSaleNo, JSON.toJSONString(resultDTO));
            throw new PaymentAfterSaleException(GlobalCode.BAD_REQUEST.setMsg("自营售后单校验失败 " + afterSaleNo));
        }
    }


}

渠道2中的个性化校验逻辑:

代码语言:javascript
复制

import com.alibaba.fastjson.JSON;
import com.payment.core.domain.dto.ResultDTO;
import com.payment.core.exception.GlobalCode;
import com.payment.adapter.http.vc.order.VCOrderFeign;
import com.payment.business.refund.enums.RefundServiceNameConstant;
import com.payment.business.refund.exception.PaymentAfterSaleException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * @Auther: cheng.tang
 * @Date: 2024/6/11
 * @Description: zkh-gbb-payment
 */
@Service(RefundServiceNameConstant.VPI_SERVICE)
@Slf4j
public class AfterSalesVPIRefundServiceImpl extends AbstractAfterSalesRefundService {

    public static final String VPI_FAST_REFUND_PREFIX = "售后快速退款";

    @Autowired
    private VCOrderFeign vcOrderFeign;

    @Override
    String getFastRefundPrefix() {
        return VPI_FAST_REFUND_PREFIX;
    }

    @Override
    void doubleCheckValid(String soNo, String afterSaleNo, BigDecimal amount) {
        ResultDTO<String> refundCheck = vcOrderFeign.refundCheck(soNo, afterSaleNo, amount);
        if (!refundCheck.getSuccess()) {
            log.info("VPI售后单退款 soNo {} afterSaleNo {} 售后单校验失败 {} ", soNo, afterSaleNo, JSON.toJSONString(refundCheck));
            throw new PaymentAfterSaleException(GlobalCode.BAD_REQUEST.setMsg("VPI售后单校验失败 " + afterSaleNo));
        }
    }

}

服务已经有了,如何提供一个友好的调用入口呢?

根据不同的入参灵活地切换算法或操作的场景,适合哪种设计模式? 工厂?策略? 对的,是策略模式。

来一起回顾下策略模式的用法:

策略模式包含策略、上下文、客户端。 具体,有下面几个角色: 角色1:抽象得到的策略接口 角色2:具体策略 角色3:上下文 角色4:客户端 唐成,公众号:的数字化之路如果策略模式的代码有段位,你的是白银?黄金?还是王者?

策略接口和具体策略【退款渠道】已经有了,缺策略上下文和客户端。

在写策略上下文和调用策略的客户端之前,先做个CleanCode方面的准备: 1、代替魔法字符串的常量:

代码语言:javascript
复制
public class RefundServiceNameConstant {

    public static final String VPI_SERVICE = "VPIRefund";
    public static final String SELF_RUN_SERVICE = "SelfRunRefund";

}

是的,上面两个策略的自定义Bean名就是这两个常量。

2、作为调用参数的标识:枚举类

代码语言:javascript
复制

import lombok.Getter;

@Getter
public enum RefundEnums {
    VPI("VPI退款", RefundServiceNameConstant.VPI_SERVICE),
    SELF_RUN("自营退款", RefundServiceNameConstant.SELF_RUN_SERVICE);

    private final String memo;
    private final String serviceName;

    RefundEnums(String memo, String serviceName) {
        this.memo = memo;
        this.serviceName = serviceName;
    }

}

上面这两个类是铺垫,正戏“策略上下文”到了:

代码语言:javascript
复制
import com.payment.core.exception.GlobalCode;
import com.payment.business.refund.enums.RefundEnums;
import com.payment.business.refund.exception.PaymentAfterSaleException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
@Slf4j
public class AfterSalesRefundContext {

    private final Map<String, AfterSalesRefundService> serverName2Service = new HashMap<>();

    @Autowired
    public AfterSalesRefundContext(List<AfterSalesRefundService> afterSalesRefundServiceList) {
        if (CollectionUtils.isEmpty(afterSalesRefundServiceList)) {
            log.warn(" AfterSalesRefundContext noService ");
            return;
        }
        for (AfterSalesRefundService afterSalesRefundService : afterSalesRefundServiceList) {
            serverName2Service.put(afterSalesRefundService.getClass().getAnnotation(Service.class).value(), afterSalesRefundService);
        }
    }

    public AfterSalesRefundService getRefundService(RefundEnums refundEnums) {
        if (refundEnums == null) {
            log.warn("未指定RefundService");
            throw new PaymentAfterSaleException(GlobalCode.NOT_EXIST.setMsg("未指定Service"));
        }
        AfterSalesRefundService afterSalesRefundService = serverName2Service.get(refundEnums.getServiceName());
        if (afterSalesRefundService == null) {
            log.warn(" RefundService不存在 refundEnums {} ", refundEnums);
            throw new PaymentAfterSaleException(GlobalCode.NOT_EXIST.setMsg("RefundService不存在"));
        }
        return afterSalesRefundService;
    }


}

易错点:afterSalesRefundService.getClass().getAnnotation(Service.class).value() 获取自定义Bean名字的方法

消费策略的客户端【调用方】:

代码语言:javascript
复制

import com.payment.business.refund.enums.RefundEnums;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
@Slf4j
public class AfterSalesRefundClient {

    @Autowired
    private AfterSalesRefundContext afterSalesRefundContext;

    public void doRefund(RefundEnums refundEnums, String soNo, String afterSaleNo, BigDecimal amount) {
        log.info(" doRefund refundEnums {} soNo {} afterSaleNo {} amount {} ", refundEnums, soNo, afterSaleNo, amount);
        afterSalesRefundContext.getRefundService(refundEnums).refund(soNo, afterSaleNo, amount);
        log.info(" doRefund finish  soNo {} afterSaleNo {} ", soNo, afterSaleNo);
    }

}

在控制器消费这些策略:

完工。


最后,分享另外一个踩的坑:

需求:刷一批历史数据,需要循环遍历所有数据。

这个场景是不是想到根据ID循环遍历所有数据然后逐一处理?是的,是这样。 那使用递归,还是while(true)循环? 要使用while(true)循环,否则数据量一大,就: java.lang.StackOverflowError: null

循环到第538次时StackOverflowError

当然,使用while(true)时也有坑,踩了同学可以分享一下

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-07-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 的数字化之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档