设计模式之工厂方法模式

设计模式之工厂方法模式

点击浏览其他设计模式

一、工厂方法模式介绍
工厂方法

工厂模式又称工厂方法模式,是一种创建型设计模式,其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。

这种设计模式也是 Java 开发中最常见的一种模式,它的主要意图是定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

简单说就是为了提供代码结构的扩展性,屏蔽每一个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可,同时,这也是去掉众多ifeslse的方式。当然这可能也有一些缺点,比如需要实现的类非常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中,逐步降低。

二、模拟发奖多种商品
模拟发奖多种商品
为了可以让整个学习的案例更加贴近实际开发,这里模拟互联网中在营销场景下的业务。由于营销场景的复杂、多变、临时的特性,它所需要的设计需要更加深入,否则会经常面临各种紧急CRUD操作,从而让代码结构混乱不堪,难以维护。

在营销场景中经常会有某个用户做了一些操作;打卡、分享、留言、邀请注册等等,进行返利积分,最后通过积分在兑换商品,从而促活和拉新。

那么在这里我们模拟积分兑换中的发放多种类型商品,假如现在我们有如下三种类型的商品接口;

序号 类型 接口
1 优惠券 CouponResult sendCoupon(String uId, String couponNumber, String uuid)
2 实物商品 Boolean deliverGoods(DeliverReq req)
3 第三方爱奇艺兑换卡 void grantToken(String bindMobileNumber, String cardId)
「从以上接口来看有如下信息:」

三个接口返回类型不同,有对象类型、布尔类型、还有一个空类型。
入参不同,发放优惠券需要仿重、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。
另外可能会随着后续的业务的发展,会新增其他种商品类型。因为你所有的开发需求都是随着业务对市场的拓展而带来的。
四、用一坨坨代码实现
如果不考虑任何扩展性,只为了尽快满足需求,那么对这么几种奖励发放只需使用ifelse语句判断,调用不同的接口即可满足需求。可能这也是一些刚入门编程的小伙伴,常用的方式。接下来我们就先按照这样的方式来实现业务的需求。

  1. 工程结构
    itstack-demo-design-1-01
    └── src
    ├── main
    │ └── java
    │ └── org.itstack.demo.design
    │ ├── AwardReq.java
    │ ├── AwardRes.java
    │ └── PrizeController.java
    └── test
    └── java
    └── org.itstack.demo.design.test
    └── ApiTest.java
    工程结构上非常简单,一个入参对象 AwardReq 、一个出参对象 AwardRes,以及一个接口类 PrizeController
  2. ifelse实现需求
public class PrizeController {

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

    public AwardRes awardToUser(AwardReq req) {
        String reqJson = JSON.toJSONString(req);
        AwardRes awardRes = null;
        try {
            logger.info("奖品发放开始{}。req:{}", req.getuId(), reqJson);
            // 按照不同类型方法商品[1优惠券、2实物商品、3第三方兑换卡(爱奇艺)]
            if (req.getAwardType() == 1) {
                CouponService couponService = new CouponService();
                CouponResult couponResult = couponService.sendCoupon(req.getuId(), req.getAwardNumber(), req.getBizId());
                if ("0000".equals(couponResult.getCode())) {
                    awardRes = new AwardRes("0000", "发放成功");
                } else {
                    awardRes = new AwardRes("0001", couponResult.getInfo());
                }
            } else if (req.getAwardType() == 2) {
                GoodsService goodsService = new GoodsService();
                DeliverReq deliverReq = new DeliverReq();
                deliverReq.setUserName(queryUserName(req.getuId()));
                deliverReq.setUserPhone(queryUserPhoneNumber(req.getuId()));
                deliverReq.setSku(req.getAwardNumber());
                deliverReq.setOrderId(req.getBizId());
                deliverReq.setConsigneeUserName(req.getExtMap().get("consigneeUserName"));
                deliverReq.setConsigneeUserPhone(req.getExtMap().get("consigneeUserPhone"));
                deliverReq.setConsigneeUserAddress(req.getExtMap().get("consigneeUserAddress"));
                Boolean isSuccess = goodsService.deliverGoods(deliverReq);
                if (isSuccess) {
                    awardRes = new AwardRes("0000", "发放成功");
                } else {
                    awardRes = new AwardRes("0001", "发放失败");
                }
            } else if (req.getAwardType() == 3) {
                String bindMobileNumber = queryUserPhoneNumber(req.getuId());
                IQiYiCardService iQiYiCardService = new IQiYiCardService();
                iQiYiCardService.grantToken(bindMobileNumber, req.getAwardNumber());
                awardRes = new AwardRes("0000", "发放成功");
            }
            logger.info("奖品发放完成{}。", req.getuId());
        } catch (Exception e) {
            logger.error("奖品发放失败{}。req:{}", req.getuId(), reqJson, e);
            awardRes = new AwardRes("0001", e.getMessage());
        }

        return awardRes;
    }

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

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

}

