对之前去年学习的设计模式进行总结:
一共介绍24种设计模式。

一、设计原则

六大设计原则包括:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则、开闭原则,接下来我们一一来看看它们分别是什么。

1. 单一职责原则

单一职责是指一个类只负责一个职责。比如现在比较流行的微服务,就是将之前很复杂耦合性很高的业务,分成多个独立的功能单一的简单接口,然后通过服务编排组装的方式实现不同的业务需求,而这种细粒度的独立接口就是符合单一职责原则的具体实践。

2. 开闭原则

开闭原则指的是对拓展开放、对修改关闭。它是说我们在实现一个新功能时,首先应该想到的是扩展原来的功能,而不是修改之前的功能。
这个设计思想非常重要,也是一名优秀工程师所必备的设计思想。至于为什么要这样做?其实非常简单,我们团队在开发后端接口时遵循的也是这个理念。
随着软件越做越大,对应的客户端版本也越来越多,而这些客户端都是安装在用户的手机上。因此我们不能保证所有用户手中的 App(客户端)都一直是最新版本的,并且也不能每次都强制用户进行升级或者是协助用户去升级,那么我们在开发新功能时,就强制要求团队人员不允许直接修改原来的老接口,而是要在原有的接口上进行扩展升级。
因为直接修改老接口带来的隐患是老版本的 App 将不能使用,这显然不符合我们的要求。那么此时在老接口上进行扩展无疑是最好的解决方案,因为这样我们既可以满足新业务也不用担心新加的代码会影响到老版本的使用。

3. 里氏替换原则

里氏替换原则是面向对象(OOP)编程的实现基础,它指的是所有引用了父类的地方都能被子类所替代,并且使用子类替代不会引发任何异常或者是错误的出现。
比如,如果把鸵鸟归为了“鸟”类,那么鸵鸟就是“鸟”的子类,但是鸟类会飞,而鸵鸟不会飞,那么鸵鸟就违背了里氏替换原则。

4. 依赖倒置原则

依赖倒置原则指的是要针对接口编程,而不是面向具体的实现编程。也就说高层模块不应该依赖底层模块,因为底层模块的职责通常更单一,不足以应对高层模块的变动,因此我们在实现时,应该依赖高层模块而非底层模块。
比如我们要从 A 地点去往 B 地点,此时应该掏出手机预约一个“车”,而这个“车”就是一个顶级的接口,它的实现类可以是各种各样的车,不同厂商的车甚至是不同颜色的车,而不应该依赖于某一个具体的车。例如,我们依赖某个车牌为 XXX 的车,那么一旦这辆车发生了故障或者这辆车正拉着其他乘客,就会对我的出行带来不便。所以我们应该依赖是“车”这一个顶级接口,而不是具体的某一辆车。

5. 接口隔离原则

接口隔离原则是指使用多个专门的接口比使用单一的总接口要好,即接口应该是相互隔离的小接口,而不是一个臃肿且庞杂的大接口。
使用接口隔离原则的好处是避免接口的污染,提高了程序的灵活性。
可以看出,接口隔离原则和单一职责原则的概念很像,单一职责原则要求接口的职责要单一,而接口隔离原则要求接口要尽量细化,二者虽然有异曲同工之妙,但可以看出单一职责原则要求的粒度更细。

6. 迪米特法则

迪米特法则又叫最少知识原则,它是指一个类对于其他类知道的越少越好。
迪米特法则设计的初衷是降低类之间的耦合,让每个类对其他类都不了解,因此每个类都在做自己的事情,这样就能降低类之间的耦合性。
这就好比我们在一些电视中看到的有些人在遇到强盗时,会选择闭着眼睛不看强盗,因为知道的信息越少反而对自己就越安全,这就是迪米特法则的基本思想。

二、单例模式

单例模式是指一个类在运行期间始终只有一个实例,我们把它称之为单例模式。
单例模式的典型应用场景是 Spring 中 Bean 实例,它默认就是 singleton 单例模式。
单例模式的优点很明显,可以有效地节约内存,并提高对象的访问速度,同时避免重复创建和销毁对象所带来的性能消耗,尤其是对频繁创建和销毁对象的业务场景来说优势更明显。然而单例模式一般不会实现接口,因此它的扩展性不是很好,并且单例模式违背了单一职责原则,因为单例类在一个方法中既创建了类又提供类对象的复合操作,这样就违背了单一职责原则,这也是单例模式的缺点所在。

单例的实现分为饿汉模式和懒汉模式。顾名思义,饿汉模式就好比他是一个饿汉,而且有一定的危机意识,他会提前把食物囤积好,以备饿了之后直接能吃到食物。对应到程序中指的是,在类加载时就会进行单例的初始化,以后访问时直接使用单例对象即可。

1.饿汉式

