@[toc]
创建型模式
创建型模式提供创建对象的机制, 能够提升已有代码的灵活性和可复⽤性。
| 类型| 实现要点 |
|--|--|
| 工厂方法 | 定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。 |
| 抽象工厂| 提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定它们具体的类。 |
|建造者 |将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示 |
| 原型 | ⽤原型实例指定创建对象的种类,并且通过拷⻉这些原型创建新的对象。 |
| 单例| 保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。 |
概述
优秀的代码在结构设计上松耦合易读易扩展,在领域实现上⾼内聚不对外暴漏实现细节不被外部⼲扰。
⼯⼚模式⼜称⼯⼚⽅法模式,是⼀种创建型设计模式,其在⽗类中提供⼀个创建对象的⽅法, 允许⼦类决定实例化对象的类型。
它的主要意图是定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。
优点: 简单说就是为了提供代码结构的扩展性,屏蔽每⼀个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调⽤即可,同时,这也是去掉众多 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 - 奖品发放完成10001。
14: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 - 奖品发放完成10001。
14: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"}
模拟发放爱奇艺会员卡一张:15200101232,AQY1xjkUodl8LO975GdfrYUio
14:16:29.960 [main] INFO com.artisan.PrizeController - 奖品发放完成10001。
14: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 ICommodity getCommodityService(Class<? extends ICommodity> clazz) throws IllegalAccessException, InstantiationException {
if (null == clazz) return null;
return clazz.newInstance();
}
}
这⾥我们定义了⼀个商店的⼯⼚类,在⾥⾯按照类型实现各种商品的服务,后续新增的商品在这⾥扩展即可。
提供了两种获取工厂实现类的方法: 一种是根据商品类型,另外一种是根据奖品类信息进行催熟啊,
如果不喜欢 if 判断,也可以使⽤ switch 或者 map (key是类型值,value是具体的实现逻辑) 配置结构,会让代码更加⼲净。
单元测试
@Test
public void test_StoreFactory_01() 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);
commodityService_2.sendCommodity("10001", "9820198721311", "1023000020112221113", new HashMap<String, String>() {{
put("consigneeUserName", "谢飞机");
put("consigneeUserPhone", "15200292123");
put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
}});
// 3. 第三方兑换卡(模拟爱奇艺)
ICommodity commodityService_3 = storeFactory.getCommodityService(3);
commodityService_3.sendCommodity("10001", "AQY1xjkUodl8LO975GdfrYUio", null, null);
}
@Test
public void test_StoreFactory_02() throws Exception {
StoreFactory storeFactory = new StoreFactory();
// 1. 优惠券
ICommodity commodityService = storeFactory.getCommodityService(CouponCommodityService.class);
commodityService.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);
}
可以看到在进⾏封装后可以⾮常清晰的看到⼀整套发放奖品服务的完整性,统⼀了⼊参、统⼀了结果。
小结
- ⼯⼚⽅法模式并不复杂,可以使开发结构更加简单。
- 避免创建者与具体的产品逻辑耦合 、 满⾜单⼀职责,每⼀个业务逻辑实现都在所属⾃⼰的类中完成 、 满⾜开闭原则,⽆需更改使⽤调⽤⽅就可以在程序中引⼊新的产品类型 。
- 也会带来⼀些问题,⽐如有⾮常多的奖品类型,那么实现的⼦类会极速扩张。因此也需要使⽤其他的模式进⾏优化.