写出优雅的业务代码(1):项目中的模版方法,策略模式

关键字:

  1. 如何写好业务代码
  2. 业务架构
  3. 设计模式
  4. 模版方法
  5. 策略模式
  6. 工厂模式

本文概要:

对于做web开发的java程序员来说,如何写出更好看的业务代码。本文会展示利用设计模式中模版方法策略工厂3种模式来优化平铺直叙的代码。

业务简介:

开始之前需要先了解一下业务。

  1. 业务是通过调用支付宝接口来做支付订单。
  2. 业务中有10种订单类型。
  3. 通过接口参数里的payType参数确定是哪种订单,然后执行对应的订单分支逻辑,调用阿里支付,返回交易编号

优化前的部分代码如下:

  1. http接口接收订单类型参数,然后调用service的getPayInfo方法时传入这个类型。
    001
  2. getPayInfo方法根据参数payType做switch case,这本身没有太大问题,但是清注意,每一个case里,代码都分为2个步骤,步骤1准备参数,步骤2调用与case对应的方法,并且每个方法的参数个数一样,类型除了最后一个之外,也都是一样的。
    002
  3. 上面case语句里调用的方法如下图,可以分为2个或3个步骤,步骤1组装参数对象,步骤2调用alipayService.getTradeNo方法,步骤3根据步骤2的返回内容做后续处理,最后返回阿里支付的交易编号。
    003

优化前代码的一些问题

  1. 对于上面图002中的getPayInfo方法,如果业务要在增加一种订单类型,那么就需要再增加一个case的同时,增加case中的步骤1,步骤2代码。这样的话就会导致每次增加一种新订单类型都需要维护很大的一大坨代码。而且如果某个现有订单类型的步骤1,步骤2逻辑需要改动,也需要在这个switch case里修改。如果用高大上的语言来描述,那就是没有符合开闭原则,没有做到单一职责原则,没有做到高内聚低耦合等等。

怎么来优化呢

  1. 上面的代码图002中service里的getPayInfo方法中, case里写了大量(省略了7个case分支)的重复代码,各个case中的步骤1,步骤2都是可以抽象出来的。
  2. 图002中case里调用各自的方法获取tradeNo。这些方法在图003中可以看出来也有共通性,也可以抽象出来。
  3. 从整体的代码逻辑上来看,图002中的每个case分支,及其后续操作,似乎都有着同样的共性,只有少部分的逻辑有各自的特性,那么很容易想到用模版方法模式来做优化。因为有很多case分支,也很容易联想到策略模式了。
  4. 优化的思路如下:
    1. 把不同的订单类型都当作不同的策略,那么也就是每一个case中的逻辑都是一个不同的策略;
    2. 因为每一个订单类型的代码流程大致都相同,只有少数不同的步骤,那么就定义一个模版类,将相同的步骤统一写在模版类里,不同的步骤,让策略类去继承模版类后,自己去实现;
    3. 把switch case转移到一个工厂类里,通过工厂类生成不同的策略对象;
    4. 外层代码调用工厂类返回对象的模版方法,即可完成订单支付流程。

优化之后的代码

  1. 首先创建一个模版类的接口IOrderAlipayStrategy,模版类实现这个接口,该接口只有一个方法payThroughAlipay,具体使用请看 2.
/**
 * @Author: yesiming
 * @Platform: Mac
 * @Date: 5:14 下午 2020/9/25
 *
 * 代码优化:支付宝小程序支付优化成通过工厂,策略,模版 3种模式实现
 * 此接口为:订单支付的策略接口
 */
public interface IOrderAlipayStrategy {

    /**
     * @param alipayConfig
     * @param payType
     * @param orderId
     * @param alipayUserId
     * @return
     *
     * 通过alipay支付,OrderInfo,包含阿里支付返回的TradeNo
     */
    OrderInfo payThroughAlipay(AlipayConfig alipayConfig,  PayType payType, String orderId, String alipayUserId) throws Exception;
}

OrderInfo用户存放模版方法返回的内容,代码如下。

public class OrderInfo {
    private String payOrderId;
    private BigDecimal totalAmount;
    private String subject;
    private String tradeNo;

    public OrderInfo(BigDecimal totalAmount, String subject, PayType payType, String orderNo) {
        this.payOrderId = String.join("-", payType.getCode(), orderNo);
        this.totalAmount = totalAmount;
        this.subject = subject;
    }