类加载的 初始化阶段就完成了 实例的初始化 。本质上就是借助于jvm 类加载机制,保证实例的唯一性(初始化过程只会执行一次)及线程安 全(JVM以同步的形式来完成类加载的整个过程)。
类加载过程:
1,加载二进制数据到内存中, 生成对应的Class数据结构,
2,连接: a. 验证, b.准备(给类的静态成员变量赋默认值),c.解析
3,初始化: 给类的静态变量赋初值
只有在真正使用对应的类时,才会触发初始化 如( 当前类是启动类即 main函数所在类,直接进行new 操作,访问静态属性、访问静态方 法,用反射访问类,初始化一个类的子类等.)

  1. public class Singleton {
  2. // 声明私有对象
  3. private static Singleton instance = new Singleton();
  4. // 获取实例(单例对象)
  5. public static Singleton getInstance() {
  6. return instance;
  7. }
  8. private Singleton() {
  9. }
  10. // 方法
  11. public void sayHi() {
  12. System.out.println("Hi,Java.");
  13. }
  14. }
  15. class SingletonTest {
  16. public static void main(String[] args) {
  17. // 调用单例对象
  18. Singleton singleton = Singleton.getInstance();
  19. // 调用方法
  20. singleton.sayHi();
  21. }
  22. }
  23. //Hi,Java.

从上述结果可以看出,单例对象已经被成功获取到并顺利地执行了类中的方法。它的优点是线程安全,因为单例对象在类加载的时候就已经被初始化了,当调用单例对象时只是把早已经创建好的对象赋值给变量;它的缺点是可能会造成资源浪费,如果类加载了单例对象(对象被创建了),但是一直没有使用,这样就造成了资源的浪费。

懒汉模式也被称作为饱汉模式,顾名思义他比较懒,每次只有需要吃饭的时候,才出去找饭吃,而不是像饿汉那样早早把饭准备好。对应到程序中指的是,当每次需要使用实例时,再去创建获取实例,而不是在类加载时就将实例创建好。

2.懒汉式

