一文分清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 是怎么做兼容的
   @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(
     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,中间件等。

图片

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值