    // 省略其他getter,setter
}
  1. 创建抽象模版类,实现IOrderAlipayStrategy接口,并且实现payThroughAlipay方法,可以从代码中看到,payThroughAlipay里的1,2,3,4,5,这5个步骤中,只有1,5,是与业务相关的,2,3,4是与业务无关的。那么该方法中年的2,3,4步骤的代码就可以在该模版类中实现,并且由该方法调用。1,5步骤因为是与业务相关,也就是说对于不同的订单类型,实现不同,那么1,5步骤的方法在模版类中就写成抽象方法,具体实现推迟到继承模版方法的各个策略类中。当然某些场景下1,5步骤也可以写成默认实现,具体策略类对默认实现不满意的话,可以覆盖默认实现。
/**
 * @Author: yesiming
 * @Platform: Mac
 * @Date: 5:28 下午 2020/9/25
 */
public abstract class AbstractOrderAlipayStrategy implements IOrderAlipayStrategy {

    @Value("${url.domain.name}")
    public String DOMAIN_NAME;

    @Autowired
    private AlipayService alipayService;

    @Override
    public OrderInfo payThroughAlipay(AlipayConfig alipayConfig, PayType payType, String orderId, String alipayUserId) throws Exception {
        // 1. 准备参数,业务相关
        OrderInfo orderInfo = prepareArgs(orderId, payType);

        // 2. 组装DTO对象
        AlipayTradeCreateBizContentDTO bizContentInputDTO =
                assembleAlipayTradeCreateBizContentDTO(alipayUserId, orderInfo);

        // 3. 调用阿里支付
        String tradeNo = realPayThroughAlipay(alipayConfig,
                bizContentInputDTO,
                DOMAIN_NAME + AlipayConstants.NOTIFY_MINIPROGRAM_PAYCALLBACK);

        // 4. 附加对TradeNo的处理
        attachTradeNo(orderInfo, tradeNo);

        // 5. 后续处理,业务相关
        followUp(tradeNo);
        return orderInfo;
    }

    /**
     * 业务相关,返回不同Order类型
     * @param orderId
     * @return
     */
    public abstract OrderInfo prepareArgs(String orderId, PayType payType);

    /**
     * 组装DTO
     * 业务无关,模版实现
     */
    private AlipayTradeCreateBizContentDTO assembleAlipayTradeCreateBizContentDTO(String alipayUserId,
                                                OrderInfo orderInfo) {

        AlipayTradeCreateBizContentDTO bizContentInputDTO = new AlipayTradeCreateBizContentDTO();
        bizContentInputDTO.setBuyerId(alipayUserId);
        bizContentInputDTO.setOutTradeNo(orderInfo.getPayOrderId());
        bizContentInputDTO.setTotalAmount(orderInfo.getTotalAmount());
        bizContentInputDTO.setSubject(orderInfo.getSubject());

        return bizContentInputDTO;
    }

    /**
     * 调用Alipay支付
     * 业务无关,模版实现
     */
    private String realPayThroughAlipay(AlipayConfig alipayConfig,
                     AlipayTradeCreateBizContentDTO bizContentInputDTO,
                     String notifyUrl) throws Exception {
        String tradeNo = alipayService.getTradeNo(alipayConfig, bizContentInputDTO,
                DOMAIN_NAME + AlipayConstants.NOTIFY_MINIPROGRAM_PAYCALLBACK);
        return tradeNo;
    }

    /**
     * 处理tradeNo返回值
     * 业务无关,模版实现
     */
    private void attachTradeNo(OrderInfo orderInfo, String tradeNo) {
        orderInfo.setTradeNo(tradeNo);
    }

    /**
     * 业务相关,自定义后续处理,
     * 如果需要后续处理可以重写此方法
     *
     * @param tradeNo
     */
    public void followUp(String tradeNo) {}
}
  1. 到此为止,已经把优化之前的每个case分支里的流程框架写完了,接下来只需要完成每个case分支的策略类即可。
  2. 策略类需要继承模版类,并且重写模版类中的业务相关方法。
    4.1 策略类:员工订单服务费
/**
 * @Author: yesiming
 * @Platform: Mac
 * @Date: 5:13 下午 2020/9/25
 *
 * 员工订单服务费
 */
