一、开发环境

  1. JDK 1.8
    2. Idea + Maven

    二、工厂方法模式介绍

    1631430449(1).png
    工厂模式又称工厂方法模式,是一种创建型设计模式,其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
    这种设计模式也是 Java 开发中最常见的一种模式,它的主要意图是定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
    简单说就是为了提供代码结构的扩展性,屏蔽每一个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可,同时,这也是去掉众多 ifelse 的方式。当然这可能也有一些缺点,比如需要实现的类非常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中,逐步降低。

    三、模拟发奖多种商品

    1631430626(1).png
    为了可以让整个学习的案例更加贴近实际开发,这里模拟互联网中在营销场景下的业务。由于营销场景的复杂、多变、临时的特性,它所需要的设计需要更加深入,否则会经常面临各种紧急CRUD操作,从而让代码结构混乱不堪,难以维护。
    在营销场景中经常会有某个用户做了一些操作;打卡、分享、留言、邀请注册等等,进行返利积分,最后通过积分在兑换商品,从而促活和拉新。
    那么在这里我们模拟积分兑换中的发放多种类型商品,假如现在我们有如下三种类型的商品接口;
    1631431154(1).png
    从以上接口来看有如下信息:
    三个接口返回类型不同,有对象类型、布尔类型、还有一个空类型。
    入参不同,发放优惠券需要仿重、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。
    另外可能会随着后续的业务的发展,会新增其他种商品类型。因为你所有的开发需求都是随着业务对市场的拓展而带来的。

    四、用一坨坨代码实现

    如果不考虑任何扩展性,只为了尽快满足需求,那么对这么几种奖励发放只需使用ifelse语句判断,调用不同的接口即可满足需求。可能这也是一些刚入门编程的小伴,常用的方式。接下来我们就先按照这样的方式来实现业务的需求。

    1. 工程结构

    1631433255(1).png

    2.ifelse实现需求

    ```java public class PrizeController {

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

    public AwardRes awardToUser(AwardReq req) {

    1. String reqJson = JSON.toJSONString(req);
    2. AwardRes awardRes = null;
    3. try {
    4. logger.info("奖品发放开始{}。req:{}", req.getuId(), reqJson);
    5. // 按照不同类型方法商品[1优惠券、2实物商品、3第三方兑换卡(爱奇艺)]
    6. if (req.getAwardType() == 1) {
    7. CouponService couponService = new CouponService();
    8. CouponResult couponResult = couponService.sendCoupon(req.getuId(), req.getAwardNumber(), req.getBizId());
    9. if ("0000".equals(couponResult.getCode())) {
    10. awardRes = new AwardRes("0000", "发放成功");
    11. } else {
    12. awardRes = new AwardRes("0001", couponResult.getInfo());
    13. }
    14. } else if (req.getAwardType() == 2) {
    15. GoodsService goodsService = new GoodsService();
    16. DeliverReq deliverReq = new DeliverReq();
    17. deliverReq.setUserName(queryUserName(req.getuId()));
    18. deliverReq.setUserPhone(queryUserPhoneNumber(req.getuId()));
    19. deliverReq.setSku(req.getAwardNumber());
    20. deliverReq.setOrderId(req.getBizId());
    21. deliverReq.setConsigneeUserName(req.getExtMap().get("consigneeUserName"));
    22. deliverReq.setConsigneeUserPhone(req.getExtMap().get("consigneeUserPhone"));
    23. deliverReq.setConsigneeUserAddress(req.getExtMap().get("consigneeUserAddress"));
    24. Boolean isSuccess = goodsService.deliverGoods(deliverReq);
    25. if (isSuccess) {
    26. awardRes = new AwardRes("0000", "发放成功");
    27. } else {
    28. awardRes = new AwardRes("0001", "发放失败");
    29. }
    30. } else if (req.getAwardType() == 3) {
    31. String bindMobileNumber = queryUserPhoneNumber(req.getuId());
    32. IQiYiCardService iQiYiCardService = new IQiYiCardService();
    33. iQiYiCardService.grantToken(bindMobileNumber, req.getAwardNumber());
    34. awardRes = new AwardRes("0000", "发放成功");
    35. }
    36. logger.info("奖品发放完成{}。", req.getuId());
    37. } catch (Exception e) {
    38. logger.error("奖品发放失败{}。req:{}", req.getuId(), reqJson, e);
    39. awardRes = new AwardRes("0001", e.getMessage());
    40. }
    41. return awardRes;

    }

    private String queryUserName(String uId) {

    1. return "花花";

    }

    private String queryUserPhoneNumber(String uId) {

    1. return "15200101232";

    }

  1. 如上就是使用 ifelse 非常直接的实现出来业务需求的一坨代码,如果仅从业务角度看,研发如期甚至提前实现了功能。<br />那这样的代码目前来看并不会有什么问题,但如果在经过几次的迭代和拓展,接手这段代码的研发将十分痛苦。重构成本高需要理清之前每一个接口的使用,测试回归验证时间长,需要全部验一次。这也就是很多人并不愿意接⼿手别人的代码,如果接手了又被压榨开发时间。那么可想而知这样的 ifelse 还会继续增加。
  2. <a name="s1mQz"></a>
  3. ## 3. 测试验证
  4. 写一个单元测试来验证上面编写的接口方式,养成单元测试的好习惯会为你增强代码质量。
  5. <a name="YEdaf"></a>
  6. ### 编写测试类
  7. ```java
  8. public class ApiTest {
  9. private Logger logger = LoggerFactory.getLogger(ApiTest.class);
  10. @Test
  11. public void test_awardToUser() {
  12. PrizeController prizeController = new PrizeController();
  13. System.out.println("\r\n模拟发放优惠券测试\r\n");
  14. // 模拟发放优惠券测试
  15. AwardReq req01 = new AwardReq();
  16. req01.setuId("10001");
  17. req01.setAwardType(1);
  18. req01.setAwardNumber("EGM1023938910232121323432");
  19. req01.setBizId("791098764902132");
  20. AwardRes awardRes01 = prizeController.awardToUser(req01);
  21. logger.info("请求参数:{}", JSON.toJSON(req01));
  22. logger.info("测试结果:{}", JSON.toJSON(awardRes01));
  23. System.out.println("\r\n模拟方法实物商品\r\n");
  24. // 模拟方法实物商品
  25. AwardReq req02 = new AwardReq();
  26. req02.setuId("10001");
  27. req02.setAwardType(2);
  28. req02.setAwardNumber("9820198721311");
  29. req02.setBizId("1023000020112221113");
  30. req02.setExtMap(new HashMap<String, String>() {{
  31. put("consigneeUserName", "谢飞机");
  32. put("consigneeUserPhone", "15200292123");
  33. put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
  34. }});
  35. AwardRes awardRes02 = prizeController.awardToUser(req02);
  36. logger.info("请求参数:{}", JSON.toJSON(req02));
  37. logger.info("测试结果:{}", JSON.toJSON(awardRes02));
  38. System.out.println("\r\n第三方兑换卡(爱奇艺)\r\n");
  39. AwardReq req03 = new AwardReq();
  40. req03.setuId("10001");
  41. req03.setAwardType(3);
  42. req03.setAwardNumber("AQY1xjkUodl8LO975GdfrYUio");
  43. AwardRes awardRes03 = prizeController.awardToUser(req03);
  44. logger.info("请求参数:{}", JSON.toJSON(req03));
  45. logger.info("测试结果:{}", JSON.toJSON(awardRes03));
  46. }
  47. }

