[js话设计模式]单例模式

设计模式的目的

  • 代码重用性,即相同的代码,不用多次重复编写

  • 可读性。编程规范性,便于其他程序员的阅读和理解

  • 可拓展性。当需要增加新的功能时,非常地方便

  • 可靠性、增加新的功能后,对原来的功能没有影响

  • 高内聚、低耦合。不同模块之间权责分明。

七大设计原则

1.  开闭原则(Open-Closed Principle, OCP)

定义软件实体应当对扩展开放,对修改关闭。软件系统中包含的各种组件,例如模块(Modules)、类(Classes)以及功能(Functions)等等,应该在不修改现有代码的基础上,去扩展新功能。开闭原则中原有“开”,是指对于组件功能的扩展是开放的,是允许对其进行功能扩展的;开闭原则中“闭”,是指对于代码的修改是封闭的,即不应该修改原有的代码。

问题由来在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。这就对整个系统的影响特别大,这也充分展现出了系统的耦合性如果太高,会大大的增加后期的扩展,维护。为了解决这个问题,故人们总结出了开闭原则。解决开闭原则的根本其实还是在解耦合。所以,我们面向对象的开发,我们最根本的任务就是解耦合。

解决方法当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

小结开闭原则具有理想主义的色彩,说的很抽象,它是面向对象设计的终极目标。其他几条原则,则可以看做是开闭原则的实现。我们要用抽象构建框架,用实现扩展细节。

2.  单一职责原则(Single Responsibility Principle)

**定义:**一个类,只有一个引起它变化的原因。即:应该只有一个职责。

每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。例如:要实现逻辑和界面的分离。需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都需要遵循这一重要原则。

**问题由来:**类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

**解决方法:**分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

3.  里氏替换原则(Liskov Substitution Principle)

**定义:**子类型必须能够替换掉它们的父类型。注意这里的能够两字。有人也戏称老鼠的儿子会打洞原则。

**问题由来:**有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

**解决方法:**类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法

**小结:**所有引用父类的地方必须能透明地使用其子类的对象。子类可以扩展父类的功能,但不能改变父类原有的功能,即:子类可以实现父类的抽象方法,子类也中可以增加自己特有的方法,但不能覆盖父类的非抽象方法。当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

4.  迪米特法则(Law Of Demeter)

**定义:**迪米特法则又叫最少知道原则,即:一个对象应该对其他对象保持最少的了解。如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。简单定义为只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

**问题由来:**类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。迪米特法则还有一个更简单的定义:只与直接的朋友通信。

**解决方法:**尽量降低类与类之间的耦合。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系。故过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

5.  依赖倒置原则(Dependence Inversion Principle)

**定义:**高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。中心思想是面向接口编程

**问题由来:**类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

**解决方法:**将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

在实际编程中,我们一般需要做到如下3点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。

  • 变量的声明类型尽量是抽象类或接口。

  • 使用继承时遵循里氏替换原则。

采用依赖倒置原则尤其给多人合作开发带来了极大的便利,参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。

**小结:**依赖倒置原则就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

6.  接口隔离原则(Interface Segregation Principle)

**定义:**客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

**问题由来:**类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法

解决方法:

  • 使用委托分离接口。

  • 使用多重继承分离接口。

  • 将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

**小结:**在代码编写过程中,运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。设计接口的时候,只有多花些时间去思考和筹划,就能准确地实践这一原则。

7.  合成/聚合原则(Composite/Aggregate Reuse Principle,CARP)

**定义:**也有人叫做合成复用原则,及尽量使用合成/聚合,尽量不要使用类继承。换句话说,就是能用合成/聚合的地方,绝不用继承。

为什么要尽量使用合成/聚合而不使用类继承?

  • 对象的继承关系在编译时就定义好了,所以无法在运行时改变从父类继承的子类的实现

  • 子类的实现和它的父类有非常紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化

  • 当你复用子类的时候,如果继承下来的实现不适合解决新的问题,则父类必须重写或者被其它更适合的类所替换,这种依赖关系限制了灵活性,并最终限制了复用性。

总结:这些原则在设计模式中体现的淋淋尽致,设计模式就是实现了这些原则,从而达到了代码复用、增强了系统的扩展性。所以设计模式被很多人奉为经典。我们可以通过好好的研究设计模式,来慢慢的体会这些设计原则。




模式特点

单例模式:限制类实例化次数只能一次,一个类只有一个实例,并提供一个访问它的全局访问点。

单例模式是创建型设计模式的一种。

针对全局仅需一个对象的场景,例如

  • 线程池

  • 数据库连接池

  • 全局缓存

  • js中的window顶层对象

  • Vue中的vuex

在《headFirst设计模式》一书中,相关的定义在170页。

[js话设计模式]单例模式
img

特点:

  • 类只有一个实例

  • 全局可访问该实例

  • 自行实例化(主动实例化)

  • 可推迟初始化,即延迟执行(与静态类/对象的区别)

// 全局对象
var globaObj = {};

使用全局变量会有以下问题:

  • 命名空间污染(变量名冲突)

  • 维护时不方便管控(容易不小心覆盖)

全局变量问题折中的应对方案:

  • 使用命名空间

  • 闭包封装私有变量(利用函数作用域)

  • ES6的 const/symbol

虽然全局变量可以实现单例,但因其自身的问题,不建议在实际项目中将其作为单例模式的应用,特别是中大型项目的应用中,全局变量的维护该是考虑的成本。




实现

多线程编程语言中,单例模式会涉及同步锁的问题。而 JavaScript 是单线程的编程语言,所以下面暂时忽略有关锁的问题。

基本思路

  • 使用一个变量存储类实例对象(值初始为 null/undefined )。

  • 进行类实例化时,判断类实例对象是否存在

    • 存在则返回该实例
    • 不存在则创建类实例后返回。
  • 多次调用类生成实例方法,返回同一个实例对象。




实现方式

使用构造函数的默认属性

