设计模式 - 创建型模式_工厂方法模式

Posted 小小工匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式 - 创建型模式_工厂方法模式相关的知识,希望对你有一定的参考价值。

文章目录


创建型模式

创建型模式提供创建对象的机制, 能够提升已有代码的灵活性和可复⽤性。

类型实现要点
工厂方法定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。
抽象工厂提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定它们具体的类。
建造者将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
原型⽤原型实例指定创建对象的种类,并且通过拷⻉这些原型创建新的对象。
单例保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。

概述

优秀的代码在结构设计上松耦合易读易扩展,在领域实现上⾼内聚不对外暴漏实现细节不被外部⼲扰。

⼯⼚模式⼜称⼯⼚⽅法模式,是⼀种创建型设计模式,其在⽗类中提供⼀个创建对象的⽅法, 允许⼦类决定实例化对象的类型。

它的主要意图是定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。

优点: 简单说就是为了提供代码结构的扩展性,屏蔽每⼀个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调⽤即可,同时,这也是去掉众多 ifelse 的⽅式。

缺点: ⽐如需要实现的类⾮常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使⽤中,逐步降低。


Case

模拟积分兑换中的发放多种类型商品,假如现在我们有如下三种类型的商品接⼝

  • 优惠券 : CouponResult sendCoupon(String uId, String couponNumber, String uuid)
  • 实物商品 : Boolean deliverGoods(DeliverReq req)
  • 第三⽅爱奇艺兑换卡:void grantToken(String bindMobileNumber, String cardId)


从以上接⼝来看有如下信息:
三个接⼝返回类型不同,有对象类型、布尔类型、还有⼀个空类型。也可能会随着后续的业务的发展,会新增其他种商品类型。


Bad Impl

不考虑任何扩展性,只为了尽快满⾜需求,那么对这么⼏种奖励发放只需使⽤ifelse语句判断,调⽤不同的接⼝即可满⾜需求。

【if else 大法实现】

public class PrizeController 

    private Logger logger = LoggerFactory.getLogger(PrizeController.class);

    public AwardRes awardToUser(AwardReq req) 
        String reqJson = JSON.toJSONString(req);
        AwardRes awardRes = null;
        logger.info("奖品发放开始。req:", req.getuId(), reqJson);
       
        // 按照不同类型方法商品[1优惠券、2实物商品、3第三方兑换卡(爱奇艺)]
         if (req.getAwardType() == 1) 
           .......
           .......
           .......
            awardRes = new AwardRes("0000", "发放成功");
          else if (req.getAwardType() == 2) 
           .......
           .......
           .......
            awardRes = new AwardRes("0000", "发放成功");
          else if (req.getAwardType() == 3) 
             ......
             ......
             ......
             awardRes = new AwardRes("0000", "发放成功");
         
         logger.info("奖品发放完成。", req.getuId());

        return awardRes;
    

  ......
  ......
  ......



这样的代码⽬前来看并不会有什么问题,但如果在经过⼏次的迭代和拓展,非常痛苦。

  • 重构成本⾼,需要梳理之前每⼀个接⼝的使⽤;
  • 测试回归验证时间⻓,需要全部验证⼀次。

这也就是很多⼈并不愿意接⼿别⼈的代码,如果接⼿了⼜被压榨开发时间。那么可想⽽知这样的 ifelse 还会继续增加。


【测试验证】

写⼀个单元测试来验证上⾯编写的接⼝⽅式