@Component
public class OrgOrderServiceStrategy extends AbstractOrderAlipayStrategy {
    
    @Autowired
    private OrgOrderMapper orgOrderMapper;

    /**
     * 重写了模版类中的业务相关方法,实现业务的独立性
     * @param orderId
     * @param payType
     * @return
     */
    @Override
    public OrderInfo prepareArgs(String orderId, PayType payType) {
        OrgOrder orgOrderService = orgOrderMapper.getById(Long.parseLong(orderId));
        BigDecimal payAmount = orgOrderService.getTotalServiceCostAmount();
        OrderInfo orderInfo = new OrderInfo(payAmount,orgOrderService.getTypeName(), payType, orgOrderService.getOrderNo());
        return orderInfo;
    }
}

4.2 策略类:员工订单商品服务费

@Component
public class OrgOrderObjStrategy extends AbstractOrderAlipayStrategy {

    @Autowired
    private OrgOrderMapper orgOrderMapper;

    @Override
    public OrderInfo prepareArgs(String orderId, PayType payType) {
        OrgOrder orgOrderService = orgOrderMapper.getById(Long.parseLong(orderId));
        BigDecimal payAmount = orgOrderService.getObjCostAmount();
        OrderInfo orderInfo = new OrderInfo(payAmount,orgOrderService.getTypeName(), payType, orgOrderService.getOrderNo());
        return orderInfo;
    }
}

4.3. 策略类:活动订单

@Component
public class ActivityOrderStrategy extends AbstractOrderAlipayStrategy {

    @Autowired
    private ConfigurableActivityService configurableActivityService;

    @Autowired
    private ActivityOwnerMapper activityOwnerMapper;

    /**
     * 简化方法之间的数据传递
     */
    private ThreadLocal<Long> activityOwnerIdTL = new ThreadLocal<>();

    @Override
    public OrderInfo prepareArgs(String orderId, PayType payType) {

        ActivityOwner activityOwner = configurableActivityService.getActivityOwnerById(Long.parseLong(orderId));
        activityOwnerIdTL.set(activityOwner.getId());
        BigDecimal payAmount = activityOwner.getApplyMoney();
        OrderInfo orderInfo = new OrderInfo(
                payAmount, // 支付金额
                WechatConstants.orderBody, //
                payType, // 支付类型
                activityOwner.getId().toString() + "-" + System.currentTimeMillis());

        return orderInfo;
    }

    /**
     * 后续处理,
     * 需要prepareArgs()方法执行过程中的数据,还不想通过返回值来传递
     * 可以通过ThreadLocal来完成
     *
     * @param tradeNo
     */
    @Override
    public void followUp(String tradeNo) {
        if (tradeNo != null) {
            ActivityOwner activityOwner = new ActivityOwner();
            activityOwner.setId(activityOwnerIdTL.get());
            activityOwner.setSource("mini_alipay");
            activityOwnerMapper.updateSource(activityOwner);
        }
    }
}

4.4 还有很多策略类不写了,从上面给出的3个策略类的代码看,前2个策略类,只实现了prepareArgs方法,没有实现followUp方法,那么对于这2个策略类来说,followUp的处理将使用模版类里提供的默认处理方式,也就是什么都不做,第三个策略类重写了followUp方法,那么它将使用自己的重写逻辑。

  1. 策略类写完了,下面就可以在service里调用了。可是这么多策略类如何调用呢,怎么知道调用哪一个呢?可以将switch case放到一个工厂类里面。需要注意的是,使用spring框架时,这里一定要注入需要用到的策略类,不能在case里new,new的话会导致策略类里的属性不被填充。
    ---------- 这里有个小小的坑,请看后文【后记】部分 ----------
/**
 * @Author: yesiming
 * @Platform: Mac
 * @Date: 10:16 下午 2020/9/25
 */
@Component
public class OrderStrategyFactory {

    @Autowired
    private ActivityOrderStrategy activityOrderStrategy;

    @Autowired
    private GroupbuyOrderStrategy groupbuyOrderStrategy;

    @Autowired
    private OrgOrderObjStrategy orgOrderObjStrategy;

    @Autowired
    private OrgOrderRewardStrategy orgOrderRewardStrategy;

    @Autowired
    private OrgOrderServiceStrategy orgOrderServiceStrategy;