测试结果

1631432995(1).png
运行结果正常,满足当前所有业务产品需求,写的还很快。但!实在难以为维护!

五、工厂模式优化代码

接下来使用工厂方法模式来进行代码优化,也算是一次很小的重构。整理重构会你会发现代码结构清晰了、也具备了下次新增业务需求的扩展性。但在实际使用中还会对此进行完善,目前的只是抽离出最核
心的部分体现到你面前,方便便学习。

1. 工程结构

1631433213(1).png

2. 代码实现

2.1 定义发奖接口

  1. public interface ICommodity {
  2. void sendCommodity(String uId, String commodityId, String bizId,
  3. Map<String, String> extMap) throws Exception;
  4. }

2.2 实现奖品发放接口

优惠券

  1. public class CouponCommodityService implements ICommodity {
  2. private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class);
  3. private CouponService couponService = new CouponService();
  4. @Override
  5. public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
  6. CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId);
  7. logger.info("请求参数[优惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
  8. logger.info("测试结果[优惠券]:{}", JSON.toJSON(couponResult));
  9. if (!"0000".equals(couponResult.getCode())) {
  10. throw new RuntimeException(couponResult.getInfo());
  11. }
  12. }
  13. }

实物商品

  1. public class GoodsCommodityService implements ICommodity {
  2. private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class);
  3. private GoodsService goodsService = new GoodsService();
  4. @Override
  5. public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
  6. DeliverReq deliverReq = new DeliverReq();
  7. deliverReq.setUserName(queryUserName(uId));
  8. deliverReq.setUserPhone(queryUserPhoneNumber(uId));
  9. deliverReq.setSku(commodityId);
  10. deliverReq.setOrderId(bizId);
  11. deliverReq.setConsigneeUserName(extMap.get("consigneeUserName"));
  12. deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone"));
  13. deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress"));
  14. Boolean isSuccess = goodsService.deliverGoods(deliverReq);
  15. logger.info("请求参数[优惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
  16. logger.info("测试结果[优惠券]:{}", isSuccess);
  17. if (!isSuccess) {
  18. throw new RuntimeException("实物商品发放失败");
  19. }
  20. }
  21. private String queryUserName(String uId) {
  22. return "花花";
  23. }
  24. private String queryUserPhoneNumber(String uId) {
  25. return "15200101232";
  26. }
  27. }