如上就是使用 ifelse 非常直接的实现出来业务需求的一坨代码,如果仅从业务角度看,研发如期甚至提前实现了功能。
那这样的代码目前来看并不会有什么问题,但如果在经过几次的迭代和拓展,接手这段代码的研发将十分痛苦。重构成本高需要理清之前每一个接口的使用,测试回归验证时间长,需要全部验证一次。这也就是很多人并不愿意接手别人的代码,如果接手了又被压榨开发时间。那么可想而知这样的 ifelse 还会继续增加。
3. 测试验证
写一个单元测试来验证上面编写的接口方式,养成单元测试的好习惯会为你增强代码质量。

「编写测试类:」

@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");
    Map<String,String> extMap = new HashMap<String,String>();
    extMap.put("consigneeUserName", "谢飞机");
    extMap.put("consigneeUserPhone", "15200292123");
    extMap.put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
    req02.setExtMap(extMap);

    commodityService_2.sendCommodity("10001","9820198721311","1023000020112221113", extMap);

    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));
}

「结果:」

模拟发放优惠券测试

22:17:55.668 [main] INFO  o.i.demo.design.PrizeController - 奖品发放开始10001。req:{"awardNumber":"EGM1023938910232121323432","awardType":1,"bizId":"791098764902132","uId":"10001"}
模拟发放优惠券一张:10001,EGM1023938910232121323432,791098764902132
22:17:55.671 [main] INFO  o.i.demo.design.PrizeController - 奖品发放完成1000122:17:55.673 [main] INFO  org.itstack.demo.test.ApiTest - 请求参数:{"uId":"10001","bizId":"791098764902132","awardNumber":"EGM1023938910232121323432","awardType":1}
22:17:55.674 [main] INFO  org.itstack.demo.test.ApiTest - 测试结果:{"code":"0000","info":"发放成功"}

模拟方法实物商品

22:17:55.675 [main] INFO  o.i.demo.design.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"}
22:17:55.677 [main] INFO  o.i.demo.design.PrizeController - 奖品发放完成1000122:17:55.677 [main] INFO  org.itstack.demo.test.ApiTest - 请求参数:{"extMap":{"consigneeUserName":"谢飞机","consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserPhone":"15200292123"},"uId":"10001","bizId":"1023000020112221113","awardNumber":"9820198721311","awardType":2}
22:17:55.677 [main] INFO  org.itstack.demo.test.ApiTest - 测试结果:{"code":"0000","info":"发放成功"}

第三方兑换卡(爱奇艺)

22:17:55.678 [main] INFO  o.i.demo.design.PrizeController - 奖品发放开始10001。req:{"awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3,"uId":"10001"}
模拟发放爱奇艺会员卡一张:15200101232AQY1xjkUodl8LO975GdfrYUio
22:17:55.678 [main] INFO  o.i.demo.design.PrizeController - 奖品发放完成1000122:17:55.678 [main] INFO  org.itstack.demo.test.ApiTest - 请求参数:{"uId":"10001","awardNumber":"AQY1xjkUodl8LO975GdfrYUio","awardType":3}
22:17:55.678 [main] INFO  org.itstack.demo.test.ApiTest - 测试结果:{"code":"0000","info":"发放成功"}

Process finished with exit code 0

运行结果正常,满足当前所有业务产品需求,写的还很快。但!实在难以为维护!
三、工厂模式优化代码
接下来使用工厂方法模式来进行代码优化,也算是一次很小的「重构」。整理重构会你会发现代码结构清晰了、也具备了下次新增业务需求的扩展性。但在实际使用中还会对此进行完善,目前的只是抽离出最核心的部分体现到你面前,方便学习。

  1. 工程结构
    itstack-demo-design-1-02
    └── src
    ├── main
    │ └── java
    │ └── org.itstack.demo.design
    │ ├── store
    │ │ ├── impl
    │ │ │ ├── CardCommodityService.java
    │ │ │ ├── CouponCommodityService.java
    │ │ │ └── GoodsCommodityService.java
    │ │ └── ICommodity.java
    │ └── StoreFactory.java
    └── test
    └── java
    └── org.itstack.demo.design.test
    └── ApiTest.java
    首先,从上面的工程结构中你是否一些感觉,比如;它看上去清晰了、这样分层可以更好扩展了、似乎可以想象到每一个类做了什么。
    如果还不能理解为什么这样修改,也没有关系。因为你是在通过这样的文章,来学习设计模式的魅力。并且再获取源码后,进行实际操作几次也就慢慢掌握了工厂模式的技巧。
  2. 代码实现
    2.1 定义发奖接口
public interface ICommodity {

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

}

所有的奖品无论是实物、虚拟还是第三方,都需要通过我们的程序实现此接口进行处理,以保证最终入参出参的统一性。
接口的入参包括;用户ID、奖品ID、业务ID以及扩展字段用于处理发放实物商品时的收获地址。
2.2 实现奖品发放接口
「优惠券」

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";
    }

}

