首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

白话讲解创建型设计模式:单例、原型,构建

写在前面


  • 分享一些设计模式的笔记。陆续整理,按照设计模式类型,创建型,结构型,行为型发布
  • 博文会用通俗的话梳理一些自己的理解,结合开发中的实际场景,
  • 关于设计模式,个人觉得,在理解上要站在问题域的角度,而不是它的实现方式,因为学完全部的设计模式,你会感觉,好像大多设计模式实现上基本一样。往往有这一种被欺骗的感觉....哈
  • 理解不足小伙伴帮忙指正,虚心接受 ^_^

傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。--------王小波


在 23 种设计模式中,创建型的设计模式有了 5 种,分别为:单例、原型、建造、工厂方法和抽象工厂。

今天要温习的是前三个

单例

关于单例的实现方式,先不讲,聊聊为什么需要单例?单例的优点是什么,有哪些地方使用了单例?

单例用通俗的话讲,即在某个作用域内,不管如何操作,某个类的实例只能是同一个,创建的这种类实例称为单例模式。

为什么需要单例?

  • 一种情况是有些实例没理由重新创建或者丢弃它,它需要一直存在着,同时,在内存里面只有一个实例,可以减少内存开销,频繁的创建和销毁实例需要考虑 GC 等其他的的问题,比如常见的一些工厂类实例,只是希望通过他来生成一些实例,没必多个实例存在,没必要创建销毁,而且多次重建,编码角度考虑,是很坏的编码,比如一些ORM框架中生成SqlSessionSqlSessionFactory实例,一般使用单例模式或者静态单例模式,粗了减少加载配置的同时,考虑数据库连接数性能问题。
  • 二是某些实例,希望它在整个生命周期内是不可变得,全局唯一的。不管在什么情况下,它的构成能够被严格的控制,可以始终保证他是共享且安全的。同时使用单例可以避免那些存储唯一实例的全局变量污染命名空间。比如 Java 中某个类对应的class实例,都是单例模式,一个 Class 实例用于描述一个类加载到内存中的数据,只描述一个类,即一个类只有一个 Class 实例。同时它没有构造函数,不能主动实例化,而是在类在加载时由 java 虚拟机通过类加载器中的 defineClass 自动构造的。

如何实现单例

对于单例,本质的问题是如何保证只能被实例化一次,所以不管如何实现都需要构造函数私有化.或者没有构造函数由 JVM 自动构造

最简单的实现是饿汉式单例 ,singleton 作为类变量并且直接得到了初始化,即类中所有的变量都会被初始化 singleton 作为类变量在初始化的过程中会被收集进<clinit>()方法中,虽然这样能够百分之百的保证同步,但是因为不是懒加载,singleton 被加载后可能很长一段时间不被使用,即实例所开辟的空间会存在很长时间,内存角度考虑,不是好的实现。

代码语言:javascript
复制
private Singleton(){ }
private  static final Singleton singleton = new Singleton();
public static  Singleton getInstance(){
        return singleton;
  }

所以为了实现懒加载,有了懒汉式单例模式,虽然懒汉式可以保证懒加载,但是线程不安全, 当有两个线程访问时,不能保证单例的唯一性

代码语言:javascript
复制
private Singleton(){ }
private static  Singleton singleton =null;
public static  Singleton getInstance(){
    if (singleton == null) {
                singleton = new Singleton();
    }
                return singleton;
}

为了保证线程安全,有了懒汉式+同步方法,即能保证懒加载,又可以保证 singleton 实例的唯一性,但是synchronizeed关键字的排他性导致getInstance()方法只能在同一时间被一个线程访问。性能低下。

代码语言:javascript
复制
private Singleton(){ }
private static  Singleton singleton =null;
public static synchronized Singleton getInstance(){
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
}

这个时候,有人对懒汉式+同步方法做了改进,双重校验锁单例(Double-Check)+Volatile ,当有两个线程发现 singleton 为 null 时,只有一个线程可以进入到同步代码块里。即满足了懒加载,又保证了线程的唯一性,不加 volition 的缺点,有时候可能会报 NPE,(JVM 运行指令重排序),有可能实例实例的变量未完成实例化其他线程去获取到 singleton 变量。未完成初始化的实例调用其方法会抛出空指针异常。

