Design Patterns Of Singleton and Factory


设计模式概述和分类

1、设计模式介绍

  1. 设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是==某类问题的通用解决方案==,设计模式(Design Pattern)代表了最佳的实践。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
  2. 设计模式的本质:提高软件的 维护性、通用性和扩展性,并降低软件的复杂程度。
  3. 经典书籍《Design Pattern》

2、设计模式类型

设计模式分为三种类型,共 23 种(当然也有一些变种)

  1. 创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式

  2. 结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式

  3. 行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter 模式)、状态模式、策略模式、职责链模式(责任链模式)

  • 创建型模式:用于描述“怎样创建对象”,它的主要特点是 “将对象的创建与使用分离
  • 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构
  • 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责

一、单例模式

类的单例设计模式,就是采取一定的方法保证在整个软件系统中,对某个类 只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例的设计考察很多知识:线程安全、序列化攻击、反射、懒加载、继承等等。

分类

  • 饿汉式(静态常量)
  • 饿汉式(静态代码块)
  • 懒汉式(线程不安全)
  • 懒汉式(线程安全,同步方法)
  • 懒汉式(线程安全,同步代码块)
  • 双重检查
  • 静态内部类
  • 枚举
  • 原子引用

1、饿汉式(静态常量)

静态常量

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。

饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。

  • 优缺点说明:
    • 优点:写法简单,类加载的时候完成实例化。避免线程同步问题
    • 缺点:如果从未使用过这个对象,就会造成内存的浪费
  • 这种机制基于 ClassLoader 机制避免了多线程的同步问题
    • 通常大部分的单例模式都是通过调用 getInstance()方法,让 JVM 进行类的加载
    • 但是造成类加载的原因不止这一种,我们不能确定是否是因为其他原因造成了 JVM 加载我们的单例类
  • 结论:这种单例模式可以使用,但是 可能 造成内存浪费

2、饿汉式(静态代码块)

public class HungrySingleton {

    private static HungrySingleton instance = null;

    static {
        // 加载类的时候初始化
        instance = new HungrySingleton();
    }

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}
  • 优缺点说明:
    • 这种方式和上一种类似,只不过将类实例化的过程放在了静态代码块中,也是在类加载的时候,执行静态代码块中的内容,优缺点和上面一样
  • 结论:这种单例模式可以使用,但是 可能 造成内存浪费

3、懒汉式(线程不安全|安全)

public class LazySingleton {
  
    private static volatile LazySingleton instance = null;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }

        return instance;
    }
}

该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。

注意:如果编写的是多线程程序,则不要删除上例代码中的关键字 volatile 和 synchronized,否则将存在线程不安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。

补充:临界资源、临界区

  • 优缺点说明:
    • 起到了 Lazy Loading 的效果,但是只有在单线程下使用是安全的
    • 如果在多线程下就需要进行同步,会影响性能且消耗更多的资源
    • 结论:在实际开发中,不要使用这种模式(会存在潜在的风险或同步效率太低)

4、懒汉式(同步代码块)

上一种方法中,我们使用了同步方法(将 synchronized 作用于类方法),这种方式同步粒度太大,效率太低。

所以有人会选择将同步机制放到代码块上,但是这样是无法解决线程安全的,不要这样写。

因为假如一个线程进入了 if 判断中,还没来得及往下执行,另一个线程也通过了这个 if 判断,就有可能产生多个实例。

public static LazySingleton getInstance() {
  if (instance == null) {

    synchronized (LazySingleton.class) {
      instance = new LazySingleton();
    }
  }

  return instance;
}

结论:不能这样做

5、单例(双重检查)推荐使用

public class Singleton {

    private static volatile Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

使用 volatile 关键字修饰,可以让变量在多线程之间可见,并且每次修改变量后就会迅速同步。

优缺点说明:

  • Double-Check 概念是多线程开发中常常用到的,我们进行了两次检查,这样就可以保证线程安全了
  • 实例化的代码只执行一次,后面再次访问时,就会直接 return 实例化的对象,这样可以避免同步方法
  • 线程安全:延迟加载、效率较高
  • 结论:在实际开发中,推荐使用这种单例设计模式

6、单例(静态内部类)推荐使用

public class Singleton {

    private Singleton() {
    }

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

优缺点说明:

  • 这种方式利用了 JVM 类加载机制的特点保证了线程安全(在初始化实例时只有一个线程)。
  • 静态内部类方式在 Singleton 类被加载时不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化
  • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程安全,在类进行初始化时,别的线程无法进入
  • 优点:避免了线程不安全,利用静态内部类特点实现懒加载,效率高
  • 结论:推荐使用

7、单例(枚举)推荐使用

public enum Singleton {
    INSTANCE;   // 属性
    /**
     * operation
     */
    public void operation() {
        System.out.println("end operation.");
    }
}

优缺点说明:

  • 使用枚举实现单例模式,不仅可以避免多线程同步问题,还可以防止反序列化重新创建新的对象
  • 这种方式是 Effective Java 的作者 Josh Bloch 提倡的方式
    • 可以有效防御复杂的序列化和反射攻击
    • 但是如果单例必须扩展一个超类,而不是扩展 Enum 的时候,就不适合使用这种方法(虽然可以声明枚举去实现接口)
  • 结论:推荐使用

8、原子引用

利用 JUC 提供的 java.util.concurrent.atomic.AtomicReference 原子引用类来确保单例:

public class Instance {
    
    private static final AtomicReference<Instance> INSTANCE = new AtomicReference<>();

    private Instance() {
    }
    
    public static final Instance getInstance() {
        // 循环 CAS 确保对单例的引用唯一
        for (;;) {
            Instance instance = INSTANCE.get();
            if (null != instance)
                return instance;
            INSTANCE.compareAndSet(null, new Instance());
            return INSTANCE.get();
        }
    }
}

9、总结

单例模式的实现方法有很多种,可以这样去选择使用:

  • 如果确定该单例对象一定会被使用,可以选择饿汉式
  • 否则可以根据情况选择上述推荐使用的方式

注意事项和细节说明

  1. 单例模式保证了,系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  2. 想要实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用关键字 new
  3. 单例模式使用的场景:
    • 需要频繁创建和销毁的对象、创建对象耗时过多或者耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等等)

二、工厂设计模式

看一个具体的需求:

关于披萨的项目:要便于披萨种类的扩展,也要便于维护

  • 披萨种类有很多:比如 GreekPizza、CheesePizza 等等
  • 披萨的制作流程:prepare、bake、cut、box
  • 完善披萨店的订购功能

1、简单工厂模式

常规思路

按照 oop 的思想:我们需要准备

Pizza 抽象类、CheesePizza 子类、GreekPizza 子类

OrderPizza 定制披萨的工厂类

PizzaStroe 模拟客户端

常规思路优缺点:

  1. 优点是比较好理解,简单易操作
  2. 缺点是违背了设计模式的 ocp 原则,即 对扩展开放,对修改关闭,当我们给类增加新功能时,尽量不修改代码,或者尽可能少改代码

改进:

  • 分析:增加披萨种类就要创建类,需要该很多地方
  • 思路:把创建 pizza 对象封装到一个类中 简单工厂模式

简单工厂模式(静态工厂方法模式)

基本介绍:

  1. 简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品的实例。
  2. 简单工厂模式:定义了一个创建对象的类,这个类来 封装实例化对象的行为
  3. 在软件开发中,当我们会用到大量的创建某种、某类或某批对象时,就会使用到工厂模式。

优点:

  • 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
  • 客户端无需知道所创建具体产品的类名,只需知道参数即可。
  • 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。

缺点:

  • 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则
  • 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
  • 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
  • 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构。

应用场景:

对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。

模式结构:

简单工厂模式的主要角色如下:

  • 简单工厂(SimpleFactory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
  • 抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
  • 具体产品(ConcreteProduct):是简单工厂模式的创建目标。

2、工厂方法模式

“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。
  • 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
  • 典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。

缺点:

  • 类的个数容易过多,增加复杂度
  • 增加了系统的抽象性和理解难度
  • 抽象产品只能生产一种产品,此弊端可使用抽象工厂模式解决。

结构:

工厂方法模式的主要角色如下。

  1. 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
  2. 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  3. 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  4. 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机

同种类称为同等级,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如农场里既养动物又种植物,电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。

3、抽象工厂模式

抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族

基本介绍:

  1. 抽象工厂模式:定义了一个 interface 用于创建相关或有依赖关系的对象簇,而无需指明具体的类
  2. 抽象工厂模式可以将 简单工厂模式工厂方法模式 进行整合
  3. 从设计层看,抽象工厂模式是对简单工厂模式的改进(或进一步抽象)
  4. 将工厂抽象为两层,AbsFactory(抽象工厂)和具体实现的工厂子类。程序员可以根据创建对象类型使用相应的工厂子类。这样将单个的简单工厂变成了 工厂簇,更利于代码的维护和扩展。

4、工厂模式小结

  1. 工厂模式的意义
    • 将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦。从而提高项目的扩展和维护性
  2. 三种工厂模式
  3. 设计模式的 依赖抽象原则
  • 创建对象实例时,不要 new,要把 new 类的动作放到一个工厂的方法中,并返回实例。
  • 不要让类继承具体类,而是继承抽象类或实现接口
  • 不要覆盖基类中已经实现的方法

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC