寫出優(yōu)雅的業(yè)務(wù)代碼(1):項目中的模版方法,策略模式

關(guān)鍵字:

  1. 如何寫好業(yè)務(wù)代碼
  2. 業(yè)務(wù)架構(gòu)
  3. 設(shè)計模式
  4. 模版方法
  5. 策略模式
  6. 工廠模式

本文概要:

對于做web開發(fā)的java程序員來說,如何寫出更好看的業(yè)務(wù)代碼。本文會展示利用設(shè)計模式中模版方法策略,工廠3種模式來優(yōu)化平鋪直敘的代碼。

業(yè)務(wù)簡介:

開始之前需要先了解一下業(yè)務(wù)。

  1. 業(yè)務(wù)是通過調(diào)用支付寶接口來做支付訂單。
  2. 業(yè)務(wù)中有10種訂單類型。
  3. 通過接口參數(shù)里的payType參數(shù)確定是哪種訂單,然后執(zhí)行對應(yīng)的訂單分支邏輯,調(diào)用阿里支付,返回交易編號

優(yōu)化前的部分代碼如下:

  1. http接口接收訂單類型參數(shù),然后調(diào)用service的getPayInfo方法時傳入這個類型。
    001
  2. getPayInfo方法根據(jù)參數(shù)payType做switch case,這本身沒有太大問題,但是清注意,每一個case里,代碼都分為2個步驟,步驟1準(zhǔn)備參數(shù),步驟2調(diào)用與case對應(yīng)的方法,并且每個方法的參數(shù)個數(shù)一樣,類型除了最后一個之外,也都是一樣的。
    002
  3. 上面case語句里調(diào)用的方法如下圖,可以分為2個或3個步驟,步驟1組裝參數(shù)對象,步驟2調(diào)用alipayService.getTradeNo方法,步驟3根據(jù)步驟2的返回內(nèi)容做后續(xù)處理,最后返回阿里支付的交易編號。
    003

優(yōu)化前代碼的一些問題

  1. 對于上面圖002中的getPayInfo方法,如果業(yè)務(wù)要在增加一種訂單類型,那么就需要再增加一個case的同時,增加case中的步驟1,步驟2代碼。這樣的話就會導(dǎo)致每次增加一種新訂單類型都需要維護很大的一大坨代碼。而且如果某個現(xiàn)有訂單類型的步驟1,步驟2邏輯需要改動,也需要在這個switch case里修改。如果用高大上的語言來描述,那就是沒有符合開閉原則,沒有做到單一職責(zé)原則,沒有做到高內(nèi)聚低耦合等等。

怎么來優(yōu)化呢

  1. 上面的代碼圖002中service里的getPayInfo方法中, case里寫了大量(省略了7個case分支)的重復(fù)代碼,各個case中的步驟1,步驟2都是可以抽象出來的。
  2. 圖002中case里調(diào)用各自的方法獲取tradeNo。這些方法在圖003中可以看出來也有共通性,也可以抽象出來。
  3. 從整體的代碼邏輯上來看,圖002中的每個case分支,及其后續(xù)操作,似乎都有著同樣的共性,只有少部分的邏輯有各自的特性,那么很容易想到用模版方法模式來做優(yōu)化。因為有很多case分支,也很容易聯(lián)想到策略模式了。
  4. 優(yōu)化的思路如下:
    1. 把不同的訂單類型都當(dāng)作不同的策略,那么也就是每一個case中的邏輯都是一個不同的策略;
    2. 因為每一個訂單類型的代碼流程大致都相同,只有少數(shù)不同的步驟,那么就定義一個模版類,將相同的步驟統(tǒng)一寫在模版類里,不同的步驟,讓策略類去繼承模版類后,自己去實現(xiàn);
    3. 把switch case轉(zhuǎn)移到一個工廠類里,通過工廠類生成不同的策略對象;
    4. 外層代碼調(diào)用工廠類返回對象的模版方法,即可完成訂單支付流程。

優(yōu)化之后的代碼

  1. 首先創(chuàng)建一個模版類的接口IOrderAlipayStrategy,模版類實現(xiàn)這個接口,該接口只有一個方法payThroughAlipay,具體使用請看 2.