public class Singleton {
    // 声明私有对象
    private static Singleton instance;
    // 获取实例(单例对象)
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton() {
    }
    // 方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}


//Hi,Java.

从上述结果可以看出,单例对象已经被成功获取到并顺利地执行了类中的方法,它的优点是不会造成资源的浪费,因为在调用的时候才会创建被实例化对象;它的缺点在多线程环境下是非线程是安全的,比如多个线程同时执行到 if 判断处,此时判断结果都是未被初始化,那么这些线程就会同时创建 n 个实例,这样就会导致意外的情况发生。

3.双重检测锁

为了保证懒汉模式的线程安全我们最简单的做法就是给获取实例的方法上加上 synchronized(同步锁)修饰,如下代码所示:

public class Singleton {
    // 声明私有对象
    private static Singleton instance;
    // 获取实例(单例对象)
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton() {
    }
    // 类方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

这样虽然能让懒汉模式变成线程安全的,但由于整个方法都被 synchronized 所包围,因此增加了同步开销,降低了程序的执行效率。

于是为了改进程序的执行效率,我们将 synchronized 放入到方法中,以此来减少被同步锁所修饰的代码范围,实现代码如下:

public class Singleton {
    // 声明私有对象
    private static Singleton instance;
    // 获取实例(单例对象)
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
    private Singleton() {
    }
    // 类方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

细心的你可能会发现以上的代码也存在着非线程安全的问题。例如,当两个线程同时执行到「if (instance == null) { 」判断时,判断的结果都为 true,于是他们就排队都创建了新的对象,这显然不符合我们的预期。
双重检测锁(Double Checked Lock,DCL)

public class Singleton {
    // 声明私有对象
    private static Singleton instance;
    // 获取实例(单例对象)
    public static Singleton getInstance() {
        // 第一次判断
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次判断
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton() {
    }
    // 类方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

上述代码看似完美,其实隐藏着一个不容易被人发现的小问题,该问题就出在 new 对象这行代码上,也就是 instance = new Singleton() 这行代码。这行代码看似是一个原子操作,然而并不是,这行代码最终会被编译成多条汇编指令,它大致的执行流程为以下三个步骤:

  1. 给对象实例分配内存空间;
  2. 调用对象的构造方法、初始化成员字段;
  3. 将 instance 对象指向分配的内存空间。

但由于 CPU 的优化会对执行指令进行重排序,也就说上面的执行流程的执行顺序有可能是 1-2-3,也有可能是 1-3-2。假如执行的顺序是 1-3-2,那么当 A 线程执行到步骤 3 时,切换至 B 线程了,而此时 B 线程判断 instance 对象已经指向了对应的内存空间,并非为 null 时就会直接进行返回,而此时因为没有执行步骤 2,因此得到的是一个未初始化完成的对象,这样就导致了问题的诞生。执行时间节点如下表所示:
image.png
为了解决此问题,我们可以使用关键字 volatile 来修饰 instance 对象,这样就可以防止 CPU 指令重排,从而完美地运行懒汉模式,实现代码如下

public class Singleton {
    // 声明私有对象
    private volatile static Singleton instance;
    // 获取实例(单例对象)
    public static Singleton getInstance() {
        // 第一次判断
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次判断
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton() {
    }
    // 类方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

单例其他实现方式
除了以上的 6 种方式可以实现单例模式外,还可以使用静态内部类和枚举类来实现单例。静态内部类的实现代码如下:

public class Singleton {
    // 静态内部类
    private static class SingletonInstance {
        private static final Singleton instance = new Singleton();
    }
    // 获取实例(单例对象)
    public static Singleton getInstance() {
        return SingletonInstance.instance;
    }
    private Singleton() {
    }
    // 类方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}

从上述代码可以看出,静态内部类和饿汉方式有异曲同工之妙,它们都采用了类装载的机制来保证,当初始化实例时只有一个线程执行,从而保证了多线程下的安全操作。JVM 会在类初始化阶段(也就是类装载阶段)创建一个锁,该锁可以保证多个线程同步执行类初始化的工作,因此在多线程环境下,类加载机制依然是线程安全的。

但静态内部类和饿汉方式也有着细微的差别,饿汉方式是在程序启动时就会进行加载,因此可能造成资源的浪费;而静态内部类只有在调用 getInstance() 方法时,才会装载内部类从而完成实例的初始化工作,因此不会造成资源浪费的问题。由此可知,此方式也是较为推荐的单例实现方式。

单例的另一种实现方式为枚举,它也是《Effective Java》作者极力推荐地单例实现方式,因为枚举的实现方式不仅是线程安全的,而且只会装载一次,无论是序列化、反序列化、反射还是克隆都不会新创建对象。它的实现代码如下:

ublic class Singleton {
    // 枚举类型是线程安全的,并且只会装载一次
    private enum SingletonEnum {
        INSTANCE;
        // 声明单例对象
        private final Singleton instance;
        // 实例化
        SingletonEnum() {
            instance = new Singleton();
        }
        private Singleton getInstance() {
            return instance;
        }
    }
    // 获取实例(单例对象)
    public static Singleton getInstance() {
        return SingletonEnum.INSTANCE.getInstance();
    }
    private Singleton() {
    }
    // 类方法
    public void sayHi() {
        System.out.println("Hi,Java.");
    }
}
class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.sayHi();
    }
}


//Hi,Java.

反序列化

三、 原型模式

原型模式属于创建型模式,它是指通过“克隆”来产生一个新的对象。所以它的核心方法是 clone(),我们通过该方法就可以复制出一个新的对象。

在 Java 语言中我们只需要实现 Cloneable 接口,并重写 clone() 方法就可以实现克隆了,实现代码如下:

public class CloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 创建一个新对象
        People p1 = new People();
        p1.setId(1);
        p1.setName("Java");
        // 克隆对象
        People p2 = (People) p1.clone();
        // 输出新对象的名称
        System.out.println("People 2:" + p2.getName());
    }
    static class People implements Cloneable {
        private Integer id;
        private String name;
        /**
         * 重写 clone 方法
         */
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}


//People 2:Java

但需要注意的是,以上代码为浅克隆的实现方式,如果要实现深克隆(对所有属性无论是基本类型还是引用类型的克隆)可以通过以下手段实现:

  • 所有对象都实现克隆方法;
  • 通过构造方法实现深克隆;
  • 使用 JDK 自带的字节流实现深克隆;
  • 使用第三方工具实现深克隆,比如 Apache Commons Lang;
  • 使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等。
  • 具体的实现代码可以参考我们第 07 课时的内容。

原型模式的典型使用场景是 Java 语言中的 Object.clone() 方法,它的优点是性能比较高,因为它是通过直接拷贝内存中的二进制流实现的复制,因此具备很好的性能。它的缺点是在对象层级嵌套比较深时,复制的代码实现难度比较大。

深拷贝TODO

浅克隆(浅拷贝)

四、命令模式

命令模式属于行为模式的一种,它是指将一个请求封装成一个对象,并且提供命令的撤销和恢复功能。说得简单一点就是将发送者、接收者和调用命令封装成独立的对象,以供客户端来调用,它的具体实现代码如下。

接收者的示例代码:

// 接收者
class Receiver {
    public void doSomething() {
        System.out.println("执行业务逻辑");
    }
}

命令对象的示例代码:

// 命令接口
interface Command {
    void execute();
}
// 具体命令类
class ConcreteCommand implements Command {
    private Receiver receiver;
    public ConcreteCommand(Receiver receiver) {
        this.receiver = receiver;
    }
    public void execute() {
        this.receiver.doSomething();
    }
}

请求者的示例代码

// 请求者类
class Invoker {
    // 持有命令对象
    private Command command;
    public Invoker(Command command) {
        this.command = command;
    }
    // 请求方法
    public void action() {
        this.command.execute();
    }
}

客户端的示例代码:

// 客户端
class Client {
    public static void main(String[] args) {
        // 创建接收者
        Receiver receiver = new Receiver();
        // 创建命令对象,设定接收者
        Command command = new ConcreteCommand(receiver);
        // 创建请求者,把命令对象设置进去
        Invoker invoker = new Invoker(command);
        // 执行方法
        invoker.action();
    }
}

Spring 框架中的 JdbcTemplate 使用的就是命令模式,它的优点是降低了系统的耦合度,新增的命令可以很容易地添加到系统中;其缺点是如果命令很多就会造成命令类的代码很长,增加了维护的复杂性。

五、委派模式

委派模式的定义及应用场景
委派模式不属于GOF23种设计模式中。委派模式(Delegate Pattern)的基本作用就是负责任务的调用和分配任务,跟代理模式很像,可以看做是一种特殊情况下的静态代理的全权代理,但是代理模式注过程,而委派模式注重结果。委派模式在Spring中应用非常多,大家常用的DispatcherServlet其实就是用到了委派模式。现实生活中也常有委派的场景发生,例如:老板(Boss)给项目经理(Leader)下达任务,项目经理会根据实际情况给每个员工派发工作任务,待员工把工作任务完成之后,再由项目经理汇报工作进度和结果给老板。我们用代码来模拟下这个业务场景,先来看一下类图:
image.png

创建IEmployee 员工接口:

package com.gupaoedu.vip.pattern.delegate.simple;

/**
* Created by Tom.
*/
public interface IEmployee {
    public void doing(String command);
}

创建员工EmployeeA 类:

package com.gupaoedu.vip.pattern.delegate.simple;

/**
* Created by Tom.
*/
public class EmployeeA implements IEmployee { @Override
public void doing(String command) {
System.out.println("我是员工 A,我现在开始干" + command + "工作");
    }
}

创建员工EmployeeB类:

package com.gupaoedu.vip.pattern.delegate.simple;

/**
* Created by Tom.
*/
public class EmployeeB implements IEmployee { @Override
public void doing(String command) {
System.out.println("我是员工 B,我现在开始干" + command + "工作");
}
}

创建项目经理Leader类:

package com.gupaoedu.vip.pattern.delegate.simple;

import java.util.HashMap; import java.util.Map;

/**
* Created by Tom.
*/
public class Leader implements IEmployee {
    private Map<String,IEmployee> targets = new HashMap<String,IEmployee>(); 
    public Leader() {
        targets.put("加密",new EmployeeA()); targets.put("登录",new EmployeeB());
    }

    //项目经理自己不干活
    public void doing(String command){ targets.get(command).doing(command);
    }

}

创建Boss 类下达命令:

* Created by Tom.
*/
public class Boss {

public void command(String command,Leader leader){ 
    leader.doing(command);
}
}

测试代码:

package com.gupaoedu.vip.pattern.delegate.simple;

/**
* Created by Tom.
*/
public class DelegateTest {

public static void main(String[] args) {

//客户请求(Boss)、委派者(Leader)、被被委派者(Target)
//委派者要持有被委派者的引用
//代理模式注重的是过程, 委派模式注重的是结果
//策略模式注重是可扩展(外部扩展),委派模式注重内部的灵活和复用
//委派的核心:就是分发、调度、派遣

//委派模式:就是静态代理和策略模式一种特殊的组合new Boss().command("登录",new Leader());
}

}

通过上面的代码,生动地还原了项目经理分配工作的业务场景,也是委派模式的生动体现。
委派模式在源码中的体现
下面我们再来还原一下SpringMVC 的DispatcherServlet 是如何实现委派模式的。创建业务类MemberController:

/**
* Created by Tom.
*/
public class MemberController {

public void getMemberById(String mid){

}

}

OrderController 类:

package com.gupaoedu.vip.pattern.delegate.mvc.controllers;

/**
* Created by Tom.
*/
public class OrderController {

public void getOrderById(String mid){

}

}

SystemController 类:

package com.gupaoedu.vip.pattern.delegate.mvc.controllers;

/**
* Created by Tom.
*/
public class SystemController { public void logout(){
}
}

创建DispatcherServlet 类:

/**
*相当于是项目经理的角色
*Created by Tom.
*/
public class DispatcherServlet extends HttpServlet{

private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception{

String uri = request.getRequestURI(); String mid = request.getParameter("mid");

    if("getMemberById".equals(uri)){
    new MemberController().getMemberById(mid);
    }else if("getOrderById".equals(uri)){
    new OrderController().getOrderById(mid);
    }else if("logout".equals(uri)){
    new SystemController().logout();
    }else {
    response.getWriter().write("404 Not Found!!");
    }
}

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req,resp);
} catch (Exception e) { e.printStackTrace();
}
}

}

配置web.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<display-name>Gupao Web Application</display-name>


<servlet>
<servlet-name>delegateServlet</servlet-name>
<servlet-class>com.gupaoedu.vip.pattern.delegate.mvc.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>delegateServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>


</web-app>

一个完整的委派模式就实现出来了。当然,在Spring中运用到委派模式不仅于此,还有很多。小伙伴们可以通过命名就可以识别。在Spring源码中,只要以Delegate结尾的都是实现了委派模式。例如:BeanDefinitionParserDelegate根据不同类型委派不同的逻辑解析BeanDefinition。

六、策略模式

策略模式(Strategy Pattern)是指定义了算法家族、分别封装起来,让它们之间可以互相替换,此模式让算法的变化不会影响到使用算法的用户。

策略模式的应用场景
1、假如系统中有很多类,而他们的区别仅仅在于他们的行为不同。
2、一个系统需要动态地在几种算法中选择一种。
用策略模式实现选择支付方式的业务场景
大家都知道,我们咕泡学院的架构师课程经常会有优惠活动,优惠策略会有很多种可能如:领取优惠券抵扣、返现促销、拼团优惠。下面我们用代码来模拟,首先我们创建一个促销策略的抽象PromotionStrategy:

package com.gupaoedu.vip.pattern.strategy.promotion;

/**
*促销策略抽象
*Created by Tom
*/
public interface PromotionStrategy { void doPromotion();
}

然后分别创建优惠券抵扣策略CouponStrategy 类、返现促销策略 CashbackStrategy 类、拼团优惠策略GroupbuyStrategy 类和无优惠策略EmptyStrategy 类:
CouponStrategy 类:

package com.gupaoedu.vip.pattern.strategy.promotion;
/**
*优惠券
*Created by Tom
*/
public class CouponStrategy implements PromotionStrategy { public void doPromotion() {
System.out.println("领取优惠券,课程的价格直接减优惠券面值抵扣");
}
}

CashbackStrategy 类:

/** 
* 返现活动
* Created by Tom
*/
public class CashbackStrategy implements PromotionStrategy { public void doPromotion() {
System.out.println("返现促销,返回的金额转到支付宝账号");
}
}

GroupbuyStrategy 类:

package com.gupaoedu.vip.pattern.strategy.promotion;
/**
*拼团优惠
*Created by Tom
*/
public class GroupbuyStrategy implements PromotionStrategy{ public void doPromotion() {
System.out.println("拼团,满 20 人成团,全团享受团购价");
}
}

EmptyStrategy 类:

package com.gupaoedu.vip.pattern.strategy.promotion;
/**
*无优惠
*Created by Tom
*/
public class EmptyStrategy implements PromotionStrategy { public void doPromotion() {
System.out.println("无促销活动");
}
}

然后创建促销活动方案PromotionActivity 类:

package com.gupaoedu.vip.pattern.strategy.promotion;

/**
*优惠活动
*Created by Tom
*/
public class PromotionActivity {
    private PromotionStrategy promotionStrategy;
    public PromotionActivity(PromotionStrategy promotionStrategy) { this.promotionStrategy = promotionStrategy;
    }
    public void execute(){ promotionStrategy.doPromotion();
    }
}

编写客户端测试类:

public static void main(String[] args) {
PromotionActivity activity618 = new PromotionActivity(new CouponStrategy()); PromotionActivity activity1111 = new PromotionActivity(new CashbackStrategy());

activity618.execute(); activity1111.execute();
}

此时,小伙伴们会发现,如果把上面这段测试代码放到实际的业务场景其实并不实用。因为我们做活动时候往往是要根据不同的需求对促销策略进行动态选择的,并不会一次性执行多种优惠。所以,我们的代码通常会这样写:

public static void main(String[] args) { 
    PromotionActivity promotionActivity = null;

    String promotionKey = "COUPON";

    if(StringUtils.equals(promotionKey,"COUPON")){
        promotionActivity = new PromotionActivity(new CouponStrategy());
    }else if(StringUtils.equals(promotionKey,"CASHBACK")){ 
        promotionActivity = new PromotionActivity(new CashbackStrategy());
    }//......
    promotionActivity.execute();
}

这样改造之后,满足了业务需求,客户可根据自己的需求选择不同的优惠策略了。但是,经过一段时间的业务积累,我们的促销活动会越来越多。于是,我们的程序猿小哥哥就忙不赢了,每次上活动之前都要通宵改代码,而且要做重复测试,判断逻辑可能也变得越来越复杂。这时候,我们是不需要思考代码是不是应该重构了?回顾我们之前学过的设计模式应该如何来优化这段代码呢?其实,我们可以结合单例模式和工厂模式。创建

PromotionStrategyFactory 类:

package com.gupaoedu.vip.pattern.strategy.promotion;
import java.util.HashMap; import java.util.Map;
/**
*促销策略工厂
*Created byTom
*/
public class PromotionStrategyFactory {
    private static Map<String,PromotionStrategy> PROMOTION_STRATEGY_MAP = new HashMap<String, PromotionStrategy>();
    static {
        PROMOTION_STRATEGY_MAP.put(PromotionKey.COUPON,new CouponStrategy()); 
        PROMOTION_STRATEGY_MAP.put(PromotionKey.CASHBACK,new CashbackStrategy()); 
        PROMOTION_STRATEGY_MAP.put(PromotionKey.GROUPBUY,new GroupbuyStrategy());
    }
    private static final PromotionStrategy NON_PROMOTION = new EmptyStrategy();
    private PromotionStrategyFactory(){}
    public static PromotionStrategy getPromotionStrategy(String promotionKey){               
        PromotionStrategy promotionStrategy = PROMOTION_STRATEGY_MAP.get(promotionKey); 
       return promotionStrategy == null ? NON_PROMOTION : promotionStrategy;
    }
    private interface PromotionKey{ 
        String COUPON = "COUPON"; 
        String CASHBACK = "CASHBACK"; 
        String GROUPBUY = "GROUPBUY";
    }
}

这时候我们客户端代码就应该这样写了:

public static void main(String[] args) { String promotionKey = "GROUPBUY"; PromotionActivity promotionActivity = new
PromotionActivity(PromotionStrategyFactory.getPromotionStrategy(promotionKey)); promotionActivity.execute();
}

代码优化之后,是不是我们程序猿小哥哥的维护工作就轻松了?每次上新活动,不影响原来的代码逻辑。为了加深对策略模式的理解,
我们再来举一个案例。相信小伙伴们都用过支付宝、微信支付、银联支付以及京东白条。一个常见的应用场景就是大家在下单支付时会提示选择支付方式,如果用户未选,系统也会默认好推荐的支付方式进行结算。来看一下类图,下面我们用策略模式来模拟此业务场景:
image.png
创建Payment 抽象类,定义支付规范和支付逻辑,代码如下:

package com.gupaoedu.vip.pattern.strategy.pay.payport; import com.gupaoedu.vip.pattern.strategy.pay.PayState;
/**
*支付渠道
*Created by Tom.
*/
public abstract class Payment {

//支付类型
public abstract String getName();

//查询余额
protected abstract double queryBalance(String uid);

//扣款支付
public PayState pay(String uid,double amount) {
    if(queryBalance(uid) < amount){
        return new PayState(500,"支付失败","余额不足");
    }
    return new PayState(200,"支付成功","支付金额:" + amount);
    }
}

分别创建具体的支付方式,支付宝AliPay 类:

package com.gupaoedu.vip.pattern.strategy.pay.payport;

/**
* Created by Tom.
*/
public class AliPay extends Payment {
    public String getName() { return "支付宝";
    }

