鸟瞰设计模式

1,019 阅读13分钟

写完后倒回来看一下这篇文章,感觉有点乱糟糟的,权当自己的笔记了。本文未完待续。

个人认为,所谓的设计模式,就是前人总结的面向接口编程中的最佳实践,为什么是接口呢?因为设计模式中一般涉及三个类:

  • 客户端
  • 接口
  • 接口实现类

客户端(一般指业务代码)持有接口,设计模式就是对接口进行实现。本文不讲UML类图,因为代码实践中的设计模式跟文献中学院派的设计模式有些是有点不同的,学院派理论很完美但复杂,代码中如果写太复杂的可能后期维护成本大。本文主要说明Java中23种设计模式在工作中的应用,同时结合Spring框架或JDK源码等常用的代码,说说设计模式在Java中的例子。

总结归纳

Java设计模式主要分为三种类型:建造者模式、结构性模式、行为型模式。
刚好对应于对象的生命周期:实例化、实例之间的关系、实例的使用。
Spring框架的设计也符合这个顺序:IoC(建造者)、AOP(结构型)、其他如Data Access(行为型)。

20191105085652.png
如上图,我已经把常用设计模式背景色改为黑色了。Java设计模式一共23种,上图中标红的简单工厂模式不是GoF总结出来的23种设计模式之一。一句话总结如下:

  • 建造型:
    • 工厂模式:根据传入参数实例化对象,如BeanFactory.getBean("helloBean");
    • 单例模式:构造方法私有化,提供静态方法获取实例。如Singleton.getInstance();
    • 创建者模式:根据属性命令方法,该方法返回一个本身Builer对象,build()方法建造一个实例,如HelloBean helloBean = new HelloBeanBuilder().name("donggua").age(26).build()
  • 结构型:
    • 代理模式:代理对象中持有被代理对象的实例,增强接口的抽象方法。如:HelloIntf helloIntf = new HelloIntfProxy();
    • 装饰模式:装饰对象中通过参数传入持有被装饰对象的实例,增强接口的抽象方法。如HelloIntf helloIntf = new HelloIntfDecorator(new HelloIntfImpl());
    • 适配器模式:连接两个不同的接口,实现左边接口,持有右边接口对象。如:class USBAdapter implements USBSerialPort; class TFCard; USBAdapter{ void readSerial(tfCard.readTF()) };
  • 行为型:
    • 模板方法模式:抽象类中定义好方法顺序,子类继承后顺序不变。如:class HelloTemplate { void display(){ preHello(), hello(); postHello();} };
    • 策略模式:抽象类定义好策略,子类实现,客户端选择具体子类,如:class PayPolicy{ pay(); }; class AliPayPolicy { pay(){}; }; PayPolicy pp = new AliPayPolicy()
    • 观察者模式:被观察对象持有观察对象的列表,事件触发时遍历通知。如:class Observable{ List<Observer> observerList; };
    • 责任链模式:抽象类中定义级别和本抽象类的引用,事件触发时,级别大于本节点级别的不处理。如:class Chain{ int level; Chain nextNode; do(level){ if(level < this.level) dosomething(); nextNode.do(); }; };

建造型

创建型模式,就是创建对象的模式,抽象了实例化的过程。主要是为了规范化new实例的过程。比如,工厂模式可以对对象进行统一管理,修改一个配置就可以实例化出单例的对象或者多例对象。Spring最基本的IoC容器就是工厂模式。

工厂模式

工厂模式一般类名以Factory结尾,工厂模式三剑客:简单工厂模式、工厂方法模式、抽象工厂模式。

简单工厂模式(常用)

简单工厂模式又叫静态工厂方法模式,工厂类提供一个静态方法,对不同的入参返回不同的具体实现类。如slf4j + logback的实现。其中。

private Logger logger = LoggerFactory.getLogger(HelloTest.class); 
  • 客户端:业务代码
  • 接口:Logger
  • 接口实现:定义一个LoggerFactory工厂,在logback中返回具体的实例

工厂方法模式

连载中。。。

抽象工厂模式

连载中。。。

单例模式(常用)

单例模式是 Java 中最简单的设计模式之一。其涉及到一个单一的类,该类负责创建自己的对象(一般使用getInstance()方法),同时确保只有单个对象被创建。单例有懒汉、饿汉、双重检查锁,静态内部类等多种实现方式。JDK和Spring源码中暂未找到比较常用的实例。双重检查锁代码如下:

package me.zebin.demo.javaio;

public class Singleton {

    // 这里的变量务必使用volatile关键字,否则会有线程安全问题
    public static volatile Singleton singleton = null;
    // 构造方法私有化,禁止直接new
    private Singleton(){

    }
    // 获取唯一实例的方法
    public static Singleton getInstance(){
        if (null != singleton) {
            return singleton;
        }
        // 多个线程在此等待
        synchronized (Singleton.class) {
            // 只有第一个线程执行了new代码,第二个线程条件不成立
            if (null == singleton) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

创建者模式(常用)

创建者模式(建造者模式)将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。说人话,建造者和工厂模式不同之处在于,工厂模式只需要说明我要哪个对象,工厂就能帮你造,而创建者则需要提供原材料。创建者一个典型的建造过程就是链式建造,最后调用build()方法触发。JDK源码中的例子就是StringBuilder类。

StringBuilder sb = new StringBuilder("Hello").append("world").sppend("!").toString();

普通的JavaBean也可以很容易实现建造者模式,使用lombok插件,在Bean上加@Builder就可给你创建一个建造者模式的类。然后你使用建造者的链式写法来创建一个实例,如:

Person person = new Person().age(26).name("dongua").city("guangzhou").build();

lombok是提高编码效率的很强大的插件,不了解的可以搜索一下。

原型模式

连载中。。。

结构型

结构型模式是为解决怎样组装现有的类,设计它们的交互方式,从而达到实现一定的功能目的。结构型模式包容了对很多问题的解决。例如,Spring的AOP就是解决类间的结构融合,使用的时结构型的代理模式。

代理模式

代理模式是对象的结构模式。代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。但是JDK和Spring源码中大多使用动态代理,虽然静态代理实现比较简单,但是在实际项目中我们需要为每个类都写一个代理类,需要写很多重复冗余的代码,不利于代码的解耦与扩展。为一个类做代理主要是为了增强该类的功能,我们这里只讨论静态代理,JDK和Spring中没找到合适的静态代理的例子,这里手动写一个吧。

如下,定义一个Car接口,只有一个drive()方法,所有品牌的车都继承自该接口,普通的奔驰车实现了Car接口的drive()方法,通过定义奔驰车的代理,增强车的功能。为了方便,我写在了一个类里。

package me.zebin.demo.javaio;

import org.junit.Test;

public class ProxyTest {

    @Test
    public void client() {
        // 面向接口编程,持有一个Car接口
        Car car = new BenzCarProxy();
        car.drive();
    }

    // Car接口,Car能drive
    interface Car{
        void drive();
    }

    // 普通的奔驰车,能开
    class BenzCar implements Car{

        @Override
        public void drive() {
            System.out.println("我是奔驰车,我在开车呢老铁...");
        }
    }

    // 代理,增强型的奔驰
    class BenzCarProxy implements Car{

        // 持有普通奔驰引用
        BenzCar benzCar = new BenzCar();
        @Override
        public void drive() {
            System.out.println("普通车进化,超级车,能飞了兄dei...");
            benzCar.drive();
        }
    }

}

  • 客户端:业务代码client
  • 接口:Car接口
  • 接口实现:BenzCar,BenzCarProxy。

装饰模式

装饰模式允许向一个现有的对象添加新的功能,同时又不改变其结构。所以,装饰模式是增强类的功能的,这点和代理模式很像。相同之处是,代理类或装饰类都持有源类的引用,而不同点是,代理类在内部直接new出一个被代理对象,装饰类在初始化时需要传入被代理类作为参数。所以装饰模式才是在源类本身上增加功能,源类变强了。而代理模式只是在代理类在源类上做一些操作,实际上源类并没有增强。但整个代理类看起来是增强了。
Java中的源码比如io流就大量使用装饰模式,如:

FileInputStream  fis = new FileInputStream(inputStream);

其中FileInputStream增强了InputStream的功能。

适配器模式

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。例如,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。代码如下:

package me.zebin.demo.javaio;

import org.junit.Test;

public class AdapterTest {

    // USB计算机串口,可以读USB设备
    interface USBSerialPort{
        void readUSB();
    }

    // 手机的tf卡
    class TFCard{

        // TFCard可以被读取
        public void readTFCard(){
            System.out.println("我是TFCard,数据正在输出,被读取中...");
        }
    }

    // TFCard适配器是一种USB串口设备,所以实现了该接口,USBSerialPort和TFCard接口不兼容,需要适配器转接
    class TFAdapater implements USBSerialPort{

        // TF适配器是专门适配TFCard的,所以持有一个TFCard
        TFCard tfCard = new TFCard();

        // 计算机读取TFCard适配器时,适配器读取TFCard中的内容
        @Override
        public void readUSB() {
            tfCard.readTFCard();
        }
    }

    @Test
    public void computerClient(){

        // USB接口插上适配器
        USBSerialPort usbSerialPort = new TFAdapater();
        usbSerialPort.readUSB();
    }

}

当然,USB串口上可以插各种各样的其他设备,如键盘等,只要你做一个适配器就可以了。

JDK源码中,早期日志框架使用的是java自带的JUL,后来出现了log4j,这两者都是具体的实现,如果一个项目中这两种日志都使用了,那么就很难控制日志的配置,比如,调整日志级别等。于是出现了common-logging这种面向接口的设计,客户端只需要持有common-logging中的Logger引用,commom-logging对接JUL或者log4j,当然这JUL和log4j具体的接口是不一致的,common-logging就使用了适配器来对接两者。如下图部分源码。

20191104144503.png

还有JDK IO中的InputStreamReader也属于适配器模式,InputStreamReader适配器两端连接了Reader和InputStream。

门面模式

门面模式是对象的结构模式,外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。说白了,就是提供给外部使用的要尽可能简单,外部系统可能完成一件事要调用你多个api,这时可以使用门面模式,将这些步骤整合在一起,提供一个门面接口即可。例如,slf4j和common-logging,他们并不具体实现写日志的功能,而只是一个门面,我们只需要和门面交互,就可以实现打日志的功能,而不需要和具体的log4j,jul等交互。

桥接模式

桥接模式将抽象部分与它的实现部分分离,使它们都可以独立地变化。举个例子,汽车Car有品牌Brand、驾驶模式DriveMode两种模式。品牌有宝马、奔驰等,驾驶模式又有手动挡和自动挡等,相当于在桥的一端有宝马、奔驰,另一端有自动挡、手动挡。如果以品牌为实现类,为驾驶模式创建子类,如宝马手动挡、宝马自动挡。那么将会创造出大量的类。这个时候,可以将DriveMode类型不作为Brand的子类,而是与Brand平级,Brand持有DriveMode的实例,即Brand桥接到DriveMode。如此,新增一个Brand就不会影响DriveMode,两者独立变化。
JDK中的例子,就是jdbc,然而jdbc的实现看起来跟桥接模式定义有点不一样,我分不清,现在先不讨论了。

行为型模式

在对象的结构和对象的创建问题都解决了之后,就剩下对象的行为问题了,如果对象的行为设计的好,那么对象的行为就会更清晰,它们之间的协作效率就会提高。比如Spring中JdbcTemplate即使来控制数据库访问流程,使用的是行为型的模板方法模式。

策略模式

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。策略模式比较简单,通俗的讲,就是客户端持有一个策略抽象,并实例化需要的具体策略,进行调用即可。
策略模式经常用来解决if-else嵌套过多的问题。
JDK中使用策略模式的有ThreadPoolExecutor中的拒绝策略,总共四个拒绝策略实现了RejectedExecutionHandler,子类实现其拒绝算法rejectedExecution(),使用时可以自己选择不同算法。这就是策略模式。

模板方法模式

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。通俗的讲,模板方法就是一个思路很清晰的智者,他将第一步做啥第二步做啥都清清楚楚的罗列出来,等子类来继承,重新子类自己需要的步骤。
Spring源码中jdbcTemplate就是使用模板方法模式,他将连接数据库的步骤放置在模板中,子类去重新自定义的步骤。

责任链模式

责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。通俗的讲,就是每个子类都给自身定一个级,并持有下一个父引用,每次执行抽象方法先判断级别,级别不够的转入链中的下一个节点。

观察者模式

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知它的依赖对象。通俗的说,就是被观察者持有观察者对象列表,当某个事件发生时,会遍历列表执行方法。

参考资料

23种设计模式要在一篇文章中写完,篇幅有点太长了,花了一周有些设计模式还没写到。心力有点交瘁,感觉越写到后面越简略,只能当成自己的笔记来看了。先把文章post出来吧,改为连载模式。