/**
 * @Author: yesiming
 * @Platform: Mac
 * @Date: 5:14 下午 2020/9/25
 *
 * 代碼優(yōu)化:支付寶小程序支付優(yōu)化成通過工廠,策略,模版 3種模式實現(xiàn)
 * 此接口為:訂單支付的策略接口
 */
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用戶存放模版方法返回的內(nèi)容,代碼如下。

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. 創(chuàng)建抽象模版類,實現(xiàn)IOrderAlipayStrategy接口,并且實現(xiàn)payThroughAlipay方法,可以從代碼中看到,payThroughAlipay里的1,2,3,4,5,這5個步驟中,只有1,5,是與業(yè)務(wù)相關(guān)的,2,3,4是與業(yè)務(wù)無關(guān)的。那么該方法中年的2,3,4步驟的代碼就可以在該模版類中實現(xiàn),并且由該方法調(diào)用。1,5步驟因為是與業(yè)務(wù)相關(guān),也就是說對于不同的訂單類型,實現(xiàn)不同,那么1,5步驟的方法在模版類中就寫成抽象方法,具體實現(xiàn)推遲到繼承模版方法的各個策略類中。當(dāng)然某些場景下1,5步驟也可以寫成默認實現(xiàn),具體策略類對默認實現(xiàn)不滿意的話,可以覆蓋默認實現(xiàn)。
/**
 * @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. 準(zhǔn)備參數(shù),業(yè)務(wù)相關(guān)
        OrderInfo orderInfo = prepareArgs(orderId, payType);

        // 2. 組裝DTO對象
        AlipayTradeCreateBizContentDTO bizContentInputDTO =
                assembleAlipayTradeCreateBizContentDTO(alipayUserId, orderInfo);

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

        // 4. 附加對TradeNo的處理
        attachTradeNo(orderInfo, tradeNo);

        // 5. 后續(xù)處理,業(yè)務(wù)相關(guān)
        followUp(tradeNo);
        return orderInfo;
    }

    /**
     * 業(yè)務(wù)相關(guān),返回不同Order類型
     * @param orderId
     * @return
     */
    public abstract OrderInfo prepareArgs(String orderId, PayType payType);

    /**
     * 組裝DTO
     * 業(yè)務(wù)無關(guān),模版實現(xiàn)
     */
    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;
    }

    /**
     * 調(diào)用Alipay支付
     * 業(yè)務(wù)無關(guān),模版實現(xiàn)
     */
    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返回值
     * 業(yè)務(wù)無關(guān),模版實現(xiàn)
     */
    private void attachTradeNo(OrderInfo orderInfo, String tradeNo) {
        orderInfo.setTradeNo(tradeNo);
    }

    /**
     * 業(yè)務(wù)相關(guān),自定義后續(xù)處理,
     * 如果需要后續(xù)處理可以重寫此方法
     *
     * @param tradeNo
     */
    public void followUp(String tradeNo) {}
}
  1. 到此為止,已經(jīng)把優(yōu)化之前的每個case分支里的流程框架寫完了,接下來只需要完成每個case分支的策略類即可。
  2. 策略類需要繼承模版類,并且重寫模版類中的業(yè)務(wù)相關(guān)方法。
    4.1 策略類:員工訂單服務(wù)費
/**
 * @Author: yesiming
 * @Platform: Mac
 * @Date: 5:13 下午 2020/9/25
 *
 * 員工訂單服務(wù)費
 */
@Component
public class OrgOrderServiceStrategy extends AbstractOrderAlipayStrategy {
    
    @Autowired
    private OrgOrderMapper orgOrderMapper;

    /**
     * 重寫了模版類中的業(yè)務(wù)相關(guān)方法,實現(xiàn)業(yè)務(wù)的獨立性
     * @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 策略類:員工訂單商品服務(wù)費

@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;

    /**
     * 簡化方法之間的數(shù)據(jù)傳遞
     */
    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;
    }

    /**
     * 后續(xù)處理,
     * 需要prepareArgs()方法執(zhí)行過程中的數(shù)據(jù),還不想通過返回值來傳遞
     * 可以通過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個策略類,只實現(xiàn)了prepareArgs方法,沒有實現(xiàn)followUp方法,那么對于這2個策略類來說,followUp的處理將使用模版類里提供的默認處理方式,也就是什么都不做,第三個策略類重寫了followUp方法,那么它將使用自己的重寫邏輯。

  1. 策略類寫完了,下面就可以在service里調(diào)用了??墒沁@么多策略類如何調(diào)用呢,怎么知道調(diào)用哪一個呢?可以將switch case放到一個工廠類里面。需要注意的是,使用spring框架時,這里一定要注入需要用到的策略類,不能在case里new,new的話會導(dǎo)致策略類里的屬性不被填充。
    ---------- 這里有個小小的坑,請看后文【后記】部分 ----------
/**
 * @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. 有了工廠類,那么現(xiàn)在就能在service里調(diào)用了。
    為了看出來區(qū)別,優(yōu)化前的那一大段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:
//        }

        // 工廠根據(jù)payType創(chuàng)建并返回對應(yīng)的策略類。
        IOrderAlipayStrategy orderAlipayStrategy = orderStrategyFactory.createOrderInstannce(payType);
        // 策略類去執(zhí)行模版方法
        OrderInfo orderInfo = orderAlipayStrategy.payThroughAlipay(alipayConfig, payType, orderId, alipayUserId);

        savePayLog(payType, orderId, orderInfo.getPayOrderId(), PayChannel.ALI, orderInfo.getTradeNo(), alipayUserId, orderInfo.getTotalAmount());
        return orderInfo.getTradeNo();
    }
  1. 優(yōu)化完成,看一下增加的類與接口
    004

總結(jié)

對于大部分業(yè)務(wù)代碼,遇到這種業(yè)務(wù)場景應(yīng)該也不少,而模版方法與策略模式的結(jié)合正是用來解決這種場景的利器,當(dāng)然,當(dāng)策略特別多的時候也會導(dǎo)致其他問題,比如類爆炸,不過那是另一個話題了。

后記

以上的代碼,基本上實現(xiàn)了目標(biāo),提取出通用代碼,各個策略類各司其職只專注于自身特性的業(yè)務(wù)代碼,通過工廠類產(chǎn)生需要的業(yè)務(wù)類對象。
但是,有一個缺陷:工廠類,工廠類通過switch case語句來產(chǎn)生策略類對象,如果訂單類型增加了,也就需要策略類,工廠也就需要增加對新策略類的支持。
這時就需要修改工廠類,修改類容如下:
1. 增加新的策略類屬性;
2. 增加case語句。
好的,問題來了:開閉原則被打破了!
如何繼續(xù)優(yōu)化這部分呢?請看《寫出優(yōu)雅的業(yè)務(wù)代碼(2):優(yōu)化掉工廠模式中的 switch case》

最后附上uml圖
004
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容