    @Autowired
    private ShopOrderStrategy shopOrderStrategy;

    @Autowired
    private ShopVoucherStrategy shopVoucherStrategy;

    @Autowired
    private YxOrderStrategy yxOrderStrategy;

    public IOrderAlipayStrategy createOrderInstannce(PayType payType) {

        IOrderAlipayStrategy orderAlipayStrategy = null;
        switch (payType) { // TODO: 这里需要增加一个策略工厂类
            case ORG_ORDER_SERVICE: // DONE
                orderAlipayStrategy = orgOrderServiceStrategy;
                break;
            case ORG_ORDER_OBJ: // DONE
                orderAlipayStrategy = orgOrderObjStrategy;
                break;
            case ORG_ORDER_REWAED: // DONE
                orderAlipayStrategy = orgOrderRewardStrategy;
                break;
            case GROUPBUY_ORDER: // DONE
                orderAlipayStrategy = groupbuyOrderStrategy;
                break;
            case SHOP_ORDER: // DONE
                orderAlipayStrategy = shopOrderStrategy;
                break;
            case SHOP_VOUCHER: // DONE
                orderAlipayStrategy = shopVoucherStrategy;
                break;
            case YX_ORDER: // DONE
                orderAlipayStrategy = yxOrderStrategy;
                break;
            case PROPERTY_FEE:
//                PropertyCostGd propertyCostGd = propertyCostsGdMapper.selPropertyCostGdByApplyId(orderId);
//                OwnerAccountPropertyGd ownerAccountPropertyGd = getPropertyFee(alipayConfig, domainName, payOrderId, alipayUserId, propertyCostGd);
//                payOrderId = String.join("-", PayType.PROPERTY_FEE.getCode(), ownerAccountPropertyGd.getNumber());
//                tradeNo = ownerAccountPropertyGd.getPaymentPlatformBillCode();
//                break;
            case ACTIVITY_ORDER: // DONE
                orderAlipayStrategy = activityOrderStrategy;
                break;
            default:
        }

        return orderAlipayStrategy;
    }

}
  1. 有了工厂类,那么现在就能在service里调用了。
    为了看出来区别,优化前的那一大段switch case我保留下来了。
    @Override
    public String getPayInfo(AlipayConfig alipayConfig, String domainName, PayType payType, String orderId, String alipayUserId) throws Exception {
//        switch (payType) { // TODO: 这里需要增加一个策略工厂类
//            case ORG_ORDER_SERVICE: // DONE
//                OrgOrder orgOrderService = orgOrderMapper.getById(Long.parseLong(orderId));
//                payAmount = orgOrderService.getTotalServiceCostAmount();
//                payOrderId = String.join("-", payType.getCode(), orgOrderService.getOrderNo());
//                tradeNo = getOrgOrderServicePrepayId(alipayConfig, domainName, payOrderId, alipayUserId, orgOrderService);
//                break;
//            case ORG_ORDER_OBJ: // DONE
//                OrgOrder orgOrderObj = orgOrderMapper.getById(Long.parseLong(orderId));
//                payAmount = orgOrderObj.getObjCostAmount();
//                payOrderId = String.join("-", payType.getCode(), orgOrderObj.getOrderNo());
//                tradeNo = getOrgOrderObjPrepayId(alipayConfig, domainName, payOrderId, alipayUserId, orgOrderObj);
//                break;
//            case ORG_ORDER_REWAED: // DONE
//                OrgOrder orgOrderReward = orgOrderMapper.getById(Long.parseLong(orderId));
//                payAmount = orgOrderReward.getRewardAmount();
//                payOrderId = String.join("-", payType.getCode(), orgOrderReward.getOrderNo(), Long.toString(System.currentTimeMillis()));
//                tradeNo = getOrgOrderRewardPrepayId(alipayConfig, domainName, payOrderId, alipayUserId, orgOrderReward);
//                break;
//            case GROUPBUY_ORDER: // DONE
//                PrivilegeGroupbuyingOrder privilegeGroupbuyingOrder = privilegeGroupbuyingOrderMapper.getById(Long.parseLong(orderId));
//                payAmount = privilegeGroupbuyingOrder.getTotalAmount();
//                payOrderId = String.join("-", payType.getCode(), privilegeGroupbuyingOrder.getNumber());
//                tradeNo = getGroupbuyAlipayPrepayId(alipayConfig, domainName, payOrderId, alipayUserId, privilegeGroupbuyingOrder);
//                break;
//            case SHOP_ORDER: // DONE
//                PrivilegeShopsOrder privilegeShopsOrder = privilegeShopsOrderMapper.getById(Long.parseLong(orderId));
//                payAmount = privilegeShopsOrder.getTotalAmount();
//                payOrderId = String.join("-", payType.getCode(), privilegeShopsOrder.getNumber());
//                tradeNo = getShopOrderAlipayPrepayId(alipayConfig, domainName, payOrderId, alipayUserId, privilegeShopsOrder);
//                break;
//            case SHOP_VOUCHER: // DONE
//                PrivilegeShopsVoucher privilegeShopsVoucher = privilegeShopsVoucherMapper.getById(Long.parseLong(orderId));
//                payAmount = privilegeShopsVoucher.getTotalAmount();
//                payOrderId = String.join("-", payType.getCode(), privilegeShopsVoucher.getNumber());
//                tradeNo = getShopVoucherAlipayPrepayId(alipayConfig, domainName, payOrderId, alipayUserId, privilegeShopsVoucher);
//                break;
//            case YX_ORDER: // DONE
//                YxOrder yxOrder = yxOrderMapper.getYxOrder(orderId);
//                payAmount = yxOrder.getRealPrice();
//                payOrderId = String.join("-", payType.getCode(), yxOrder.getOrderId());
//                tradeNo = getYxOrderAlipayPrepayId(alipayConfig, domainName, payOrderId, alipayUserId, yxOrder);
//                break;
//            case PROPERTY_FEE:
////                PropertyCostGd propertyCostGd = propertyCostsGdMapper.selPropertyCostGdByApplyId(orderId);
////                OwnerAccountPropertyGd ownerAccountPropertyGd = getPropertyFee(alipayConfig, domainName, payOrderId, alipayUserId, propertyCostGd);
////                payOrderId = String.join("-", PayType.PROPERTY_FEE.getCode(), ownerAccountPropertyGd.getNumber());
////                tradeNo = ownerAccountPropertyGd.getPaymentPlatformBillCode();
////                break;
//            case ACTIVITY_ORDER: // DONE
//                ActivityOwner activityOwner = configurableActivityService.getActivityOwnerById(Long.parseLong(orderId));
//                payAmount = activityOwner.getApplyMoney();
//                payOrderId = String.join("-", PayType.ACTIVITY_ORDER.getCode(), activityOwner.getId().toString(), System.currentTimeMillis()+"");
//                tradeNo = getActivitePrepayId(alipayConfig, domainName, payOrderId, alipayUserId, activityOwner);
//                break;
//            default:
//        }

        // 工厂根据payType创建并返回对应的策略类。
        IOrderAlipayStrategy orderAlipayStrategy = orderStrategyFactory.createOrderInstannce(payType);
        // 策略类去执行模版方法
        OrderInfo orderInfo = orderAlipayStrategy.payThroughAlipay(alipayConfig, payType, orderId, alipayUserId);

        savePayLog(payType, orderId, orderInfo.getPayOrderId(), PayChannel.ALI, orderInfo.getTradeNo(), alipayUserId, orderInfo.getTotalAmount());
        return orderInfo.getTradeNo();
    }
  1. 优化完成,看一下增加的类与接口
    004

总结

对于大部分业务代码,遇到这种业务场景应该也不少,而模版方法与策略模式的结合正是用来解决这种场景的利器,当然,当策略特别多的时候也会导致其他问题,比如类爆炸,不过那是另一个话题了。

后记

以上的代码,基本上实现了目标,提取出通用代码,各个策略类各司其职只专注于自身特性的业务代码,通过工厂类产生需要的业务类对象。
但是,有一个缺陷:工厂类,工厂类通过switch case语句来产生策略类对象,如果订单类型增加了,也就需要策略类,工厂也就需要增加对新策略类的支持。
这时就需要修改工厂类,修改类容如下:
1. 增加新的策略类属性;
2. 增加case语句。
好的,问题来了:开闭原则被打破了!
如何继续优化这部分呢?请看《写出优雅的业务代码(2):优化掉工厂模式中的 switch case》

最后附上uml图
004
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342