    protected double queryBalance(String uid) { return 900;
    }
}

京东白条JDPay 类:

package com.gupaoedu.vip.pattern.strategy.pay.payport;
/**
* Created by Tom.
*/
public class JDPay extends Payment {

public String getName() { return "京东白条";
}

protected double queryBalance(String uid) { return 500;
}
}

微信支付WechatPay 类:

*/
public class WechatPay extends Payment {

public String getName() { return "微信支付";
}

protected double queryBalance(String uid) { return 256;
}
}

银联支付UnionPay 类:

package com.gupaoedu.vip.pattern.strategy.pay.payport;
/**
* Created by Tom.
*/
public class UnionPay extends Payment { public String getName() {
return "银联支付";
}

protected double queryBalance(String uid) { return 120;
}
}

创建支付状态的包装类PayState:

package com.gupaoedu.vip.pattern.strategy.pay;

/**
*支付完成以后的状态
*Created by Tom.
*/
public class PayState { 
    private int code; private Object data; private String msg;
    public PayState(int code, String msg,Object data) { 
        this.code = code;
        this.data = data; this.msg = msg;
    }

    public String toString(){
        return ("支付状态:[" + code + "]," + msg + ",交易详情:" + data);
    }
}

创建支付策略管理类:

package com.gupaoedu.vip.pattern.strategy.pay.payport;

import java.util.HashMap; import java.util.Map;

/**
*支付策略管理
*Created by Tom.
*/
public class PayStrategy {
public static final String ALI_PAY = "AliPay"; public static final String JD_PAY = "JdPay"; public static final String UNION_PAY = "UnionPay";
public static final String WECHAT_PAY = "WechatPay"; public static final String DEFAULT_PAY = ALI_PAY;

private static Map<String,Payment> payStrategy = new HashMap<String,Payment>(); static {
payStrategy.put(ALI_PAY,new AliPay()); payStrategy.put(WECHAT_PAY,new WechatPay()); payStrategy.put(UNION_PAY,new UnionPay()); payStrategy.put(JD_PAY,new JDPay());
}

public static Payment get(String payKey){ if(!payStrategy.containsKey(payKey)){
return payStrategy.get(DEFAULT_PAY);
}
return payStrategy.get(payKey);
}
}

创建订单Order 类:


/**
* Created by Tom.
*/
public class Order { private String uid; private String orderId; private double amount;

public Order(String uid,String orderId,double amount){ this.uid = uid;
this.orderId = orderId; this.amount = amount;
}

//完美地解决了 switch 的过程,不需要在代码逻辑中写 switch 了
//更不需要写 if    else if public PayState pay(){
return pay(PayStrategy.DEFAULT_PAY);
}

public PayState pay(String payKey){
Payment payment = PayStrategy.get(payKey); System.out.println("欢迎使用" + payment.getName());
System.out.println("本次交易金额为:" + amount + ",开始扣款...");
return payment.pay(uid,amount);
}
}

测试代码:

package com.gupaoedu.vip.pattern.strategy.pay;

import com.gupaoedu.vip.pattern.strategy.pay.payport.PayStrategy; public class PayStrategyTest {
public static void main(String[] args) {

//省略把商品添加到购物车,再从购物车下单
//直接从点单开始
Order order = new Order("1","20180311001000009",324.45);

//开始支付,选择微信支付、支付宝、银联卡、京东白条、财付通
//每个渠道它支付的具体算法是不一样的
    //基本算法固定的

//这个值是在支付的时候才决定用哪个值System.out.println(order.pay(PayStrategy.ALI_PAY));

}
}

运行结果
image.png
希望通过大家耳熟能详的业务场景来举例,让小伙伴们更深刻地理解策略模式。希望小伙伴们在面试和工作体现出自己的优势。

策略模式在JDK 源码中的体现

首先来看一个比较常用的比较器Comparator 接口, 我们看到的一个大家常用的compare()方法,就是一个策略抽象实现:

public interface Comparator<T> { int compare(T o1, T o2);
...
}

Comparator 抽象下面有非常多的实现类,我们经常会把 Comparator 作为参数传入作为排序策略,例如Arrays 类的parallelSort 方法等:

public class Arrays {
...
public static <T> void parallelSort(T[] a, int fromIndex, int toIndex,
Comparator<? super T> cmp) {
...
}
...
}

还有TreeMap 的构造方法:

public class TreeMap<K,V> extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
...
public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator;
}
...
}

