Featured image of post 常见设计模式理解

常见设计模式理解

设计模式的原则:

  • 面向接口编程,而不是面向实现。这个很重要,也是优雅的、可扩展的代码的第一步。
  • 单一职责原则;一个类只负责一个功能,并且该功能有该类完全封装起来。
  • 对修改关闭,对扩展开放。对修改关闭是说,我们辛辛苦苦加班写出来的代码,该实现的功能和该修复的 bug 都完成了,别人可不能说改就改;对扩展开放就比较好理解了,也就是说在我们写好的代码基础上,很容易实现扩展。

创建型模式

一般作用是创建对象,比如工厂模式、单例模式:

工厂模式

简单工厂模式

工厂模式,不对客户端暴露创建逻辑,只提供一个创建对象的接口,由客户端来决定创建哪一个对象。本质是用工厂方法代替new操作;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class FoodFactory {

    public static Food makeFood(String name) {
      //anZhouNoodle 和 HuangMenChicken 都继承自 Food。
        if (name.equals("兰州拉面")) {
            Food noodle = new LanZhouNoodle();
            System.out.println("兰州拉面"+noodle+"出锅啦");
            return noodle;
        } else if (name.equals("黄焖鸡")) {
            Food chicken = new HuangMenChicken();
            System.out.println("黄焖鸡"+ chicken +"出锅啦");
            return chicken;
        } else {
            System.out.println("不知道你做的什么哦~");
            return null;
        }
    }
}

简单工厂模式就是有工厂类,根据不同参数,返回不同的派生自一个父类的对象。

我们强调职责单一原则,一个类只提供一种功能,FoodFactory 的功能就是只要负责生产各种 Food。

工厂模式

当我们需要两个或者两个以上工厂时,引入工厂模式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface FoodFactory {
    Food makeFood(String name);
}
public class ChineseFoodFactory implements FoodFactory {

    @Override
    public Food makeFood(String name) {
        if (name.equals("A")) {
            return new ChineseFoodA();
        } else if (name.equals("B")) {
            return new ChineseFoodB();
        } else {
            return null;
        }
    }
}
public class AmericanFoodFactory implements FoodFactory {

    @Override
    public Food makeFood(String name) {
        if (name.equals("A")) {
            return new AmericanFoodA();
        } else if (name.equals("B")) {
            return new AmericanFoodB();
        } else {
            return null;
        }
    }
}

核心在于,我们只需要知道我们需要哪个工厂,就可以获取到我们想要的的对象。

抽象工厂模式

当涉及产品组的时候,就需要抽象工厂模式;抽象工厂模式(Abstract Factory Pattern)是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

public interface Button {}
public interface Border {}

//实现抽象类
public class MacButton implements Button {}
public class MacBorder implements Border {}

public class WinButton implements Button {}
public class WinBorder implements Border {}


//接着实现超级工厂
public class MacFactory {
	public static Button createButton() {
	    return new MacButton();
	}
	public static Border createBorder() {
	    return new MacBorder();
	}
}
public class WinFactory {
	public static Button createButton() {
	    return new WinButton();
	}
	public static Border createBorder() {
	    return new WinBorder();
	}
}
  • 优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

  • 缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。

单例模式

实例对象只有一个,没有办法new,一般通过getInstance()获取它们的实例。getInstance()的返回值是一个对象的引用,并不是一个新的实例,所以不要错误的理解成多个对象。

饿汉

1
2
3
4
5
6
7
public class Singleton {  
   private static Singleton instance = new Singleton();  
   private Singleton (){}  
   public static Singleton getInstance() {  
   return instance;  
   }  
}

缺点:类加载的时候就创建对象,可能造成内存浪费。