function A(name{
    // 如果已存在对应的实例
    if (typeof A.instance === 'object') {
        return A.instance
    }
    //否则正常创建实例
    this.name = name

    // 缓存
    A.instance = this
    return this
}
var a1 = new A()
var a2 = new A()
console.log(a1 === a2)  //true

借助闭包

var Head = (function ({
    var HeadClass = function ({ }; // 声明HeadClass对象,无法在外部直接调用
    var instance; // 声明一个instance对象
    return function ({
        if (instance) { // 如果已存在 则返回instance
            return instance;
        }
        instance = new HeadClass() // 如果不存在 则new一个
        return instance;
    }
})();
var a = Head();
var b = new Head();
console.log(a===b) // true
var a = HeadClass(); // 报错,HeadClass is not defined

立即执行函数

var A;
(function (name{
    var instance;
    A = function (name{
        if (instance) {
            return instance
        }

        //赋值给私有变量
        instance = this

        //自身属性
        this.name = name
    }
}());
A.prototype.pro1 = "from protptype1"

var a1 = new A('a1')
A.prototype.pro2 = "from protptype2"
var a2 = new A('a2')

console.log(a1.name)
console.log(a1.pro1) //from protptype1
console.log(a1.pro2) //from protptype2
console.log(a2.pro1) //from protptype1
console.log(a2.pro2) //from protptype2

实现类别

简单版

let Singleton = function(name{
    this.name = name;
    this.instance = null;
}

Singleton.prototype.getName = function({
    console.log(this.name);
}

Singleton.getInstance = function(name{
    if (this.instance) {
        return this.instance;
    }
    return this.instance = new Singleton(name);
}

let Winner = Singleton.getInstance('Winner');
let Looser = Singleton.getInstance('Looser');

console.log(Winner === Looser); // true
console.log(Winner.getName());  // 'Winner'
console.log(Looser.getName());  // 'Winner'

在函数 Singleton 中定义一个 getInstance() 方法来管控单例,并创建返回类实例对象,而不是通过传统的 new 操作符来创建类实例对象。

存在问题:

  1. 不够“透明”,无法使用 new 来进行类实例化,需约束该类实例化的调用方式:Singleton.getInstance(...);
  2. 管理单例的操作,与对象创建的操作,功能代码耦合在一起,不符合 “单一职责原则”




“透明版” 单例模式

实现 “透明版” 单例模式,意图解决:统一使用 new 操作符来获取单例对象, 而不是 Singleton.getInstance(...)

let CreateSingleton = (function(){
    let instance;
    return function(name{
        if (instance) {
            return instance;
        }
        this.name = name;
        return instance = this;
    }
})();
CreateSingleton.prototype.getName = function({
    console.log(this.name);
}

let Winner = new CreateSingleton('Winner');
let Looser = new CreateSingleton('Looser');

console.log(Winner === Looser); // true
console.log(Winner.getName());  // 'Winner'
console.log(Looser.getName());  // 'Winner'

“透明版”单例模式解决了不够“透明”的问题,我们又可以使用 new 操作符来创建实例对象。


“代理版“ 单例模式

通过“代理”,将管理单例操作,与对象创建操作进行拆分,实现更小的粒度划分,符合“单一职责原则”

let ProxyCreateSingleton = (function(){
    let instance;
    return function(name{
        // 代理函数仅作管控单例
        if (instance) {
            return instance;
        }
        return instance = new Singleton(name);
    }
})();

// 独立的Singleton类,处理对象实例
let Singleton = function(name{
    this.name = name;
}
Singleton.prototype.getName = function({
    console.log(this.name);
}

let Winner = new PeozyCreateSingleton('Winner');
let Looser = new PeozyCreateSingleton('Looser');

console.log(Winner === Looser); // true
console.log(Winner.getName());  // 'Winner'
console.log(Looser.getName());  // 'Winner'

惰性单例模式(懒汉)

惰性单例,意图解决:需要时才创建类实例对象。

对于懒加载的性能优化,想必前端开发者并不陌生。

惰性单例就是解决 “按需加载” 的问题。

需求:页面弹窗提示,多次调用,都只有一个弹窗对象,只是展示信息内容不同。

开发这样一个全局弹窗对象,可以应用单例模式。

为了提升它的性能,可以让它在需要调用时再去生成实例,创建 DOM 节点。

let getSingleton = function(fn{
    var result;
    return function({
        return result || (result = fn.apply(thisarguments)); 
        // 确定this上下文并传递参数
    }
}
let createAlertMessage = function(html{
    var div = document.createElement('div');
    div.innerHTML = html;
    div.style.display = 'none';
    document.body.appendChild(div);
    return div;
}

let createSingleAlertMessage = getSingleton(createAlertMessage);
document.body.addEventListener('click'function(){
    // 多次点击只会产生一个弹窗
    let alertMessage = createSingleAlertMessage('您的知识需要付费充值!');
    alertMessage.style.display = 'block';
})

代码中演示是一个通用的 “惰性单例” 的创建方式,如果还需要 createLoginLayer 登录框, createFrameFrame框, 都可以调用 getSingleton(...) 生成对应实例对象的方法。

适用场景

“单例模式的特点,意图解决:维护一个全局实例对象。”

  1. 引用第三方库(多次引用只会使用一个库引用,如 jQuery)
  2. 弹窗(登录框,信息提升框)
  3. 购物车 (一个用户只有一个购物车)
  4. 全局态管理 store (Vuex / Redux)

项目中引入第三方库时,重复多次加载库文件时,全局只会实例化一个库对象,如 jQuerylodashmoment …, 其实它们的实现理念也是单例模式应用的一种:

// 引入代码库 libs(库别名)
if (window.libs != null) {
  return window.libs;    // 直接返回
else {
  window.libs = '...';   // 初始化
}

如图,vuex就是一个典型的单例模式。

[js话设计模式]单例模式

优缺点

  • 优点:适用于单一对象,只生成一个对象实例,避免频繁创建和销毁实例,减少内存占用。
  • 缺点:不适用动态扩展对象,或需创建多个相似对象的场景。

《参考资料》

vuex从使用到原理解析https://www.imooc.com/article/291242

《headFirst设计模式》


原文始发于微信公众号(豆子前端):[js话设计模式]单例模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/57035.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!