这就是 Comparator 在 JDK 源码中的应用。那我们来看策略模式在 Spring 源码中的应用,来看Resource 类

package org.springframework.core.io;

import  java.io.File; import java.io.IOException; import  java.net.URI; import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel; import org.springframework.lang.Nullable;

public interface Resource extends InputStreamSource { boolean exists();

default boolean isReadable() { return true;
}

default boolean isOpen() { return false;
}

default boolean isFile() { return false;
}
URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException;

default ReadableByteChannel readableChannel() throws IOException { return Channels.newChannel(this.getInputStream());
}

long contentLength() throws IOException; long lastModified() throws IOException;
Resource createRelative(String var1) throws IOException;

@Nullable
String getFilename();

String getDescription();
}

我们虽然没有直接使用Resource 类,但是我们经常使用它的子类,例如:
image.png
还有一个非常典型的场景,Spring 的初始化也采用了策略模式,不同的类型的类采用不同的初始化策略。首先有一个InstantiationStrategy 接口,我们来看一下源码:

import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.lang.Nullable;

public interface InstantiationStrategy {
Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3) throws BeansException;

Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, Constructor<?> var4, @Nullable Object... var5) throws BeansException;

Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, @Nullable Object var4, Method var5, @Nullable Object... var6) throws BeansException;
}