饱汉

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
    // 首先,也是先堵死 new Singleton() 这条路
    private Singleton() {}
    // 和饿汉模式相比,这边不需要先实例化出来,注意这里的 volatile,它是必须的
    private static volatile Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            // 加锁
            synchronized (Singleton.class) {
                // 这一次判断也是必须的,不然会有并发问题
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

双重检查是在保障性能前提下,保障并发安全;比如在多线程环境下,可能会出现多个线程同时进入到内层 if (instance == null) 这一步,但是只有一个线程会进入到 instance = new Singleton() 这一步,其他线程都会进入到 else 这一步,这样就保证了安全性。 volatile 关键字保证了可见性,多线程情况下,某一个线程赋值成功,其它线程则能够看到这个值,但是不能保证原子性,所以还是要加锁。

静态内部类单例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

public class SingleTon{
  private SingleTon(){}
 
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。

枚举单例

在其他创建型设计模式的学习中,我们已经了解,除了直接通过new和使用工厂来创建对象以外,还可以通过克隆、反射和反序列化等方式来创建对象。

但是用这些方式来创建对象时有可能会导致单例对象的不唯一,如何解决这些问题呢?

(1) 为了防止客户端使用克隆方法来创建对象,单例类不能实现Cloneable接口,即不能支持clone()方法。

(2) 由于反射可以获取到类的构造函数,包括私有构造函数,因此反射可以生成新的对象。

采用一些传统的实现方法都不能避免客户端通过反射来创建新对象,此时,我们可以通过枚举单例对象的方式来解决该问题。

(3) 在原型模式中,我们可以通过反序列化实现深克隆,反序列化也会生成新的对象。具体来说就是每调用一次readObject()方法,都将会返回一个新建的实例对象,这个新建的实例对象不同于类在初始化时创建的实例对象。

那么,如何防止反序列化创建对象呢?解决方法一是类不能实现Serializable接口,即不允许该类支持序列化,这将导致类的应用受限制(有时候我们还是需要对一个对象进行持久化处理);解决方法二就是枚举实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//简单实现
public enum SingleTon{
  INSTANCE;
  public void whateverMethod(){}
}

//改造现有类
public class Singleton {
    private Singleton(){
    }   
    public static enum SingletonEnum {
        SINGLETON;
        private Singleton instance = null;
        private SingletonEnum(){
            instance = new Singleton();
        }
        public Singleton getInstance(){
            return instance;
        }
    }
}

在实现过程中,Java虚拟机会保证枚举类型不能被反射并且构造函数只被执行一次。在这种实现方式中,既可以避免多线程同步问题;还可以防止通过反射和反序列化来重新创建新的对象。

建造者模式

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。

主要解决:主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。

何时使用:==一些基本部件不会变,而其组合经常变化的时候。==

如何解决:将变与不变分离开。

优点:

  • 建造者独立,易扩展。
  • 便于控制细节风险。

缺点:

  • 产品必须有共同点,范围有限制。
  • 如内部变化复杂,会有很多的建造类。

使用场景: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。

注意事项:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。而工厂更专注于零件的制造过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//车实体
public class Car {
  //车轮
    private String wheel;
    //外壳
    private String shell;
    //发动机
    private String engine;
    //方向盘
    private String steeringWheel;
}

//车的建造者
public abstract class CarBuilder {
    //造车轮的方法,因为实际上车轮可能是一个复杂对象
    public abstract void buildWheel();
    //造外壳的方法
    public abstract void buildShell();
    //造引擎的方法
    public abstract void buildEngine();
    //造方向盘的方法
    public abstract void buildSteeringWheel();
    
    public abstract Car getCar();
}


//指挥者,指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类
public class CarDirector {
    private CarBuilder builder;

    public CarDirector(CarBuilder builder) {
        this.builder = builder;
    }
    public Car build(){
        builder.buildEngine();
        builder.buildShell();
        builder.buildSteeringWheel();
        builder.buildWheel();
        return builder.getCar();
    }
}

原型模式

通过给出一个原型对象来指明所有创建的对象的类型,然后用复制这个原型对象的办法创建出更多同类型的对象。这就是选型模式的用意。

原型模式要求对象实现一个可以“克隆”自身的接口,这样就可以通过复制一个实例对象本身来创建一个新的实例。这样一来,通过原型实例创建新的对象,就不再需要关心这个实例本身的类型,只要实现了克隆自身的方法,就可以通过这个方法来获取新的对象,而无须再去通过new来创建。

原型模式有两种表现形式:(1)简单形式、(2)登记形式,这两种表现形式仅仅是原型模式的不同实现。

结构型模式

结构型模式旨在通过改变代码结构来达到解耦的目的,使得我们的代码容易维护和扩展。

代理模式

代理模式是一种使用代理对象来执行目标对象的方法并在代理对象中增强目标对象方法的一种设计模式。

使用代理模式的原因有:

  • 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理对象可以在客户类和委托对象之间起到中介的作用(代理类和委托类实现相同的接口)。以现实生活为例,经纪人就是明星的代理,外界可以通过联系经纪人来间接与明星沟通。
  • 开放封闭原则:可以通过给代理类增加额外的功能来扩展委托类的功能,这样只需要修改代理类而不需要再修改委托类,符合开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。使用代理模式,可以在调用委托类业务功能的前后加入一些公共的服务(例如鉴权、计时、缓存、日志、事务处理等),甚至修改委托类的业务功能。

代理可以分为静态代理和动态代理,前者更接近代理模式的本质。

  • 静态代理是由程序员编写代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就已确定。
  • 动态代理是代理类的源码是在程序运行期间由编译器动态的生成(如JVM根据反射等机制生成代理类)。代理类和委托类的关系在程序运行时确定。

场景和实现的不同,分为静态代理、JDK动态代理、Cglib动态代理(也称子类代理)以及Spring AOP代理

行为型模式

模板模式

模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。

模板模式的关键点:

    1、使用抽象类定义模板类,并在其中定义所有的基本方法、模板方法,钩子方法,不限数量,以实现功能逻辑为主。其中基本方法使用final修饰,其中要调用基本方法和钩子方法,基本方法和钩子方法可以使用protected修饰,表明可被子类修改。

    2、定义实现抽象类的子类,重写其中的模板方法,甚至钩子方法,完善具体的逻辑。

  使用场景:

    1、在多个子类中拥有相同的方法,而且逻辑相同时,可以将这些方法抽出来放到一个模板抽象类中。

    2、程序主框架相同,细节不同的情况下,也可以使用模板方法。

架构方法介绍

模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。其主要分为两大类:模版方法和基本方法,而基本方法又分为:抽象方法(Abstract Method),具体方法(Concrete Method),钩子方法(Hook Method)。

四种方法的基本定义(前提:在抽象类中定义):

(1)抽象方法:由抽象类声明,由具体子类实现,并以abstract关键字进行标识。

(2)具体方法:由抽象类声明并且实现,子类并不实现或者做覆盖操作。其实质就是普遍适用的方法,不需要子类来实现。

(3)钩子方法:由抽象类声明并且实现,子类也可以选择加以扩展。通常抽象类会给出一个空的钩子方法,也就是没有实现的扩展。它和具体方法在代码上没有区别,不过是一种意识的区别;而它和抽象方法有时候也是没有区别的,就是在子类都需要将其实现的时候。而不同的是抽象方法必须实现,而钩子方法可以不实现。也就是说钩子方法为你在实现某一个抽象类的时候提供了可选项,相当于预先提供了一个默认配置。

(4)模板方法:定义了一个方法,其中定义了整个逻辑的基本骨架。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public abstract class AbstractTemplate {
    // 这就是模板方法
    public void templateMethod() {
        init();
        apply(); // 这个是重点
        end(); // 可以作为钩子方法
    }
	//这是具体方法
    protected void init() {
        System.out.println("init 抽象层已经实现,子类也可以选择覆写");
    }

    // 这是抽象方法,留给子类实现
    protected abstract void apply();
	//这是钩子方法,可定义一个默认操作,或者为空
    protected void end() {
    }
}
Built with Hugo     主题 StackJimmy 设计