代码语言:javascript
复制
private Singleton(){ }

private  static volatile Singleton singleton = null;
public static Singleton getInstance() {

    if (singleton == null){
        synchronized (Singleton.class){
            if (singleton ==null){
                singleton = new Singleton();
            }
        }
    }
    return singleton2;
}

有人觉得这样写好麻烦,有些有没有简单的写法,当然有,静态内部类单例,静态内部类的单例模式在 Singleton 类初始化时并不会创建 Singleton 实例,在静态内部类中定义了 singleton 实例。 当给静态内部类被主动创建时则会创建 Singleton 静态变量,是最好的单例模式之一,;类似于静态工厂,将类的创建延迟到静态内部类,外部类的初始化不会实例化静态内部类的静态变量。

代码语言:javascript
复制
private Singleton(){ }
 
private  static class Singtetons{
    private static  Singleton SINGLETON = new Singleton();
   /* static {
         final Singleton SINGLETON = new Singleton();
    }*/

}
public static  Singleton getInstance(){
    return Singtetons.SINGLETON;
}

当然还有其他的一些写法,比如基于枚举的单例等,感兴趣小伙伴可以去了解下

原型

为什么需要原型?

原型模式,用通俗的话讲,即在原有的实例的基础上创建多个新的实例,减少对多实例和复杂实例创建的内存消耗原型模式享元模式有些类似,都是尝试重用现有的同类实例,但是他们本质是不相同的,原型模式对现有实例的再加工,比如 JS 里的原型设计,原型链,或者克隆当前实例;而享元模式是对现有实例的重复使用,比如Java里的整形池、字符串池(String Pool),另一个角度,原型是创建型设计模式,而享元是结构型设计模式。

所以可以这样理解记忆,如果希望重用现有实例,再加工或者直接克隆,属于实例创建是原型模式,如果直接使用,属于结构型享元模式。

原型模式一般用于克隆生成实例,会结合工厂模式使用,换一种角度考虑,其实和一种叫写时复制(copy-on-write)的技术特别类似,在容器、虚拟化技术中都有应用。

容器技术中,应用级别考虑,内核共用本身就是一种原型设计,同时一个运行的容器分为镜像层和容器层,这里的镜像层可以理解为原型层,在对容器层的数据进行修改时,如果是 update 会把文件复制到容器层 update,如果是新增会在容器层新增,如果删除,会屏蔽镜像层的文件。如果读取,会在由容器层到镜像层自上而下的查找。

虚拟化技术中,如果系统级别考虑,硬件资源共享本身也是一种原型设计,同时OpenStack 利用其 Glance组件,把虚机的分为原始后端盘和增量前端盘,这里的原始后端盘即可以理解为原型盘,一个标准系统镜像,在创建的虚机里。当修改系统文件的时候,会复制原始的文件到增量盘修改。创建的文件只在增量盘创建

原型设计有许多和抽象工厂建造者一样的效果:它隐藏了具体的实例类,因此减少了实例的数目。可以在运行时刻增加和删除实例,通过改变结构、改变值来指向新的实例。减少了子类的构造,也减少复杂实例的重复构建。

如何实现原型?

整体上讲,原型设计模式的应用有两种:

  • 第一种是通过改变结构、改变值来指向新的实例,把实例中公共的部分作为原型,比如 Js 实例中的一些事件函数,这种情况下,可以减少对同类实例中相同部分的创建,减少内存开销,比如 JS 的原型设计
  • 另一种情况是复杂实例的克隆,这种情况一般用于实例构建需要依赖外部数据,涉及第三方数据,比如数据库,IO 读取等情况,构建相对复杂耗时,所以把整个实例作为原型,重写克隆方法实现对实例的克隆

这里那 JS 原生的原型模式 Demo 来看下。

代码语言:javascript
复制
    // 在原型基础上的新实例
    function Person() {
         this.sex = 'man'; 
      }
    // 原型实例   
    Person.prototype = {
          name: 'Nicholas',
          age: 29,
          job: "Software Engineer",
          sayName : function () {
              console.log(this.name)
          }
      }
      var person1 = new Person(); 
      person1.name = 'liruilong';
      console.log(person1)
     
      var person2 = new Person();
      console.log(person2)
      console.log(person1.sayName == person2.sayName);//true

Person.prototype指向原型,而 Person 本身即为原型扩展后的实例。通对 name 的赋值也可以看到,修改属性会直接覆盖原型的值。

代码语言:javascript
复制
[object Object] {
  age: 29,
  job: "Software Engineer",
  name: "liruilong",
  sayName: function () {
              window.runnerWindow.proxyConsole.log(this.name)
          },
  sex: "man"
}
[object Object] {
  age: 29,
  job: "Software Engineer",
  name: "Nicholas",
  sayName: function () {
              window.runnerWindow.proxyConsole.log(this.name)
          },
  sex: "man"
}
true

关于重写克隆方法的原型设计模式利用,可以通过深度遍历,或者序列化的方式实现,感兴趣小伙伴可以下去了解下

建造者

为什么需要建造者

建造者设计模式也被称为为生成器模式,个人觉得,这是编码中使用最多的一个设计模式了,用通俗的话讲,即使用多个简单的实例一步一步构建成一个复杂的实例,为什么需要建造者,通过建造者,可以将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示

通过一个最简单的建造者设计模式应用体会下,比如一个 Bean 的生成,涉及的属性较多,构造函数很不方便,可以使用建造者设计模式,通 setter 方法对属性赋值,返回一个 this 的方式(区别于传统的 setter 方法),每个 setter 方法的调用即是一次构建,返回的是不同的实例(属性不同)。

代码语言:javascript
复制
public Bulider setD(int d) {
            this.d = d;
            return this;
}

复杂一点的,比如 Java 中 SpringBoot 系列安全组件 SpringSecurity配置的生成,即通过建造者的设计模式构建了一个配置类,可以有选择通过构建的方式配置鉴权,授权,比如表单验证等,以及各种事件的处理器。关于配置的 Demo 感兴趣小伙伴可以了解下。

开发中,需要生成一些复杂多变的东西,比如 doc,excel 等,设计策略较多,利用策略模式往往会有重叠的部分,就可以使用建造者模式来实现。比如这是一个生产中生成复杂 Excel 的 Demo:https://liruilong.blog.csdn.net/article/details/113191009

如何实现建造者

这个一个通过建造者设计模式生成实例的 Demo。这样写的好处:

  • 一是相比传统的先使用构造函数生成实例,然后通过 setter 方法一个个赋值来讲,他是一个整体,即整个构建过程是一个链式调用,传统的构造方式,构造过程被分配到了个多个 setter 方法调用中,在构造过程中 Bean 可能处于一种不一致的状态,真的出现这种情况往往很难调试
  • 另一种情况,这样的写法,可以把一个 Bean 定义为不可变得,而使用传统的形式则很难实现。
代码语言:javascript
复制
package com.liruilong.common.demo;
 
/**
 * @Auther Liruilong
 * @Date 2020/8/4 12:34
 * @Description:
 */
public class Demo {
    private final int a;
    private final int d;
    private Demo(Bulider bulider) {
        a = bulider.a;
        d = bulider.d;
    }
    public static class Bulider {
        private int a;
        private int d;
        public Bulider(int a) {
            this.a = a;
        }
        public Demo build() {
            return new Demo(this);
        }
        public Bulider setA(int a) {
            this.a = a;
            return this;
        }
        public Bulider setD(int d) {
            this.d = d;
            return this;
        }
    }
 
    public static void main(String[] args) {
        Demo build = new Demo.Bulider(3).setD(4).build();
    }
 
}

嗯,创建型前三个设计模式就和小伙伴们分享到这里,之前有时间会陆续分享剩下的 2 个,生活加油

博文参考

  • 《Java 并发编程详解》
  • 《JavaScript 高级程序设计》(第 3 版)
  • 《Effective Java》 (中文版第 3 版)
  • 《Head First 设计模式》(中文版)
  • 《设计模式 可复用面向对象软件的基础》(中文版)
  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/f7b06983c65c4a26ca15929ef
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券