顶层的策略抽象非常简单,但是它下面有两种策略SimpleInstantiationStrategy 和CglibSubclassingInstantiationStrategy,我们看一下类图:
image.png
打开类图我们还发现CglibSubclassingInstantiationStrategy 策略类还继承了SimpleInstantiationStrategy 类,说明在实际应用中多种策略之间还可以继承使用。小

策略模式的优缺点

优点:
1、策略模式符合开闭原则。
2、避免使用多重条件转移语句,如if…else…语句、switch 语句3、使用策略模式可以提高算法的保密性和安全性。
缺点:
1、客户端必须知道所有的策略,并且自行决定使用哪一个策略类。
2、代码中会产生非常多策略类,增加维护难度。

委派模式与策略模式综合应用

在上面的代码中我们列举了非常几个业务场景,相信小伙伴对委派模式和策略模式有了非常深刻的理解了。现在,我们再来回顾一下,DispatcherServlet的委派逻辑,代码如下:

package com.gupaoedu.vip.pattern.delegate.mvc;

...

public class DispatcherServlet extends HttpServlet{

private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception{

String uri = request.getRequestURI(); String mid = request.getParameter("mid");

    if("getMemberById".equals(uri)){
        new MemberController().getMemberById(mid);
    }else if("getOrderById".equals(uri)){
new OrderController().getOrderById(mid);
}else if("logout".equals(uri)){
new SystemController().logout();
}else {
response.getWriter().write("404 Not Found!!");
}
}

...

}

