设计模式-单例模式

161 阅读10分钟

单例模式简介

单例模式是指某个变量在内存中只会创建一次保证一个类只有一个实例,并提供一个访问它的全局访问点,如计算机系统中的线程池、全局缓存、日志对象、window对象等被设计成单例,因为这些应用或多或少具有资源管理器的功能,单例模式的优势在于通过控制实例产生的数量达到控制和节省资源的目的

使用单例的好处

  • 节省创建对象的相关时间和内存等对的开销;
  • 避免了重复创建对象带来的内存空间和性能的浪费,避免过多的实例对象的重复创建,可以进行全局访问;

特点

  1. 类只有一个实例
  2. 全局可访问该实例
  3. 可自行实例化(主动实例化)
  4. 可推迟初始化,即延迟执行(与静态类或对象的区别)

单一职责原则:某个函数只负责一个功能;从功能的方面来说
单例:指单个实例;对象实例方面来说的;--全局必须要用唯一的实例

在前端中可以通过创建全局对象来满足单例模式的特点:唯一可以全局访问,但也有不好维护和命名空间污染的问题,可以通过命名空间闭包封装ES6中的const/symbol进行变相解决

实现思路

一般情况下,当创建了一个类(本质上是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象,且实例对象是单独独立的,没有任何瓜葛,但是单例模式最终需要实现的是无论尝试去创建多少次,都会返回第一次创建的那个唯一的一个实例,因此就需要使得构造函数具备判断自己是否已经创建过一个实例的能力

实现方式

  1. 饿汉模式:提前将某个变量初始化好,在使用时直接拿来用即可,但当这个变量使用评率不高,且占用内存比较大时就不太适合使用饿汉模式了;
  2. 懒汉模式:只有在程序需要某个变量时才会进行初始化,避免了不必要的内存浪费等问题;
  3. 拓展
    • 在实现单例模式时,可以通过关键字进行限制,如为了不让外部访问局部变量和防止外部进行实例化可以通过private关键字进行限制,保证指定逻辑只能在当前局部范围内使用;
    • 实现懒汉模式最常用的方式就是双重检查锁(Check-Lock-Check)

思路分析

  • 定义是:保证一个类有且仅有一个实例,并提供一个访问它的全局访问点,这个单一实例可以在应用程序中共享,因此单例非常适合管理应用程序中的全局状态;
  • 思路一:用闭包返回一个实例,对这个实例进行条件判断,有就返回,没有就初始化,这样在每次new的时候就只能得到一个实例了。
    一个类只允许创建一个对象(或者实例) - 使用单例模式

注意点

  1. 当进行单例实例new导出是可以使用Object.freeze方法进行冻结全局共享的实例,使用该方法可以确保消费的代码不能修改Singleton,无法修改或添加冻结实例上的属性,这样降低了意外覆盖Singleton上的值的风险;
let instance;
let counter = 0;
class Counter {
  constructor() {
    if (instance) {
      throw new Error("You can only create one instance!");
    }
    instance = this;
  }
  getInstance() {
    return this;
  }
  getCount() {
    return counter;
  }
  increment() {
    return ++counter;
  }
  decrement() {
    return --counter;
  }
}
const singletonCounter = Object.freeze(new Counter());
export default singletonCounter;

高阶版单例

基础概念浅析

进程、线程和协程
  1. 协程是一种比线程更加轻量级的存在,一个进程可以有多个线程,同样一个线程也可以有多个协程,协程的创建和切换完全由用户来决定;
  2. 线程和进程是完全由操作系统分配的,对操作系统来说线程是最小的执行单元,进程是最小的资源管理单元;
  3. 协程和线程的关系与区别
    • 协程所需要的内存通常只有2kb,而线程则需要1MB,内存消耗更少;
    • 开销方面,线程的创建需要向操作心跳系统申请资源,并且在销毁时需要将资源进行归还操作,而协程的创建和销毁时由go语言在运行时自己管理的,因此开销相对更低;
    • 切换开销方面,线程的调度方式是抢占式的,在一个线程的执行时间超过分配给他的时间片时就会被其他线程抢占,而协程的调度是协同式的,不会直接与系统打交道;

双重检查锁

目的是防止多个调用时恰好错过普通检查锁而创建多个实例,如go语言中的多个协程同时初始化时创建多个实例,因此需要双重检查机制进行限制;

image.png 分析

  • check: 19 行先判断实例是否为空,为空说明需要初始化 instance 对象,非空直接返回。
  • lock: 20 行加一个互斥锁,目的是防止多个 goroutine 同时初始化 instance,21 行解锁。
  • check: 22 行再次判断实例是否为空,再次判断的原因是:如果多个 goroutine 同时通过第一次 check,而且其中一个 goroutine 首先通过了第二次 check 并初始化了 instance 实例,那么剩余的 goroutine 的就不必再去初始化实例了。

instanceof判断是否使用new关键字进行了对象的实例化

function User(data) {
  if(!(this instanceof User)) return;
  if(!User._instance){
    this.title = data
    User._instance = this
  }
  return User._instance
}

const user1 = new User(111)
const user2 = new User(222)
console.log(user1,user2,'user1,user2=========')
// User { title: 111 } User { title: 111 } user1,user2=========

Proxy实现单例模式

利用ES6的代理(proxy),可以让单例实现和类本身进行解耦。在ES6之前的版本不支持代理(proxy)的特性,需要手动实现(即其他版本方式实现单例模式的方案)

class CreateDiv {
  constructor(html) {
    this.html = html;
    this.init();
  }
  init() {
    let div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }
}

function singletonProxy(className) {
  let instance = null;
  return new Proxy(className, {
    /**
      * @description construct()方法用于拦截new命令,可以接受三个参数
      * @description construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`
      * @description construct()方法中的this指的是类本身,而非实例对象
      * @param target 目标对象 拦截的是构造函数 所以目标对象必须是函数
      * @param args 构造函数的参数数组
      * @param newTarget 创造实例对象时,new命令作用的构造函数
      * @notice construct()方法返回的必须是一个对象,否则会报错(如直接return 1)
          * @example Uncaught TypeError: 'construct' on proxy: trap returned non-object ('1')
      */
    construct(target, args) {
      // 内部类
      class ProxyClass {
        constructor() {
          if (instance === null) {
            instance = new target(...args);
          }
          return instance;
        }
      }
      return new ProxyClass();
    },
  });
}

let createSothx = singletonProxy(CreateDiv)
let sothx1 = new createSothx('Lbxin1')
let sothx2 = new createSothx('Lbxin2')
console.log(sothx1, sothx2)
// CreateDiv {html: 'Lbxin1'} CreateDiv {html: 'Lbxin2'}
// sothx1 === sothx2 //true

案例分析

Vuex中的单例模式

Vuex使用的是单一状态树,用一个对象就包含了全部的应用层状态,至此它便作为一个唯一数据源(SSOT)而存在,在Vue中,一个Vue实例只能对应一个Store,Vue内部使用Vuex是通过Vue.use()方法使用的,Vuex内部实现了install方法,该方法在插件安装时会被调用,从而将Store注入到Vue实例中去

let Vue 
// ...

export function install (_Vue) {
  // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若没有,则为这个Vue实例对象install一个唯一的Vuex
  Vue = _Vue
  // 将Vuex的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue)
}

log日志统计

public class Logger {
  private FileWriter writer;
  
  public Logger() {
    File file = new File("/Users/mrHandson/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message) {
    writer.write(message);
  }
}

// Logger类的应用示例:
public class UserController {
  private Logger logger = new Logger();
  
  public void login(String username, String password) {
    // ...省略业务逻辑代码...
    logger.log(username + " logined!");
  }
}

public class OrderController {
  private Logger logger = new Logger();
  
  public void create(OrderVo order) {
    // ...省略业务逻辑代码...
    logger.log("Created an order: " + order.toString());
  }
}

问题分析:后续日志功能继续继承于Logger时,会向同一份日志统计文件里输出内容,会存在日志覆盖的情况,因为Logger是一个共享类的缘故;

通用单例模式封装

var getInstance = function (fn) {
  var result;
  return function () {
    return result || (result = fn.call(this, arguments));
  }
};

引用示例

var createMask = function () {
  var div = document.createElement("div");
  div.innerHTML = "遮罩层";
  div.style.display = 'none';
  document.body.appendChild(div);
  return div;
};
// 创建iframe
var createIframe = function () {
  var iframe = document.createElement("iframe");
  document.body.appendChild(iframe);
  return iframe;
};
// 获取实例的封装代码
var getInstance = function (fn) {
  var result;
  return function () {
    return result || (result = fn.call(this, arguments));
  }
};
// 测试创建遮罩层
var createSingleMask = getInstance(createMask);
document.querySelector("body").onclick = function () {
  var mask = createSingleMask();
  mask.style.display = "block";
};
// 测试创建iframe
var createSingleIframe = getInstance(createIframe);
document.querySelector("body").onclick = function () {
  var frame = createSingleIframe();
  frame.src = "https://www.baidu.com/";
};

创建标签

不使用单例
let timePro = {}
//更改this指向为timePro 避免向window添加新熟悉
var CreateDiv = (function () {
  var num = 1;
  var CreateDiv = function (html) {
    this.html = html += num++;
    // 外部this更改 内部this调用时也许通过更改
    init.call(this);
  };
  function init() {
    var div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }
  return CreateDiv.bind(timePro,...arguments);
})();

as = new CreateDiv(12313312313)
aa = new CreateDiv(12313312313)
as === aa //false
闭包版本
let timePro = {}
//更改this指向为timePro 避免向window添加新熟悉
var CreateDiv = (function () {
  var instance,
    num = 1;
  var CreateDiv = function (html) {
    this.html = html += num++;
    // 外部this更改 内部this调用时也许通过更改
    init.call(this);
    // 判断逻辑加载执行逻辑以后 避免创建后调用逻辑被拦截
    if (instance) {
      return instance;
    }
    return (instance = this);
  };
  function init() {
    var div = document.createElement("div");
    div.innerHTML = this.html;
    document.body.appendChild(div);
  }
  return CreateDiv.bind(timePro,...arguments);
})();
let aa = new CreateDiv(12);
let as = new CreateDiv(12);
as === aa; // true

案例二

function singleton() {
  var init = function (name) {
    this.name = name;
    this.getName = getName;
  }
  let getName = function () {
    return this.name
  }
  let instance;
  return function (arg) {
    if (!instance) {
      instance = new init(arg);
    }
    return instance;
  }
}

let single = singleton()
console.log(single('12') === single('123'), '=========', single('12').getName(), single('123').getName())
//true ========= init { name: '12', getName: [Function: getName] } 12
惰性版本👍🏻👍🏻👍🏻
const CreateSingleV1 = function (fn) {
  let _instance = null;
  return function () {
    //只有在调用的时候才创建
    return _instance || (_instance = fn.apply(this, arguments))
  }
}

//我们使用惰性单例模式实现了一个弹框demo
const createModal = function (message) {
  const box = document.createElement('div')
  box.innerHTML = message
  box.className = 'singleton'
  document.body.appendChild(box)
  return box
}
const createAlertMessaeg = CreateSingleV1(createModal)
createAlertMessaeg('设计模式-单例')

优缺点分析

优点
  • 可以减少不必要的开销,不必要的内存占用;
缺点
  • 通过单例模式创建的实例是全局的,当有过多的项目组件公用这一份全局实例时会存在维护成本加大,牵一发动全身的后果;
  • 简单的单例是可以通过js中的对象来实现的,因为对象的传递是值传递,也会有共享的效果;
  • 单例堆OOP的支持度不高,OOP的特性有继承、封装、抽象和多态,单例只对于封账有较好的体现,其他三个的支持度都不高;
  • 单例对于拓展性支持度也不高:单例只能创建一个实例,当后期需要创建两个实例时就需要更改挺多底层代码逻辑,如数据库连接池和线程池等,项目后期有可能再添加一个连接池用于慢请求的执行,用于加快数据相应,这时单例就显得比较尴尬了;

推荐

golang设计模式-单例
单例模式的不同方式实现
阮一峰-es6-proxy
Learning JavaScript Design Patterns