一文分清Java开发中容易混淆的四大设计模式
作者 | 蔡柱梁
可能很多人认为设计模式只有面试时用到,这也不能算错吧。但是如果仅仅只是面试时背背八股文,在实际工作中遇到了应该使用,却不知道要用,那么你的代码能有多好也是自欺欺人的了。那么什么时候应该使用设计模式呢?
换个角度说吧,大家觉得设计模式是怎么出来的?其实就是大牛们写代码多了,觉得一些高度重复或相似的地方,可以做到更好的“高内聚”,“低耦合”。他们不断改进,然后对他们的这些设计思路进行总结,最后得到的就是我们现在的设计模式。本文就给大家介绍几个常用的设计模式。
工厂模式
在 Java 代码中,我们用各式各样的对象做各种事情,而这个过程中,我们往往是不关心创建过程的,仅仅关注它有那些方法可使用,提供了什么功能。这时,我们可以使用工厂模式进行解耦——创建的行为放在工厂里,而使用的人专注于使用工厂产生的工具。在下面的模板方法、策略模式、适配器模式中,都能看到工厂模式的身影。
我们所说的工厂模式一般有两种:
-
工厂方法 -
抽象工厂
(1)工厂方法
工厂方法模式是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
工厂方法的具体实现思路是:
制定一个创建对象 A 的接口工厂
这个工厂的实现类构建 A 的子类,如:A1、A2、A3……
通过这种方式实现对象和对象的创建的分离,可能觉得很鸡肋吧?下面通过场景对比说明它的好处。
用传统做法与使用了工厂方法的场景对比:
-
传统写代码 -
我需要用到某个类,比如 A1,我就 new A1 出来,然后进行业务操作。有一天产品告诉我这段逻辑需要增加一个 A2 的业务操作逻辑,我就得通过条件判断增加逻辑。可是 A1 和 A2 在业务抽象上是一致的,仅仅是实现细节不同(举个例子:好比运输,我用货车运输是运输,我用火车运输也是运输,也就是说运输是目的,我的实现方式可以多样化)。这时,通过 if/else 或 switch 来写就不符合开闭原则了。 -
用了工厂方法写代码 -
我代码上一开始就写着是运输工具,用这个运输工具运输(注意这里是抽象概念运输工具而已)。这样,我就可以根据业务计算得到的条件(如:公路/铁路/海运/空运)丢给工厂,工厂给我返回具体的运输工具就行(反正子类能强转成父类)。
使用了工厂方法后,我的业务代码不需要关注具体的运输工具是什么,然后再去看它怎么运输,后续产品加再多运输工具,transport()的这段代码都不会被干扰,符合了开闭原则。
伪代码如下:
public interface TransportToolFactory {
TransportTool createTool();
}
public class TruckTransportToolFactory implements TransportToolFactory {
public TransportTool createTool() {
...
}
}
public class BoatTransportToolFactory implements TransportToolFactory {
public TransportTool createTool() {
...
}
}
public class Transport {
private TransportToolFactory factory;
public Transport(int way) {
if (way == 0) {
factory = new TruckTransportToolFactory();
}
...
}
public void transport() {
TransportTool tool = factory.createTool();
// 继续业务处理
}
}
简单说下“简单工厂”,伪代码如下:
public void transport() {
int way = getWay();// 经过计算也好,前端传过来也好,反正得到了具体的运输方式
TransportTool tool = new TransportToolFactory(way).createTool();
// 继续业务处理
}
public TransportTool createTool() {
if (way == 0) {
// 货车
}
...
}
不过简单工厂的缺点很明显:
没有做到单一职责,从上面的例子不难看出,汽车、轮船、飞机、大炮都包了,如果业务足够复杂,这个工厂类真的是谁维护谁知道!
(2)抽象工厂
抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。
在我看来,JDBC 对抽象工厂模式的应用就十分经典。DB 有很多种,但是在不同的公司选择可能都不太一样,有些是 MySQL,有些是 Oracle,甚至有些是 SQL Sever 等等。但是对于我们开发而言,这些都是 DB,如果它们的连接,提交事务,回滚事务等细节都需要我们注意的话(不同 DB 的具体实现处理会有差异),这显然是很麻烦的,而且我们也不关心。我们要的只是使用 Connection 创建 Session,Session 开启事务等等。
如果有一个类可以将这一系列共性的行为都提取出来(如连接,事务处理等),我们只要使用这个抽象类和它提供的方法就好了。事实上,JDBC 也的确是这么做的,我们在配置好具体的数据库配置后,在代码上只要用接口 Factory 创建连接、会话,开启事务……
首先,连接是个对象,会话也是对象,事务也是,创建这些对象的方法都抽象到一个工厂里面,而这个工厂本身也只是一个接口定义,这就是所谓的抽象工厂;如果这时我使用的是MySQL,那么刚刚罗列的那些对象都是MySQL定制化的一系列相关对象,这就是所谓的“能创建一系列相关的对象”。
模板方法模式
模板方法模式是一种行为设计模式,它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
模板方法的核心在于抽象上行为性质一样,实际行为上有差别。
举个例子:
我们产品常常要收集各式各样的数据来分析用户行为。有时他们为了效率会给开发一堆电子文档(如 CSV、DOC、TXT等等,这些文档记录着类似的数据,但是数据结构肯定是不同的),让开发按照他们要求开发个系统功能可以导入,按他们的要求统计这些数据。
对于开发而言,代码是差不多的,都要导入文件,解析文件,逻辑计算后入库。偏偏我们导入文件后,解析文件代码不同,逻辑计算有时也会有差异,但是对于最后一步落库却大概率是一样的。对于这种类型的业务场景,我们可以定个类去规定好这些流程,上游调用时就是调用我这个类的子类,子类会根据自己的业务场景重写自己需要的流程节点的逻辑。
策略模式
举例子说明:
我们接入一个审批流组件,我们自己后台也要留一份提审的记录(方便查询和回溯),现在我们希望我们做的这个功能通用性要强一些,也就是可以做到让其他功能想加入这个审批流程就加入,如:功能鉴权的授权,工作流配置等等。
那么一开始审批时,一定是只有提审数据,而我们的鉴权授权或者工作流配置肯定是没生成到对应表的,只有审批通过后才会真的授权或者生成配置。这时问题来了,当工作流组件回调我们,难道我们每加入一个就 copy 上一个功能的回调代码,删掉修改审批状态后的代码,改改就好了吗?这里得冗余多少代码,哪怕你修改审批流的代码抽取成一个方法,你也会发现每个回调方法里都有你那个方法。
具体伪代码如下:
public class CallBack {
public void callback1(Param param) {
// 查询审批记录的合法性
// 修改审批记录
// 处理业务逻辑1
}
public void callback1(Param param) {
// 查询审批记录的合法性
// 修改审批记录
// 处理业务逻辑2
}
......
}
这种场景我们可以使用策略模式优化,我们将处理业务逻辑当成个算法对象抽离出来,不同业务场景的回调业务处理器实现这个抽离接口,用策略自动分配对应的处理器即可。
伪代码如下:
public class CallBack {
private Strategy strategy = new Strategy();
public void callback(Param param) {
// 查询审批记录的合法性
// 修改审批记录
// 处理业务逻辑
strategy.getHandle(param.getServiceName()).invokeHandle();
}
}
public class Strategy {
private Map<String, Iservice> map;
static {
map = new HashMap<String, Iservice>();
map.put("Service1", new Service1());
map.put("Service2", new Service2());
......
}
public Iservice getHandle(String serviceName) {
return map.get(serviceName);
}
}
public class Service1 implements Iservice {
public void invokeHandle() {
...
}
}
public class Service2 implements Iservice {
public void invokeHandle() {
...
}
}
......
适配器模式
适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。
说到适配器,我想大家很快就想到了一个场景:
我们家庭的标准电压是220V左右(实际会有点误差),我们大家电自然需要这么高的电压才能工作,但是我们小家电呢?如:手机充电,电脑等等。这些小家电往往都会有个“中介”——适配器去帮他们将标准电压转化成他们的适用电压。
其实我们的适配模式也是一样的。这里我们来看下 Spring 的实战使用,上源码:
/**
* Extended variant of the standard {@link ApplicationListener} interface,
* exposing further metadata such as the supported event and source type.
*
* <p>As of Spring Framework 4.2, this interface supersedes the Class-based
* {@link SmartApplicationListener} with full handling of generic event types.
*
* @author Stephane Nicoll
* @since 4.2
* @see SmartApplicationListener
* @see GenericApplicationListenerAdapter
*/
public interface GenericApplicationListener extends ApplicationListener<ApplicationEvent>, Ordered {
...
在 4.2 版本之前,Spring 监听触发事件的监听器使用的是 ApplicationListener,经过这么多迭代后,它想增强下该功能,所以又定义了一个 GenericApplicationListener。但是这里有个问题,以前实现 ApplicationListener 的那些子类也还是要兼容的!!!全部重写,那很累人;不兼容,作为高热度的开源框架,这是不能接受的。这时,Spring 的作者就采用了适配模式,具体应用代码如下:
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
// 我们都知道 spring 的广播事件都是是用了这个接口,我们看下 spring 是怎么做兼容的
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
// 重点在这 getApplicationListeners(event, type),看看他们是怎么 get 这个 list 的
// getApplicationListeners 是父类 AbstractApplicationEventMulticaster 的方法
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
}
AbstractApplicationEventMulticaster#getApplicationListeners 里面做了大量的性能优化,不是本文的重点,所以这里跳过了。大家只要知道它第一次拿的地方是:
AbstractApplicationEventMulticaster#retrieveApplicationListeners 。
这就够了,而这个方法里面给 list 添加元素的方法是:
AbstractApplicationEventMulticaster#supportsEvent(ApplicationListener<?>, ResolvableType, Class<?>),这才是我们要看的代码。
protected boolean supportsEvent(
ApplicationListenerClass<?> sourceType) { > listener, ResolvableType eventType, @Nullable
// 这里先看下这个 listener 是不是 GenericApplicationListener 的子类
// 不是就转化成 GenericApplicationListener,这样以前 ApplicationListener 的子类就能被兼容了
GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}
总结
希望上面的几个设计模式的应用例子能给大家一点启发,能在自己工作中找到共同点去尝试应用。不过,也不要滥用设计模式,因为一些刚起步的公司,业务方向也还不稳定,很难去抽取共同的抽象部分又或者由于业务太简单了,造成了过度设计,这些都是不可取的。
作者介绍
蔡柱梁,51CTO社区编辑,从事Java后端开发8年,做过传统项目广电BOSS系统,后投身互联网电商,负责过订单,TMS,中间件等。