这样的代码扩展性不太优雅,也不现实,因为我们实际项目中一定不止这几个Controller, 往往是成千上万个Controller,显然,我们不能写成千上万个if…else… 。那么我们如何来改造呢?小伙伴们一定首先就想到了策略模式,来看一下我是怎么优化的

package com.gupaoedu.vip.pattern.delegate.mvc;

import com.gupaoedu.vip.pattern.delegate.mvc.controllers.MemberController; import com.gupaoedu.vip.pattern.delegate.mvc.controllers.OrderController; import com.gupaoedu.vip.pattern.delegate.mvc.controllers.SystemController;

import  javax.servlet.ServletException; import  javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.List;

/**
*相当于是项目经理的角色
*Created by Tom.
*/
public class DispatcherServlet extends HttpServlet{
private List<Handler> handlerMapping = new ArrayList<Handler>(); public void init() throws ServletException {
try {
    Class<?> memberControllerClass = MemberController.class; handlerMapping.add(new Handler()
.setController(memberControllerClass.newInstance())
.setMethod(memberControllerClass.getMethod("getMemberById", new Class[]{String.class}))
.setUrl("/web/getMemberById.json"));
}catch(Exception e){

}
}

private void doDispatch(HttpServletRequest request, HttpServletResponse response){

    //1、获取用户请求的 url
    //    如果按照 J2EE 的标准、每个 url 对对应一个 Serlvet,url 由浏览器输入
    String uri = request.getRequestURI();

    //2、Servlet 拿到 url 以后,要做权衡(要做判断,要做选择)
    //    根据用户请求的 URL,去找到这个 url 对应的某一个java 类的方法

    //3、通过拿到的URL 去 handlerMapping(我们把它认为是策略常量) Handler handle = null;
    for (Handler h: handlerMapping) { if(uri.equals(h.getUrl())){
    handle = h; break;
    }
    }

    //4、将具体的任务分发给 Method(通过反射去调用其对应的方法) Object object = null;
    try {
    object = handle.getMethod().invoke(handle.getController(),request.getParameter("mid"));
    } catch (IllegalAccessException e) { e.printStackTrace();
    } catch (InvocationTargetException e) { e.printStackTrace();
    }

    //5、获取到 Method 执行的结果,通过 Response 返回出去
    //    response.getWriter().write();
    }
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    try {
    doDispatch(req,resp);
    } catch (Exception e) { e.printStackTrace();
    }
}