从上面可以看到每一种奖品的实现都包括在自己的类中,新增、修改或者删除都不会影响其他奖品功能的测试,降低回归测试的可能。
后续在新增的奖品只需要按照此结构进行填充即可,非常易于维护和扩展。
在统一了入参以及出参后,调用方不在需要关心奖品发放的内部逻辑,按照统一的方式即可处理。
2.3 创建商店工厂

public class StoreFactory {

    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("不存在的商品服务类型");
    }

}

这里我们定义了一个商店的工厂类,在里面按照类型实现各种商品的服务。可以非常干净整洁的处理你的代码,后续新增的商品在这里扩展即可。如果你不喜欢if判断,也可以使用switch或者map配置结构,会让代码更加干净。
另外很多代码检查软件和编码要求,不喜欢if语句后面不写扩展,这里是为了更加干净的向你体现逻辑。在实际的业务编码中可以添加括号。
3. 测试验证
「编写测试类:」

@Test
public void test_commodity() throws Exception {
    StoreFactory storeFactory = new StoreFactory();
    // 1. 优惠券
    ICommodity commodityService_1 = storeFactory.getCommodityService(1);
    commodityService_1.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);
    // 2. 实物商品
    ICommodity commodityService_2 = storeFactory.getCommodityService(2);
    
    Map<String,String> extMap = new HashMap<String,String>();
    extMap.put("consigneeUserName", "谢飞机");
    extMap.put("consigneeUserPhone", "15200292123");
    extMap.put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");

    commodityService_2.sendCommodity("10001","9820198721311","1023000020112221113", extMap);
    // 3. 第三方兑换卡(爱奇艺)
    ICommodity commodityService_3 = storeFactory.getCommodityService(3);
    commodityService_3.sendCommodity("10001","AQY1xjkUodl8LO975GdfrYUio",null,null);
}

「结果:」

模拟发放优惠券一张:10001,EGM1023938910232121323432,791098764902132
22:48:10.922 [main] INFO  o.i.d.d.s.i.CouponCommodityService - 请求参数[优惠券] => uId:10001 commodityId:EGM1023938910232121323432 bizId:791098764902132 extMap:null
22:48:10.957 [main] INFO  o.i.d.d.s.i.CouponCommodityService - 测试结果[优惠券]{"code":"0000","info":"发放成功"}
模拟发货实物商品一个:{"consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserName":"谢飞机","consigneeUserPhone":"15200292123","orderId":"1023000020112221113","sku":"9820198721311","userName":"花花","userPhone":"15200101232"}
22:48:10.962 [main] INFO  o.i.d.d.s.impl.GoodsCommodityService - 请求参数[优惠券] => uId:10001 commodityId:9820198721311 bizId:1023000020112221113 extMap:{"consigneeUserName":"谢飞机","consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserPhone":"15200292123"}
22:48:10.962 [main] INFO  o.i.d.d.s.impl.GoodsCommodityService - 测试结果[优惠券]true
模拟发放爱奇艺会员卡一张:15200101232null
22:48:10.963 [main] INFO  o.i.d.d.s.impl.CardCommodityService - 请求参数[爱奇艺兑换卡] => uId:10001 commodityId:AQY1xjkUodl8LO975GdfrYUio bizId:null extMap:null
22:48:10.963 [main] INFO  o.i.d.d.s.impl.CardCommodityService - 测试结果[爱奇艺兑换卡]:success

Process finished with exit code 0

运行结果正常,既满足了业务产品需求,也满足了自己对代码的追求。这样的代码部署上线运行,内心不会恐慌,不会觉得半夜会有电话。
另外从运行测试结果上也可以看出来,在进行封装后可以非常清晰的看到一整套发放奖品服务的完整性,统一了入参、统一了结果。
四、总结
从上到下的优化来看,工厂方法模式并不复杂,甚至这样的开发结构在你有所理解后,会发现更加简单了。
那么这样的开发的好处知道后,也可以总结出来它的优点;避免创建者与具体的产品逻辑耦合、满足单一职责,每一个业务逻辑实现都在所属自己的类中完成、满足开闭原则,无需更改使用调用方就可以在程序中引入新的产品类型。但这样也会带来一些问题,比如有非常多的奖品类型,那么实现的子类会极速扩张。因此也需要使用其他的模式进行优化,这些在后续的设计模式中会逐步涉及到。
从案例入手看设计模式往往要比看理论学的更加容易,因为案例是缩短理论到上手的最佳方式,如果你已经有所收获,一定要去尝试实操。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值