如何使用工厂模式让自己的代码更加优雅
文章目录
- 前言
- 一、工厂模式是什么?
- 二、模拟发奖多种商品
- 三、用一坨坨代码实现
- 1. 工程结构
- 2. if else实现需求
- 3. 测试验证
- 四、工厂模式优化代码
- 1. 工程结构
- 2. 代码实现
- 2.1 定义发奖接口
- 2.2 实现奖品发放接口
- 2.3 创建商店工厂
- 3. 测试验证
- 总结
前言
设计模式是由多年的经验提炼出来开发指导思想。就像我告诉你⾃行⻋怎么骑、汽⻋怎么开,但只要你没跑过几千公⾥,你能记住的只是理论,想上道依旧很慌!
所以,本文会带着你使用设计模式的思想去优化代码。从⽽学习设计模式的心得并融入给自己。当然这⾥还需要多加练习,⼀定是人车合⼀,才能站在设计模式的基础上构建出更加合理的代码。
一、工厂模式是什么?
工厂模式⼜称工厂方法模式,是⼀种创建型设计模式,其在父类中提供⼀个创建对象的方法, 允许⼦类决定实例化对象的类型。
这种设计模式也是 Java 开发中最常见的⼀种模式,它的主要意图是定义⼀个创建对象的接口,让其子类自己决定实例化哪⼀个工厂类,工厂模式使其创建过程延迟到⼦类进行。
简单说就是为了提供代码结构的扩展性,屏蔽每⼀个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可,同时,这也是去掉众多 ifelse 的方式。
当然这可能也有⼀些缺点,比如需要实现的类非常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中,逐步降低。
二、模拟发奖多种商品
为了可以让整个学习的案例更加贴近实际开发,这⾥模拟互联⽹中在营销场景下的业务。由于营销场景的复杂、多变、临时的特性,它所需要的设计需要更加深入,否则会经常⾯临各种紧急CRUD操作,从⽽让代码结构混乱不堪,难以维护。
在营销场景中经常会有某个用户做了⼀些操作;打卡、分享、留⾔、邀请注册等等,进行返利积分,最后通过积分在兑换商品,从⽽促活和拉新。
那么在这⾥我们模拟积分兑换中的发放多种类型商品,假如现在我们有如下三种类型的商品接口;
从以上接口来看有如下信息:
- 三个接口返回类型不同,有对象类型、布尔类型、还有⼀个空类型。
- 入参不同,发放优惠券需要仿重、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。
- 另外可能会随着后续的业务的发展,会新增其他种商品类型。因为你所有的开发需求都是随着业务 对市场的拓展⽽带来的。
三、用一坨坨代码实现
如果不考虑任何扩展性,只为了尽快满足需求,那么对这么几种奖励发放只需使用if else语句判断,调用不同的接口即可满足需求。可能这也是⼀些刚入门编程的小伙伴,常用的方式。接下来我们就先按照这样的方式来实现业务的需求。
1. 工程结构
- 工程结构上非常简单,⼀个入参对象 AwardReq 、⼀个出参对象 AwardRes ,以及⼀个接口类 PrizeController
2. 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;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 = newIQiYiCardService();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":"79109876
4902132","uId":"10001"}
模拟发放优惠券⼀张:10001,EGM1023938910232121323432,791098764902132
22:17:55.671 [main] INFO o.i.demo.design.PrizeController - 奖品发放完成
10001。
22:17:55.673 [main] INFO org.itstack.demo.test.ApiTest - 请求参
数:{"uId":"10001","bizId":"791098764902132","awardNumber":"EGM102393891023
2121323432","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","sk
u":"9820198721311","userName":"花花","userPhone":"15200101232"}
22:17:55.677 [main] INFO o.i.demo.design.PrizeController - 奖品发放完成
10001。
22:17:55.677 [main] INFO org.itstack.demo.test.ApiTest - 请求参
数:{"extMap":{"consigneeUserName":"谢⻜机","consigneeUserAddress":"吉林省.⻓
春市.双阳区.XX街道.檀溪苑⼩区.#18-
2109","consigneeUserPhone":"15200292123"},"uId":"10001","bizId":"102300002
0112221113","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"}
模拟发放爱奇艺会员卡⼀张:15200101232,AQY1xjkUodl8LO975GdfrYUio
22:17:55.678 [main] INFO o.i.demo.design.PrizeController - 奖品发放完成
10001。
22: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. 工程结构
接下来使用工厂方法模式来进行代码优化,也算是⼀次很⼩的重构。整理᯿构会你会发现代码结构清晰了、也具备了下次新增业务需求的扩展性。但在实际使用中还会对此进行完善,⽬前的只是抽离出最核⼼的部分体现到你⾯前,方便学习。
- ⾸先,从上⾯的工程结构中你是否⼀些感觉,比如;它看上去清晰了、这样分层可以更好扩展了、 似乎可以想象到每⼀个类做了什么。
- 如果还不能理解为什么这样修改,也没有关系。因为你是在通过这样的⽂章,来学习设计模式的魅力。并且再获取源码后,进行实际操作几次也就慢慢掌握了 工厂模式 的技巧。
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, StringbizId, 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 newRuntimeException(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, StringbizId, 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, StringbizId, 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","sk
u":"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
模拟发放爱奇艺会员卡⼀张:15200101232,null
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
- 运行结果正常,既满足了业务产品需求,也满足了自己对代码的追求。这样的代码部署上线运行,内心不会恐慌,不会觉得半夜会有电话。
- 另外从运行测试结果上也可以看出来,在进行封装后可以非常清晰的看到⼀整套发放奖品服务的完 整性,统⼀了入参、统⼀了结果。
总结
- 从上到下的优化来看,工厂方法模式并不复杂,甚至这样的开发结构在你有所理解后,会发现更加 简单了。
- 那么这样的开发的好处知道后,也可以总结出来它的优点; 避免创建者与具体的产品逻辑耦合 、
满足单⼀职责,每⼀个业务逻辑实现都在所属自己的类中完成 、 满足开闭原则,⽆需更改使用调用方就可以 在程序中引入新的产品类型 。但这样也会带来⼀些问题,比如有非常多的奖品类型,那么实现的⼦类会极速扩张。因此也需要使用其他的模式进行优化,这些在后续的设计模式中会逐步涉及到。 - 从案例入手看设计模式往往要比看理论学的更加容易,因为案例是缩短理论到上手的最佳方式,如 果你已经有所收获,⼀定要去尝试实操。
转载于公众号小傅哥——重学java设计模式
如何使用工厂模式让自己的代码更加优雅相关推荐
- java实现仿qq界面及功能、网路编程、实现抽象工厂模式、线程池代码与测试
java实现仿qq界面及功能 用Swing 代码在百度网盘:http://pan.baidu.com/s/1pJjxI4b 具体见https://www.iteye.com/topic/1137293 ...
- 23种设计模式-抽象工厂模式介绍加实战代码
1.描述 通俗一点来讲,抽象工厂模式就是在工厂方法模式的抽象工厂类中规范多个同类产品. 工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂,而抽 ...
- 【设计模式】抽象工厂模式 ( 简介 | 适用场景 | 优缺点 | 产品等级结构和产品族 | 代码示例 )
文章目录 一.抽象工厂模式简介 二.抽象工厂模式适用场景 三.抽象工厂模式优缺点 四.产品等级结构和产品族 五.抽象工厂模式代码示例 1.冰箱抽象类 2.美的冰箱实现类 3.格力冰箱实现类 4.空调抽 ...
- Python工厂模式封装各类Webhook群聊机器人代码片段
引言 企业存在给 特定群组 自动推送消息的需求,比如:监控报警推送.销售线索推送.运营内容推送等. 你可以在群聊中添加一个自定义机器人,通过服务端调用 webhook 地址,即可将外部系统的通知消息即 ...
- 设计模式之简单工厂模式(静态工厂方法)
模式定义: 从设计模式的类型上来说,简单工厂模式是属于创建型模式,又叫做静态工厂方法(Static Factory Method)模式,但不属于23种GOF设计模式之一. 简单工厂模式是由一个工厂对象 ...
- Java设计模式学习之工厂模式
简单工厂模式 我直接上代码代码里有很详细的注解 //简单工厂模式 是由一个工厂对象决定创建出哪种产品 class Factory1 {publicstatic void main(String[] a ...
- Java设计模式——工厂模式
前言 工厂模式是一种比较常见的创建模式.有时,可能你使用了工厂模式而你却还不知道你已经使用了它.至少我感觉下面说的第一种简单工厂模式,你是已经很熟悉了.并且是经常使用的.不信,就继续向下看. 版权说明 ...
- 创建型模式---工厂模式
从简单到复杂有简单工厂模式-->工厂方法模式-->抽象工厂模式 简单工厂模式(Simple Factory Pattern) 定义 又称为静态工厂方法(Static Factory Met ...
- 智能家居 (4) ——工厂模式火焰报警
目录 工厂模式火焰报警部分代码 contrlEquipments.h 文件(设备类) mainPro.c 文件(主函数) fireDetection.c(火焰传感器) buzzer.c 文件(蜂鸣器) ...
最新文章
- 经济学人:清华大学或将迅速成为全球科研领军者
- 简单易懂的多线程(通过实现Runnable接口实现多线程)
- eset14 杀毒清除无提示
- 写时拷贝(Copy On Write)方案详解
- helm search搜索charts命令
- 这些高校竟因名字太“坑爹”被误会为三本?盘点九所实力强劲但被名字耽误的大学...
- keepalived和heartbeat区别
- GWO(灰狼优化)算法
- 对于数据库视图的一些理解
- 如何导进开源库StickyListHeaders
- 常见的网络协议\端口号
- 鸟哥Linux学习笔记(从头学习Linux基础)
- 算法---LeetCode 113. 路径总和 II
- Oracle中的sys用户和system用户
- windows7计算机或资源管理器窗口,win7系统资源管理器(文件夹)总是在新窗口打开的解决方法...
- 什么是combo复用口,光电类型如何转换?
- Linux下,为应用程序添加桌面图标(ubuntu18.4)
- 等本等息,等额本息,等额本金,看懂再贷款,坑多!
- Android 3D 魔方游戏的设计与开发
- 体脂秤方案开发脂肪秤方案设计