class Handler{

    private Object controller; private Method method; private String url;

    public Object getController() { return controller;
    }

    public Handler setController(Object controller) { this.controller = controller;
    return this;
    }

    public Method getMethod() { return method;
    }

    public Handler setMethod(Method method) { this.method = method;
    return this;
    }

    public String getUrl() { return url;
    }

    public Handler setUrl(String url) { this.url = url;
    return this;
    }
}
}

上面的代码我结合了策略模式、工厂模式、单例模式。当然,我的优化方案不一定是最完美的,仅代表个人观点。感兴趣的小伙伴可以继续思考,如何让这段代码变得更优雅。当然,我们后面在讲Spring 源码时还会讲到DispatcherServlet 的相关内容。

*设计模式面试例题

1.请列举出在JDK中几个常用的设计模式?
单例模式(Singleton pattern)用于Runtime,Calendar和其他的一些类中。工厂模式(Factory pattern)被用于各种不可变的类如 Boolean,像Boolean.valueOf,观察者模式(Observer pattern)被用于 Swing 和很多的事件监听中。装饰器设计模式(Decorator design pattern)被用于多个 Java IO 类中。
2.什么是设计模式?你是否在你的代码里面使用过何设计模式?
设计模式是世界上各种各样程序员用来解决特定设计问题的尝试和测试的方法。设计模式是代码可用性的延伸
3.Java 中什么叫单例设计模式?请用Java 写出线程安全的单例模式
单例模式重点在于在整个系统上共享一些创建时较耗资源的对象。整个应用中只维护一个特定类实例,它被所有组件共同使用。Java.lang.Runtime是单例模式的经典例子。从 Java 5 开始你可以使用枚举(enum)来实现线程安全的单例。
4.在 Java 中,什么叫观察者设计模式(observer design pattern)?
观察者模式是基于对象的状态变化和观察者的通讯,以便他们作出相应的操作。简单的例子就是一个天气系统,当天气变化时必须在展示给公众的视图中进行反映。这个视图对象是一个主体,而不同的视图是观察者。
5.使用工厂模式最主要的好处是什么?在哪里使用?
工厂模式的最大好处是增加了创建对象时的封装层次。如果你使用工厂来创建对象,之后你可以使用更高级和更高性能的实现来替换原始的产品实现或类,这不需要在调用层做任何修改。
6.举一个用 Java 实现的装饰模式(decorator design pattern)?它是作用于对象层次还是类层次?
装饰模式增加强了单个对象的能力。Java IO 到处都使用了装饰模式,典型例子就是 Buffered 系列类如BufferedReader和BufferedWriter,它们增强了Reader和Writer对象,以实现提升性能的 Buffer 层次的读取和写入。
7.在 Java 中,为什么不允许从静态方法中访问非静态变量?
Java 中不能从静态上下文访问非静态数据只是因为非静态变量是跟具体的对象实例关联的,而静态的却没有和任何实例关联。
8.设计一个 ATM 机,请说出你的设计思路?
比如设计金融系统来说,必须知道它们应该在任何情况下都能够正常工作。不管是断电还是其他情况,ATM 应该保持正确的状态(事务) , 想想加锁(locking)、事务(transaction)、错误条件(error condition)、边界条件(boundary condition)等等。尽管你不能想到具体的设计,但如果你可以指出非功能性需求,提出一些问题,想到关于边界条件,这些都会是很好的。
9.在 Java 中,什么时候用重载,什么时候用重写?
如果你看到一个类的不同实现有着不同的方式来做同一件事,那么就应该用重写(overriding),而重载(overloading)是用不同的输入做同一件事。在 Java 中,重载的方法签名不同,而重写并不是。
10.举例说明什么情况下会更倾向于使用抽象类而不是接口?
接口和抽象类都遵循”面向接口而不是实现编码”设计原则,它可以增加代码的灵活性,可以适应不断变化的需求。下面有几个点可以帮助你回答这个问题:
在Java 中,你只能继承一个类,但可以实现多个接口。所以一旦你继承了一个类,你就失去了继承其他类的机会了。
接口通常被用来表示附属描述或行为如:Runnable、Clonable、Serializable等等,因此当你使用抽象类来表示行为时,你的类就不能同时是Runnable和Clonable(注:这里的意思是指如果把Runnable等实现为抽象类的情况),因为在 Java 中你不能继承两个类,但当你使用接口时,你的类就可以同时拥有多个不同的行为。
在一些对时间要求比较高的应用中,倾向于使用抽象类,它会比接口稍快一点。
如果希望把一系列行为都规范在类继承层次内,并且可以更好地在同一个地方进行编码,那么抽象类是一个更好的选择。有时,接口和抽象类可以一起使用,接口中定义函数,而在抽象类中定义默认的实现。