前言絮叨
以前经常去读一些有关“设计模式”的书籍或者博客,但是后面逐渐会发现总是会存在这样或那样的问题:
1、当时可能通过作者对设计模式场景剖析的如数家珍和理论知识理解的真知灼见,粗浅地明白了这个设计模式是怎么一回事,但过一段时间就基本上已经忘得把它七七八八了(没错,就是所谓的:一听就会,一看就懂,一做就错);
2、设计模式较少地应用到实际项目里,没有深入思考如何结合实际业务场景运用,这也是导致遗忘速度快的原因之一,还是那句话:纸上得来终觉,绝知此事要躬行;
3、如果没有较好地掌握好所用的设计模式“灵魂”,那么在阅读优秀开源框架组件的时候,就会很难明白这行代码的coder为什么要这么写?心里就会产生千万个问号,这里这么写不是很复杂么?为啥不能简化点非要绕这么大一个圈子?归根到底,一言以蔽之:coder通过精湛的设计模式组合运用来保证了整个框架高度的可扩展性、可重用性和代码健壮性。
所以,为了逃避前人的“代码屎山”和“屎山雕花”,作者开始逐渐尝试在实际项目中合理使用设计模式去解决一些目前不存在但是未来可能会发生的一些问题,这里先简单介绍一种大伙应该都能在自己项目里能够用上的设计模式组合。
不过这里还是要打打预防针:切记勿过度使用设计模式、勿过度使用设计模式、勿过度使用设计模式,否则只会事倍功半,代码也会看起来晦涩难懂。
这里没有一个明确的标准去界定设计模式用的是否恰到好处,作者在项目里会遵循一个自以为正确的准则:在适合的业务场景下用合理的设计模式做恰当的包装。
好了,啰里啰唆这么久,下面回到正题。
场景举例
还是拿电商的场景来举例比较好理解。
需求是:同一个商品从N个第三方平台进入本商城后,会对此商品做M种不同的促销活动后,最后算出一个成交价推送到风控系统。
从这个需求我们可以剖析出我们实现的接口需要满足以下要求:
1、不同平台推送过来的商品数据模型不一样,我们需要根据不同的平台类型去特定解析出对应的商品模型,再转成我们商城统一的数据模型;
2、获取到统一的商品数据模型后,需要经过多个不同的促销活动,每种促销活动的处理逻辑和促销活动的先后组合顺序业务需要经常变动;
3、推送风控系统除了成交价外,还需要根据不同平台推送不同的指定参数。
硬编码
如果是刚踏入职场的“愣头青”或者饱经风霜的“老油条”来按部就班,也许会有以下“硬编码”:
public void handle(String product, String platformCode) {
// 解析第三方平台后转化为本商城统一的商品数据结构
StandardProduct standardProduct = null;
if ("pdd".equals(platformCode)) {
standardProduct = parse4Pdd(product);
} else if ("tMall".equals(platformCode)) {
standardProduct = parse4TMall(product);
} else if ("jd".equals(platformCode)) {
standardProduct = parse4Jd(product);
} else {
throw new RuntimeException("platformCode invalid");
}
// 进行各种促销活动
mjActivity(standardProduct);
msActivity(standardProduct);
yhqActivity(standardProduct);
// 推送风控系统
PushRiskSystemParam pushRiskSystemParam = null;
if ("pdd".equals(platformCode)) {
// 为拼多多组装推送风控系统参数
pushRiskSystemParam = pushRiskSystemParam4Pdd(product, standardProduct);
} else if ("tMall".equals(platformCode)) {
// 为天猫组装推送风控系统参数
pushRiskSystemParam = pushRiskSystemParam4TMall(product, standardProduct);
} else if ("jd".equals(platformCode)) {
// 为京东组装推送风控系统参数
pushRiskSystemParam = pushRiskSystemParam4Jd(product, standardProduct);
} else {
throw new RuntimeException("platformCode invalid");
}
// 组装推送分控系统通用参数
pushRiskSystemParam4Common(standardProduct);
// 调用风控系统
invokeRiskSystem(standardProduct);
}
从功能逻辑实现上看,没有一点毛病,按部就班把流程图里的节点一 一实现了。
但是从代码设计和功能的扩展性来说必然存在以下问题:
1、相信业务后面大概率会增加第三方平台的,比如百度,腾讯和抖音商城等,那么解析逻辑还是不断叠加else -if,虽然是方便了开发者,但是else if一旦超过3个后,可读性看来是比较难受的(相信强迫症患者或者代码洁癖更甚,作者就是);
2、促销活动一定是按照上述代码的顺序?满减-秒杀-优惠券,能不能换成秒杀-优惠券-满减?当然可以,并且要随时支持业务动态调整;
3、接口整体的结构变动必然是较少的,变动的都是节点里的细节,那么能不能抽象出一个骨架结构来控制接口的核心主流程?使得结构逻辑清晰明了,让其他开发者一眼就知道接口的整体大概逻辑,因为很多时候我们去梳理逻辑的适合不必去看里面的细节实现。
那么优雅地优化上述问题呢,可以参考下面作者较为常用的解决方案之一。
基本介绍
开始前,我们先对一些基本概念作基介绍,主要是结合了自定义注解+策略模式+模板方法模式。
自定义注解
这个只要使用JDK提供的关键字和注解即可,例如在我们上述场景用到的:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface PlatformHandler {
PlatformEnum value();
}
1、首先使用的java关键字 @interface来定义当前类为注解;
2、然后需要明确要使用的自定义注解想放在哪个地方( @Target),比如我们这次是需要放在类(ElementType.TYPE)上,那么就在自定义注解上这么定义:@Target(ElementType.TYPE);
3、接着需要说明自定义注解在哪个生命周期生效( @Retention),这里当然是JVM运行时:@Retention(RetentionPolicy.RUNTIME);
4、最后就是@Documented,这里加了会把当前类的信息加载到javadoc生成的文档中。
可能你还会看到@Component这个注解,这里是把spring的组件标识加上去了,这样当我们使用PlatformHandler这个注解的时候,就默认为spring的组件了。
从上面看来,自定义注解里最最核心的就是 @Target和@Retention这两个定义了,这里把涉及到的明细选项也列一下:
- @Target:用于定义注解用在哪些作用域上
| 枚举 | 说明 |
|---|---|
| ElementType.ANNOTATION_TYPE | 注解 |
| ElementType.CONSTRUCTOR | 构造函数 |
| ElementType.FIELD | 字段或属性 |
| ElementType.LOCAL_VARIABLE | 局部变量 |
| ElementType.METHOD | 方法 |
| ElementType.PACKAGE | 包声明 |
| ElementType.PARAMETER | 方法的参数 |
| ElementType.TYPE | 接口、类、枚举 |
| lementType.TYPE_PARAMETER | 类型参数声明 (JDK1.8开始) |
| ElementType.TYPE_USE | 任何类型名称声明 (JDK1.8开始) |
- @Retention:用于定义注解的生命周期
| 枚举 | 说明 |
|---|---|
| RetentionPolicy.SOURCE | 只保留在源码中,会被编译器忽略 |
| RetentionPolicy.CLASS | 【默认】保留到编译的calss文件中,但会被Java虚拟机(JVM)无视 |
| RetentionPolicy.RUNTIME | 保留到JVM阶段,以便运行时环境可以通过反射获取它。 一般我们自定义的就是这个 |
策略模式(Stategy Pattern)
定义
策略模式属于行为设计模式,允许你在运行时改变对象的行为。
它定义了一系列的算法,并将每一个算法封装起来,使它们可以互相替换。
策略模式使得算法可以独立于使用它的客户端变化。
适用场景
这种模式特别适用于那些有许多条件语句(如if-else语句)的代码,通过将这些条件语句替换为策略模式,可以使代码更加清晰、可维护,并且更易于扩展。
组成
1、上下文(Context) :维护一个对抽象策略类的引用,并使用这个引用调用具体策略类的方法;
2、抽象策略类(Strategy) :声明所有支持的算法的接口,通常是一个抽象类或接口;
3、具体策略类(Concrete Strategy) :实现了抽象策略类中声明的接口或继承了抽象策略类,表示具体的算法。
优点
1、开闭原则:允许在不修改原有系统的情况下增加新的行为或算法;
2、灵活性:可以很容易地切换不同的算法,而不需要修改代码;
3、可扩展性:易于添加新的策略,只需创建新的具体策略类即可。
缺点
随着具体业务的不断扩展,会新增大量的策略类,增加系统的复杂性。
模板方法模式(Template Method Pattern)
定义
模板方法模式是一种行为型设计模式,在Java中常用于定义一个操作中的算法骨架,而将一些步骤延迟到子类中实现。
适用场景
在逻辑的主流程相对稳定可控但是细节经常变化且复杂的时候,通过定义好核心逻辑,让代码更加清晰且可控,且避免重复代码的出现。
组成
1、抽象类(Abstract Class) :定义了模板方法和一些抽象的基本方法。模板方法中包含了算法的骨架,调用了一系列的基本方法。
2、具体子类(Concrete Subclass) :继承自抽象类,实现抽象类中的抽象基本方法。
优点
1、代码复用:将算法的稳定部分集中在一个模板方法中,子类只需要实现变化的部分。
2、扩展性:可以灵活地添加新的子类来扩展功能。
缺点
1、增加复杂性:使用模板方法模式可能会使类层次结构变得更加复杂。每个具体的实现都需要一个子类,这可能导致类的数量增加,从而增加了系统的复杂性和维护难度;
2、减少灵活性:一旦定义了模板方法和其中的抽象操作,它们就很难被修改,因为这可能会影响到所有继承该模板方法的子类。这意味着如果需要改变算法的基本结构,可能需要对多个类进行修改;
3、强制依赖:子类必须依赖于基类中定义的方法和属性,这可能会限制子类的灵活性。如果基类中的方法签名或行为发生变化,所有子类都可能受到影响;
4、过度抽象:有时候,过多地使用抽象方法和模板方法可能导致代码难以理解,特别是对于新加入团队的开发人员来说。
优雅转身
现在我们来看看如何结合自定注解+策略模式+模板方法模式来优雅解决场景里提出的问题。
由于我们的接口需求的逻辑骨架是确定稳定且后续变动较小的,只是里面的各个业务节点的实现细节有差别,这样我们就
模板方法模式实现主逻辑
可以根据主流程来定义逻辑骨架:
1 解析第三方平台后转化为本商城统一的商品数据结构(子类实现)
2 促销活动查询(本抽象类里查询),具体促销活动处理(子类实现)
3 推送风控系统
3.1 组装推送风控系统通用参数(本类实现)
3.2 组装推送风控系统平台参数(子类实现)
3.3 调用风控系统(本类实现)
有了上面的逻辑后,结合上述模板方法设计模式,我们需要定义抽线类,里面顶一个顶层方法,然后把上述逻辑完成:
public abstract class ProductService {
public final void handle(String product, String platformCode) {
// 1 解析第三方平台后转化为本商城统一的商品数据结构(子类实现)
StandardProduct standardProduct = parseAndConvert(product, platformCode);
// 2 促销活动查询(本抽象类里查询)
List<Activity> activities = queryActivitySortFromDB();
for (Activity activity : activities) {
// 具体促销活动处理(子类实现)
handle4Activities(standardProduct, activity, platformCode);
}
// 3 推送风控系统
// 3.1 组装推送风控系统通用参数(本类实现)
assembleCommonParam(standardProduct);
// 3.2 组装推送风控系统平台参数(子类实现)
handlePlatformParam(platformCode, standardProduct);
// 3.3 调用风控系统(本类实现)
pushToRiskCtrlSystem(standardProduct);
}
protected abstract void handle4Activities(StandardProduct standardProduct, Activity activity, String platformCode);
protected abstract void handlePlatformParam(String platformCode, StandardProduct standardProduct);
public abstract StandardProduct parseAndConvert(String product, String platformCode);
public void pushToRiskCtrlSystem(StandardProduct standardProduct) {
// 调用风控系统接口...
}
protected void assembleCommonParam(StandardProduct standardProduct) {
// 组装推送风控系统参数逻辑...
}
public List<Activity> queryActivitySortFromDB() {
// 从数据库里查询配置的活动列表...
return new ArrayList<>();
}
}
毫无疑问,ProductService#handle这个方法是处理商品逻辑的主方法,里面部分逻辑是需要当前类自己来实现的,也有需要延迟到子类实现的,这样我们看这个代码核心逻辑就非常清晰了。
这里的主方法用了final修饰,防止子类重写。
策略模式实现动态获取实现类
上面我们定义好基类好先不着急子类的继承实现,可以先用策略模式将不同平台对不同功能实现的具体逻辑先定义好。首先自定义好各个平台及自定义注解:
public enum PlatformEnum {
PDD("pdd", "拼多多"),
T_MALL("tMall", "天猫"),
JD("jd", "京东"), ;
private String code;
private String name;
PlatformEnum(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public PlatformEnum setCode(String code) {
this.code = code;
return this;
}
public String getName() {
return name;
}
public PlatformEnum setName(String name) {
this.name = name;
return this;
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface PlatformHandler {
PlatformEnum value();
}
然后结合上述枚举定义好需要的抽象策略类和具体策略类:
public interface ThirdPlatformService {
StandardProduct parseAndConvert(String product);
void handleActivity(StandardProduct standardProduct, Activity activity);
void handlePlatformParam(StandardProduct standardProduct);
}
@PlatformHandler(value = PlatformEnum.JD)
public class JdService implements ThirdPlatformService {
@Override
public StandardProduct parseAndConvert(String product) {
return null;
}
@Override
public void handleActivity(StandardProduct standardProduct, Activity activity) {
}
@Override
public void handlePlatformParam(StandardProduct standardProduct) {
}
}
@PlatformHandler(value = PlatformEnum.PDD)
public class PddService implements ThirdPlatformService {
@Override
public StandardProduct parseAndConvert(String product) {
return null;
}
@Override
public void handleActivity(StandardProduct standardProduct, Activity activity) {
}
@Override
public void handlePlatformParam(StandardProduct standardProduct) {
}
}
@PlatformHandler(value = PlatformEnum.T_MALL)
public class TMallService implements ThirdPlatformService {
@Override
public StandardProduct parseAndConvert(String product) {
return null;
}
@Override
public void handleActivity(StandardProduct standardProduct, Activity activity) {
}
@Override
public void handlePlatformParam(StandardProduct standardProduct) {
}
}
这样我们就可以实现上下文了:
@Service
public class ProductHandleContext {
private final Map<String, ThirdPlatformService> handlerMap = new HashMap<>();
public ProductHandleContext(Map<String, ThirdPlatformService> map) {
map.forEach((k, v) -> {
PlatformHandler annotation = v.getClass().getAnnotation(PlatformHandler.class);
this.handlerMap.put(annotation.value().getCode(), v);
});
}
public ThirdPlatformService getHandler(String code) {
return handlerMap.get(code);
}
}
这里的构造方法参数会在spring启动的生命周期流程中将对应的bean注入,所以我们可以利用这个特性将handlerMap填充好,然后就可以通过定义getHandler方法来动态获取平台处理器。
继承抽象类
经过上面的一顿操作后,还记得我们一开始定义的抽象类吗?让我们继承它来实现它的抽象方法:
@Component
public class ProductServiceHandler extends ProductService {
@Resource
private ProductHandleContext productHandleContext;
@Override
protected void handle4Activities(StandardProduct standardProduct, Activity activity, String platformCode) {
ThirdPlatformService handler = productHandleContext.getHandler(platformCode);
handler.handleActivity(standardProduct, activity);
}
@Override
protected void handlePlatformParam(String platformCode, StandardProduct standardProduct) {
ThirdPlatformService handler = productHandleContext.getHandler(platformCode);
handler.handlePlatformParam(standardProduct);
}
@Override
public StandardProduct parseAndConvert(String product, String platformCode) {
ThirdPlatformService handler = productHandleContext.getHandler(platformCode);
StandardProduct standardProduct = handler.parseAndConvert(product);
return standardProduct;
}
}
看到没,这里几个方法的实现都通过了策略上下文去动态通过平台code来完成不同平台的操作,以后有新的平台加入,几乎可以在不动这些主流程源码的前提下完成。
控制层代码
现在来看看控制层只需要一行代码解决:
public class ProductController {
@Resource
private ProductServiceHandler productServiceHandler;
@PostMapping("/handleProduct")
public void handleProduct(@RequestParam String product, @RequestParam String platformCode) {
productServiceHandler.handle(product, platformCode);
}
}
总结
上面这种类似的场景用的这种通用解决方案是作者在经过多次实践真实在工作中常用到的,当然里面肯定还有很多可以继续挖掘的点和需要优化的点,欢迎指出~