@Test
    public void test_awardToUser() 

        PrizeController prizeController = new PrizeController();

        System.out.println("\\r\\n模拟发放优惠券测试\\r\\n");
        // 模拟发放优惠券测试
        AwardReq req01 = new AwardReq();
        req01.setuId("10001");
        req01.setAwardType(1);
        req01.setAwardNumber("EGM1023938910232121323432");
        req01.setBizId("791098764902132");
        AwardRes awardRes01 = prizeController.awardToUser(req01);

        logger.info("请求参数:", JSON.toJSON(req01));
        logger.info("测试结果:", JSON.toJSON(awardRes01));

        System.out.println("\\r\\n模拟方法实物商品\\r\\n");
        // 模拟方法实物商品
        AwardReq req02 = new AwardReq();
        req02.setuId("10001");
        req02.setAwardType(2);
        req02.setAwardNumber("9820198721311");
        req02.setBizId("1023000020112221113");
        req02.setExtMap(new HashMap<String, String>() 
            put("consigneeUserName", "谢飞机");
            put("consigneeUserPhone", "15200292123");
            put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
        );

        AwardRes awardRes02 = prizeController.awardToUser(req02);
        logger.info("请求参数:", JSON.toJSON(req02));
        logger.info("测试结果:", JSON.toJSON(awardRes02));

        System.out.println("\\r\\n第三方兑换卡(爱奇艺)\\r\\n");
        AwardReq req03 = new AwardReq();
        req03.setuId("10001");
        req03.setAwardType(3);
        req03.setAwardNumber("AQY1xjkUodl8LO975GdfrYUio");

        AwardRes awardRes03 = prizeController.awardToUser(req03);
        logger.info("请求参数:", JSON.toJSON(req03));
        logger.info("测试结果:", JSON.toJSON(awardRes03));

    

日志输出

模拟发放优惠券测试

14:16:29.947 [main] INFO  com.artisan.PrizeController - 奖品发放开始10001。req:"awardNumber":"EGM1023938910232121323432","awardType":1,"bizId":"791098764902132","uId":"10001"
模拟发放优惠券一张:10001,EGM1023938910232121323432,791098764902132
14:16:29.951 [main] INFO  com.artisan.PrizeController - 奖品发放完成1000114:16:29.953 [main] INFO  com.artisan.ApiTest - 请求参数:"uId":"10001","bizId":"791098764902132","awardNumber":"EGM1023938910232121323432","awardType":1
14:16:29.955 [main] INFO  com.artisan.ApiTest - 测试结果:"code":"0000","info":"发放成功"

模拟方法实物商品

14:16:29.956 [main] INFO  com.artisan.PrizeController - 奖品发放开始10001。req:"awardNumber":"9820198721311","awardType":2,"bizId":"1023000020112221113","extMap":"consigneeUserName":"谢飞机","consigneeUserPhone":"15200292123","consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","uId":"10001"
模拟发货实物商品一个:"consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserName":"谢飞机","consigneeUserPhone":"15200292123","orderId":"1023000020112221113","sku":"9820198721311","userName":"花花","userPhone":"15200101232"
14:16:29.959 [main] INFO  com.artisan.PrizeController - 奖品发放完成1000114:16:29.959 [main] INFO  com.artisan.ApiTest - 请求参数:"extMap":"consigneeUserName":"谢飞机","consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserPhone":"15200292123","uId":"10001","bizId":"1023000020112221113","awardNumber":"9820198721311","awardType":2
14:16:29.959 [main] INFO  com.artisan.ApiTest - 测试结果:"code":"0000","info":"发放成功"

第三方兑换卡(爱奇艺)

14:16:29.959 [main] INFO  com.artisan.PrizeController - 奖品发放开始10001。req:"awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3,"uId":"10001"
模拟发放爱奇艺会员卡一张:15200101232AQY1xjkUodl8LO975GdfrYUio
14:16:29.960 [main] INFO  com.artisan.PrizeController - 奖品发放完成1000114:16:29.960 [main] INFO  com.artisan.ApiTest - 请求参数:"uId":"10001","awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3
14:16:29.960 [main] INFO  com.artisan.ApiTest - 测试结果:"code":"0000","info":"发放成功"

运⾏结果正常,满⾜当前所有业务产品需求,写的还很快。但实在难以为维护!


Better Impl (⼯⼚模式优化代码)

接下来使⽤⼯⼚⽅法模式来进⾏代码优化,也算是⼀次很⼩的重构。整理重构后代码结构清晰了、也具备了下次新增业务需求的扩展性。

相关类的具体作用如下:

代码目录如下:

从上⾯的⼯程结构中: 它看上去清晰了、这样分层可以更好扩展了、似乎可以想象到每⼀个类做了什么。


