一、前言
好看的代码千篇一律,恶心的程序升职加薪。
该说不说几乎是程序员就都知道或者了解设计模式,但大部分小伙伴写代码总是习惯于一把梭。无论多少业务逻辑就一个类几千行,这样的开发也可以归纳为三步;定义属性、创建方法、调用展示,Done!只不过开发一时爽,重构火葬场。
好的代码不只为了完成现有功能,也会考虑后续扩展。在结构设计上松耦合易读易扩展,在领域实现上高内聚不对外暴漏实现细节不被外部干扰。而这就有点像家里三居(MVC)室、四居(DDD)室的装修,你不会允许几十万的房子把走线水管裸漏在外面,也不会允许把马桶放到厨房,炉灶安装到卫生间。
谁发明了设计模式? 设计模式的概念最早是由 克里斯托佛·亚历山大 在其著作 《建筑模式语言》 中首次提出的。 本书介绍了城市设计的 “语言”,提供了253个描述城镇、邻里、住宅、花园、房间及西部构造的模式, 而此类 “语言” 的基本单元就是模式。后来,埃里希·伽玛、 约翰·弗利赛德斯、 拉尔夫·约翰逊 和 理查德·赫尔姆 这四位作者接受了模式的概念。 1994 年, 他们出版了 《设计模式: 可复用面向对象软件的基础》 一书, 将设计模式的概念应用到程序开发领域中。
其实有一部分人并没有仔细阅读过设计模式的相关书籍和资料,但依旧可以编写出优秀的代码。这主要是由于在经过众多项目的锤炼和对程序设计的不断追求,从而在多年编程历程上提炼出来的心得体会。而这份经验最终会与设计模式提到的内容几乎一致,同样会要求高内聚、低耦合、可扩展、可复用。你可能也遇到类似的经历,在学习一些框架的源码时,发现它里的某些设计和你在做开发时一样。
我怎么学不会设计模式? 钱也花了,书也买了。代码还是一坨一坨的!设计模式是由多年的经验提炼出来开发指导思想。就像我告诉你自行车怎么骑、汽车怎么开,但只要你没跑过几千公里,你能记住的只是理论,想上道依旧很慌!
所以,本设计模式专题系列开始,会带着你使用设计模式的思想去优化代码。从而学习设计模式的心得并融入给自己。当然这里还需要多加练习,一定是人车合一,才能站在设计模式的基础上构建出更加合理的代码。
二、工厂方法模式介绍
- 工厂方法模式,图片来自 refactoringguru.cn
工厂模式又称工厂方法模式,是一种创建型设计模式,其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
这种设计模式也是 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实现需求
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 还会继续增加。
五、工厂模式优化代码
- 定义发奖接口
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 {
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语句后面不写扩展,这里是为了更加干净的向你体现逻辑。在实际的业务编码中可以添加括号。
总结
- 从上到下的优化来看,工厂方法模式并不复杂,甚至这样的开发结构在你有所理解后,会发现更加简单了。
- 那么这样的开发的好处知道后,也可以总结出来它的优点;避免创建者与具体的产品逻辑耦合、满足单一职责,每一个业务逻辑实现都在所属自己的类中完成、满足开闭原则,无需更改使用调用方就可以在程序中引入新的产品类型。但这样也会带来一些问题,比如有非常多的奖品类型,那么实现的子类会极速扩张。因此也需要使用其他的模式进行优化,这些在后续的设计模式中会逐步涉及到。
- 从案例入手看设计模式往往要比看理论学的更加容易,因为案例是缩短理论到上手的最佳方式,如果你已经有所收获,一定要去尝试实操。

评论区
还没有人评论