第三方兑换卡

  1. public class CardCommodityService implements ICommodity {
  2. private Logger logger = LoggerFactory.getLogger(CardCommodityService.class);
  3. // 模拟注入
  4. private IQiYiCardService iQiYiCardService = new IQiYiCardService();
  5. @Override
  6. public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
  7. String mobile = queryUserMobile(uId);
  8. iQiYiCardService.grantToken(mobile, bizId);
  9. logger.info("请求参数[爱奇艺兑换卡] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
  10. logger.info("测试结果[爱奇艺兑换卡]:success");
  11. }
  12. private String queryUserMobile(String uId) {
  13. return "15200101232";
  14. }
  15. }

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

2.3 创建商店工厂

  1. public class StoreFactory {
  2. public ICommodity getCommodityService(Integer commodityType) {
  3. if (null == commodityType) {
  4. return null;
  5. }
  6. if (1 == commodityType) {
  7. return new CouponCommodityService();
  8. }
  9. if (2 == commodityType) {
  10. return new GoodsCommodityService();
  11. }
  12. if (3 == commodityType) {
  13. return new CardCommodityService();
  14. }
  15. throw new RuntimeException("不存在的商品服务类型");
  16. }
  17. }

这里我们定义了一个商店的工厂类,在里面按照类型实现各种商品的服务。可以非常干净整洁的处理你的代码,后续新增的商品在这里扩展即可。如果你不喜欢 if 判断,也可以使用 switch 或者 map 配置结构,会让代码更加⼲干净。

3. 测试验证

编写测试类

  1. public class ApiTest {
  2. @Test
  3. public void test_commodity() throws Exception {
  4. StoreFactory storeFactory = new StoreFactory();
  5. // 1. 优惠券
  6. ICommodity commodityService_1 = storeFactory.getCommodityService(1);
  7. commodityService_1.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);
  8. // 2. 实物商品
  9. ICommodity commodityService_2 = storeFactory.getCommodityService(2);
  10. Map<String,String> extMap = new HashMap<String,String>();
  11. extMap.put("consigneeUserName", "谢飞机");
  12. extMap.put("consigneeUserPhone", "15200292123");
  13. extMap.put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
  14. commodityService_2.sendCommodity("10001","9820198721311","1023000020112221113",new HashMap<String, String>() {{
  15. put("consigneeUserName", "谢飞机");
  16. put("consigneeUserPhone", "15200292123");
  17. put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
  18. }});
  19. // 3. 第三方兑换卡(爱奇艺)
  20. ICommodity commodityService_3 = storeFactory.getCommodityService(3);
  21. commodityService_3.sendCommodity("10001","AQY1xjkUodl8LO975GdfrYUio",null,null);
  22. }
  23. }

结果

1631433950(1).png
运行结果正常,既满足了业务产品需求,也满足了自己对代码的追求。这样的代码部署上线运行,内⼼心不会恐慌,不会觉得半夜会有电话。
另外从运行测试结果上也可以看出来,在进行封装后可以非常清晰的看到一整套发放奖品服务的完整性,统一了入参、统一了结果。

六、总结

从上到下的优化来看,⼯厂方法模式并不复杂,甚至这样的开发结构在你有所理解后,会发现更加简单了。
那么这样的开发的好处知道后,也可以总结出来它的优点; 避免创建者与具体的产品逻辑耦合 、 满足单一职责,每一个业务逻辑实现都在所属自己的类中完成 、 满足开闭原则,无需更改使用调用方就可以在程序中引入新的产品类型 。但这样也会带来一些问题,比如有非常多的奖品类型,那么实现的子类会极速扩张。因此也需要使用其他的模式进行优化,这些在后续的设计模式中会逐步涉及到。
从案例例入手看设计模式往要比看理论学的更加容易,因为案例是缩短理论到上手的最佳方式,如果你已经有所收获,一定要去尝试实操谢谢关注。