接口定义

public interface ICommodity 

    void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception;



  • 所有的奖品⽆论是实物、虚拟还是第三⽅,都需要实现此接⼝进⾏处理,以保证最终⼊参出参的统⼀性。
  • 接⼝的⼊参包括; ⽤户ID 、 奖品ID 、 业务ID 以及 扩展字段 ⽤于处理发放实物商品时的收获地址

实现奖品发放接⼝

【优惠券】

public class CouponCommodityService implements ICommodity 

    private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class);

    private CouponService couponService = new CouponService();

    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception 
        CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId);
        logger.info("请求参数[优惠券] => uId: commodityId: bizId: extMap:", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[优惠券]:", JSON.toJSON(couponResult));
        if (!"0000".equals(couponResult.getCode())) throw new RuntimeException(couponResult.getInfo());
    




【实物商品】

public class GoodsCommodityService implements ICommodity 

    private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class);

    private GoodsService goodsService = new GoodsService();

    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception 
        DeliverReq deliverReq = new DeliverReq();
        deliverReq.setUserName(queryUserName(uId));
        deliverReq.setUserPhone(queryUserPhoneNumber(uId));
        deliverReq.setSku(commodityId);
        deliverReq.setOrderId(bizId);
        deliverReq.setConsigneeUserName(extMap.get("consigneeUserName"));
        deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone"));
        deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress"));

        Boolean isSuccess = goodsService.deliverGoods(deliverReq);

        logger.info("请求参数[实物商品] => uId: commodityId: bizId: extMap:", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[实物商品]:", isSuccess);

        if (!isSuccess) throw new RuntimeException("实物商品发放失败");
    

    private String queryUserName(String uId) 
        return "花花";
    

    private String queryUserPhoneNumber(String uId) 
        return "15200101232";
    



【第三方兑换卡】

public class CardCommodityService implements ICommodity 

    private Logger logger = LoggerFactory.getLogger(CardCommodityService.class);

    // 模拟注入
    private IQiYiCardService iQiYiCardService = new IQiYiCardService();

    public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception 
        String mobile = queryUserMobile(uId);
        iQiYiCardService.grantToken(mobile, bizId);
        logger.info("请求参数[爱奇艺兑换卡] => uId: commodityId: bizId: extMap:", uId, commodityId, bizId, JSON.toJSON(extMap));
        logger.info("测试结果[爱奇艺兑换卡]:success");
    

    private String queryUserMobile(String uId) 
        return "15200101232";
    



从上⾯可以看出

  • 每⼀种奖品的实现都包括在⾃⼰的类中,新增、修改或者删除都不会影响其他奖品功能的测试,降低回归测试的可能。

  • 如果有新增的奖品只需要按照此结构进⾏填充对应的实现类即可,易于维护和扩展。

  • 在统⼀了⼊参以及出参后,调⽤⽅不在需要关⼼奖品发放的内部逻辑,按照统⼀的⽅式即可处理


创建商店⼯⼚


public class StoreFactory 

    /**
     * 奖品类型方式实例化
     * @param commodityType 奖品类型
     * @return              实例化对象
     */
    public ICommodity getCommodityService(Integer commodityType) 
        if (null == commodityType) return null;
        if (1 == commodityType) return new CouponCommodityService();
        if (2 == commodityType) return new GoodsCommodityService();
        if (3 == commodityType) return new CardCommodityService();
        throw new RuntimeException("不存在的奖品服务类型");
    

    /**
     * 奖品类信息方式实例化
     * @param clazz 奖品类
     * @return      实例化对象
     */
    public 以上是关于设计模式 - 创建型模式_工厂方法模式的主要内容,如果未能解决你的问题,请参考以下文章

设计模式 - 创建型模式_工厂方法模式

设计模式_创建型模式_简单工厂模式_案例

设计模式 - 创建型模式_抽象工厂模式

设计模式 - 创建型模式_抽象工厂模式

设计模式 - 创建型模式_抽象工厂模式

设计模式 - 创建型模式_原型模式