vlambda博客
学习文章列表

一文分清Java开发中容易混淆的四大设计模式

作者 | 蔡柱梁


可能很多人认为设计模式只有面试时用到,这也不能算错吧。但是如果仅仅只是面试时背背八股文,在实际工作中遇到了应该使用,却不知道要用,那么你的代码能有多好也是自欺欺人的了。那么什么时候应该使用设计模式呢?


换个角度说吧,大家觉得设计模式是怎么出来的?其实就是大牛们写代码多了,觉得一些高度重复或相似的地方,可以做到更好的“高内聚”,“低耦合”。他们不断改进,然后对他们的这些设计思路进行总结,最后得到的就是我们现在的设计模式。本文就给大家介绍几个常用的设计模式。




PART 01

 工厂模式 




为什么先说工厂模式呢? 因为“工厂”这个概念在我们代码中到处都体现着,很多设计模式也离不开它,以它作为基础演进,所以先要学习它。 什么是“工厂”? 就像我们生活中的工厂是一样的,就是生产某些“物品”的地方。


在 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 { @Override public TransportTool createTool() { ... }}
public class BoatTransportToolFactory implements TransportToolFactory { @Override 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定制化的一系列相关对象,这就是所谓的“能创建一系列相关的对象”。




PART 02

模板方法模式




模板方法模式是一种行为设计模式,它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。


模板方法的核心在于抽象上行为性质一样,实际行为上有差别。


举个例子:


我们产品常常要收集各式各样的数据来分析用户行为。有时他们为了效率会给开发一堆电子文档(如 CSV、DOC、TXT等等,这些文档记录着类似的数据,但是数据结构肯定是不同的),让开发按照他们要求开发个系统功能可以导入,按他们的要求统计这些数据。


对于开发而言,代码是差不多的,都要导入文件,解析文件,逻辑计算后入库。偏偏我们导入文件后,解析文件代码不同,逻辑计算有时也会有差异,但是对于最后一步落库却大概率是一样的。对于这种类型的业务场景,我们可以定个类去规定好这些流程,上游调用时就是调用我这个类的子类,子类会根据自己的业务场景重写自己需要的流程节点的逻辑。




PART 03

 策略模式 




策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。

举例子说明:


我们接入一个审批流组件,我们自己后台也要留一份提审的记录(方便查询和回溯),现在我们希望我们做的这个功能通用性要强一些,也就是可以做到让其他功能想加入这个审批流程就加入,如:功能鉴权的授权,工作流配置等等。


那么一开始审批时,一定是只有提审数据,而我们的鉴权授权或者工作流配置肯定是没生成到对应表的,只有审批通过后才会真的授权或者生成配置。这时问题来了,当工作流组件回调我们,难道我们每加入一个就 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 { @Override public void invokeHandle() { ... }}public class Service2 implements Iservice { @Override public void invokeHandle() { ... }}......




PART 04

适配器模式




适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。

说到适配器,我想大家很快就想到了一个场景:


我们家庭的标准电压是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 是怎么做兼容的 @Overridepublic 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( ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) { // 这里先看下这个 listener 是不是 GenericApplicationListener 的子类 // 不是就转化成 GenericApplicationListener,这样以前 ApplicationListener 的子类就能被兼容了 GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ? (GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener)); return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));}




PART 05

    总结   




希望上面的几个设计模式的应用例子能给大家一点启发,能在自己工作中找到共同点去尝试应用。不过,也不要滥用设计模式,因为一些刚起步的公司,业务方向也还不稳定,很难去抽取共同的抽象部分又或者由于业务太简单了,造成了过度设计,这些都是不可取的。




作者介绍


蔡柱梁,51CTO社区编辑,从事Java后端开发8年,做过传统项目广电BOSS系统,后投身互联网电商,负责过订单,TMS,中间件等。