JavaScript设计模式———总结
设计模式总结
设计原则和编程技巧
每种设计模式都是为了让代码迎合其中一个或多个原则而出现的,它们本身已经融入了设计模式之中,给面向对象编程指明了方向。
设计原则通常指的是单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、合成复用原则和最少知识原则。
单一职责原则(SRP)
定义
就一个类而言,应该仅有一个引起它变化的原因。
在 JavaScript 中,需要用到类的场景并不太多,单一职责原则更多地是被运用在对象或者方法级别上。
单一职责原则( SRP)的职责被定义为“引起变化的原因”。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
此时,这个方法通常是一个不稳定的方法,修改代码总是一件危险的事情,特别是当两个职责耦合在一起的时候,一个职责发生变化可能会影响到其他职责的实现,造成意想不到的破坏,这种耦合性得到的是低内聚和脆弱的设计。
因此, SRP 原则体现为:一个对象(方法)只做一件事情。
设计模式中体现
SRP 原则在很多设计模式中都有着广泛的运用,例如代理模式、迭代器模式、单例模式和装饰者模式。
-
代理模式
图片预加载的例子。通过增加虚拟代理的方式,把预加载图片的职责放到代理对象中,而本体仅仅负责往页面中添加 img 标签,这也是它最原始的职责。 -
迭代器模式
在迭代器模式中,分离对数据迭代后得操作,分离迭达器和数据操作的功能。 -
单例模式
单例模式中,将创建单例对象和对象实际动作分离; -
装饰者模式
使用装饰者模式的时候,我们通常让类或者对象一开始只具有一些基础的职责,更多的职责在代码运行时被动态装饰到对象上面。装饰者模式可以为对象动态增加职责,从另一个角度来看,这也是分离职责的一种方式。
何时应该分离职责
SRP 原则是所有原则中最简单也是最难正确运用的原则之一。
要明确的是,并不是所有的职责都应该一一分离。
-
如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们。比如在 ajax请求的时候,创建 xhr 对象和发送 xhr 请求几乎总是在一起的,那么创建 xhr 对象的职责和发送xhr 请求的职责就没有必要分开。
-
职责的变化轴线仅当它们确定会发生变化时才具有意义,即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们,在代码需要重构的时候再进行分离也不迟。
SRP 原则的优缺点
优点:
降低了单个类或者对象的复杂度,按照职责把对象分解成更小的粒度,这有助于代码的复用,也有利于进行单元测试。当一个职责需要变更的时候,不会影响到其他的职责。
缺点:
最明显的是会增加编写代码的复杂度。当我们按照职责把对象分解成更小的粒度之后,实际上也增大了这些对象之间相互联系的难度。
最少知识原则(LKP)- 迪米特法则
定义
最少知识原则( LKP)说的是一个软件实体应当尽可能少地与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。
减少对象之间的联系
单一职责原则指导我们把对象划分成较小的粒度,这可以提高对象的可复用性。但越来越多的对象之间可能会产生错综复杂的联系,如果修改了其中一个对象,很可能会影响到跟它相互引用的其他对象。**对象和对象耦合在一起,有可能会降低它们的可复用性。**在程序中,对象的“朋友”太多并不是一件好事,“城门失火,殃及池鱼”和“一人犯法,株连九族”的故事时有发生。
**最少知识原则要求我们在设计程序时,应当尽量减少对象之间的交互。**如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系。常见的做法是引入一个第三者对象,来承担这些对象之间的通信作用。如果一些对象需要向另一些对象发起请求,可以通过第三者对象来转发这些请求。
设计模式中体现
最少知识原则在设计模式中体现得最多的地方是中介者模式和外观模式
- 中介者模式
中介者模式很好地体现了最少知识原则。通过增加一个中介者对象,让所有的相关对象都通过中介者对象来通信,而不是互相引用。所以,当一个对象发生改变时,只需要通知中介者对象即可。
- 外观模式
外观模式在 JavaScript 中的使用场景并不多
外观模式主要是为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使子系统更加容易使用。
外观模式的作用是对客户屏蔽一组子系统的复杂性。外观模式对客户提供一个简单易用的高层接口,高层接口会把客户的请求转发给子系统来完成具体的功能实现。大多数客户都可以通过请求外观接口来达到访问子系统的目的。但在一段使用了外观模式的程序中,请求外观并不是强制的。如果外观不能满足客户的个性化需求,那么客户也可以选择越过外观来直接访问子系统。
外观模式容易跟普通的封装实现混淆。这两者都封装了一些事物,**但外观模式的关键是定义一个高层接口去封装一组“子系统”。**子系统在 C++或者 Java 中指的是一组类的集合,这些类相互协作可以组成系统中一个相对独立的部分。在JavaScript 中我们通常不会过多地考虑“类”,如果将外观模式映射到 JavaScript 中,这个子系统至少应该指的是一组函数的集合。
例子:
var A = function(){
a1();
a2();
}
var B = function(){
b1();
b2();
}
var facade = function(){
A();
B();
}
facade();
外观模式和最少知识原则之间的关系
- 为一组子系统提供一个简单便利的访问入口。
- 隔离客户与复杂子系统之间的联系,客户不用去了解子系统的细节。
从第二点来,外观模式是符合最少知识原则的。
封装在最少知识原则中的体现
封装在很大程度上表达的是数据的隐藏。一个模块或者对象可以将内部的数据或者实现细节隐藏起来,只暴露必要的接口 API 供外界访问。对象之间难免产生联系,当一个对象必须引用另外一个对象的时候,我们可以让对象只暴露必要的接口,让对象之间的联系限制在最小的范围之内。
同时,封装也用来限制变量的作用域。在 JavaScript 中对变量作用域的规定是
- 变量在全局声明,或者在代码的任何位置隐式申明(不用 var),则该变量在全局可见;
- 变量在函数内显式申明(使用 var),则在函数内可见。
把变量的可见性限制在一个尽可能小的范围内,这个变量对其他不相关模块的影响就越小,变量被改写和发生冲突的机会也越小。这也是广义的最少知识原则的一种体现。
最少知识原则也叫迪米特法则( Law of Demeter, LoD)
优缺点:
虽然遵守最小知识原则减少了对象之间的依赖,但也有可能增加一些庞大到难以维护的第三者对象。跟单一职责原则一样,在实际开发中,是否选择让代码符合最少知识原则,要根据具体的环境来定。
开放-封闭原则
定义
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。
当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。
最佳实践(帮助我们编写遵守开放-封闭原则的代码)
开放-封闭原则是一个看起来比较虚幻的原则,并没有实际的模板教导我们怎样亦步亦趋地实现它。但我们还是能找到一些让程序尽量遵守开放-封闭原则的规律,最明显的就是找出程序中将要发生变化的地方,然后把变化封装起来。
通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经被封装好的,那么替换起来也相对容易。而变化部分之外的就是稳定的部分。在系统的演变过程中,稳定的部分是不需要改变的。
-
利用对象的多态性
用对象的多态性消除条件分支,过多的条件分支语句是造成程序违反开放-封闭原则的一个常见原因。每当需要增加一个新的 if 语句时,都要被迫改动原函数。把 if 换成 switch-case 是没有用的,这是一种换汤不换药的做法。实际上,每当我们看到一大片的 if 或者 swtich-case 语句时,第一时间就应该考虑,能否利用对象的多态性来重构它们。
利用对象的多态性来让程序遵守开放-封闭原则,是一个常用的技巧。 -
放置挂钩
放置挂钩( hook)也是分离变化的一种方式。我们在程序有可能发生变化的地方放置一个挂钩,挂钩的返回结果决定了程序的下一步走向。这样一来,原本的代码执行路径上就出现了一个分叉路口,程序未来的执行方向被预埋下多种可能性。 -
使用回调函数
在 JavaScript 中,函数可以作为参数传递给另外一个函数,这是高阶函数的意义之一。在这种情况下,我们通常会把这个函数称为回调函数。在 JavaScript 版本的设计模式中,策略模式和命令模式等都可以用回调函数轻松实现。
回调函数是一种特殊的挂钩。**我们可以把一部分易于变化的逻辑封装在回调函数里,然后把回调函数当作参数传入一个稳定和封闭的函数中。**当回调函数被执行的时候,程序就可以因为回调函数的内部逻辑不同,而产生不同的结果。
设计模式中体现
-
发布-订阅模式
发布-订阅模式用来降低多个对象之间的依赖关系,它可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。当有新的订阅者出现时,发布者的代码不需要进行任何修改;同样当发布者需要改变时,也不会影响到之前的订阅者。 -
模板方法模式
模板方法模式是一种典型的通过封装变化来提高系统扩展性的设计模式。在一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽出来放到父类的模板方法里面;而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加新的功能,并不需要改动抽象父类以及其他的子类,这也是符合开放-封闭原则的。 -
策略模式
策略模式和模板方法模式是一对竞争者。在大多数情况下,它们可以相互替换使用。模板方法模式基于继承的思想,而策略模式则偏重于组合和委托。
策略模式将各种算法都封装成单独的策略类,这些策略类可以被交换使用。策略和使用策略的客户代码可以分别独立进行修改而互不影响。我们增加一个新的策略类也非常方便,完全不用修改之前的代码。
- 代理模式
- 职责链模式
接受第一次愚弄
让程序一开始就尽量遵守开放-封闭原则,并不是一件很容易的事情。一方面,我们需要尽快知道程序在哪些地方会发生变化,这要求我们有一些“未卜先知”的能力。另一方面,留给程序员的需求排期并不是无限的,**所以我们可以说服自己去接受不合理的代码带来的第一次愚弄。****在最初编写代码的时候,先假设变化永远不会发生,这有利于我们迅速完成需求。****当变化发生并且对我们接下来的工作造成影响的时候,可以再回过头来封装这些变化的地方。**然后确保我们不会掉进同一个坑里
开放- 封闭原则的相对性
让程序保持完全封闭是不容易做到的。就算技术上做得到,也需要花费太多的时间和精力。而且让程序符合开放封闭原则的代价是引入更多的抽象层次,更多的抽象有可能会增大代码的复杂度。
更何况,有一些代码是无论如何也不能完全封闭的,总会存在一些无法对其封闭的变化。作为程序员,我们可以做到的有下面两点。
- 挑选出最容易发生变化的地方,然后构造抽象来封闭这些变化。
- 在不可避免发生修改的时候,尽量修改那些相对容易修改的地方。拿一个开源库来说,修改它提供的配置文件,总比修改它的源代码来得简单。
接口和面向接口编程
当我们谈到接口的时候,通常会涉及以下几种含义,下面先简单介绍。
- 我们经常说一个库或者模块对外提供了某某 API 接口。通过主动暴露的接口来通信,可以隐藏软件系统内部的工作细节。这也是我们最熟
悉的第一种接口含义。 - 第二种接口是一些语言提供的关键字,比如 Java 的 interface。 interface 关键字可以产生一个完全抽象的类。
- 第三种接口即是我们谈论的“面向接口编程”中的接口,接口的含义在这里体现得更为抽象。
用《设计模式》中的话说就是:接口是对象能响应的请求的集合。
JAVA的abstract和interface关键字
abstract和interface为接口编程提供支持
- 向上转型。(可确保类型可替换,以表现对象的多态性)
- 建立一些契约。 (可确保实现相应方法)
不关注对象的具体类型,而仅仅针对超类型中的**“契约方法”**来编写程序,可以产生可靠性高的程序,也可以极大地减少子系统实现之间的相互依赖关系:
面向接口编程,而不是面向实现编程。
从过程上来看,“面向接口编程”其实是“面向超类型编程”。当对象的具体类型被隐藏在超类型身后时,这些对象就可以相互替换使用,我们的关注点才能从对象的类型上转移到对象的行为上。“面向接口编程”也可以看成面向抽象编程,即针对超类型中的 abstract 方法编程,接口在这里被当成 abstract 方法中约定的契约行为。这些契约行为暴露了一个类或者对象能够做什么,但是不关心具体如何去做。
JavaScript中接口编程
因为 JavaScript 是一门动态类型语言,类型本身在 JavaScript 中是一个相对模糊的概念。也就是说,不需要利用抽象类或者 interface 给对象进行“向上转型”。除了number、 string、 boolean 等基本数据类型之外,其他的对象都可以被看成“天生”被“向上转型”成了 Object 类型。
因为不需要进行向上转型,接口在 JavaScript 中的最大作用就退化到了检查代码的规范性。
作为一门解释执行的动态类型语言,JavaScript 编译器不会帮我们检查代码的规范性;如果需要检查代码的规范性,我们只有手动编写一些接口检查的代码;
用鸭子类型进行接口检查
鸭子类型是动态类型语言面向对象设计中的一个重要概念。利用鸭子类型的思想,不必借助超类型的帮助,**就能在动态类型语言中轻松地实现本章提到的设计原则:面向接口编程,而不是面向实现编程。**比如,一个对象如果有 push 和 pop 方法,并且提供了正确的实现,它就能被当作栈来使用;一个对象如果有 length 属性,也可以依照下标来存取属性,这个对象就可以被当作数组来使用。如果两个对象拥有相同的方法,则有很大的可能性它们可以被相互替换使用。
在 Object.prototype.toString.call( [] ) === ‘[object Array]’ 被发现之前,我们经常用鸭子类型的思想来判断一个对象是否是一个数组,代码如下:
var isArray = function( obj ){
return obj &&
typeof obj === 'object' &&
typeof obj.length === 'number' &&
typeof obj.splice === 'function'
};
当然在 JavaScript 开发中,总是进行接口检查是不明智的,也是没有必要的,毕竟现在还找不到一种好用并且通用的方式来模拟接口检查,跟业务逻辑无关的接口检查也会让很多JavaScript 程序员觉得不值得和不习惯。
用 TypeScript 编写基于 interface 的命令模式进行接口检查
代码重构
在实际的项目开发中,除了使用设计模式进行重构之外,还有一些常见而容易忽略的细节,这些细节也是帮助我们达到重构目标的重要手段。
虽然有一些重构的目标和手段,但它们都是建议,没有哪些是必须严格遵守的标准。具体是否需要重构,以及如何进行重构,这需要我们根据系统的类型、项目工期、人力等外界因素一起决定。
提炼函数
如果在函数中有一段代码可以被独立出来,那我们最好把这些代码放进另外一个独立的函数中。这是一种很常见的优化工作,这样做的好处主要有以下几点。
- 避免出现超大函数。
- 独立出来的函数有助于代码复用。
- 独立出来的函数更容易被覆写。
- 独立出来的函数如果拥有一个良好的命名,它本身就起到了注释的作用。
合并重复的条件片段
如果一个函数体内有一些条件分支语句,而这些条件分支语句内部散布了一些重复的代码,那么就有必要进行合并去重工作。
把条件分支语句提炼成函数
在程序设计中,复杂的条件分支语句是导致程序难以阅读和理解的重要原因,而且容易导致一个庞大的函数。
合理使用循环
在函数体内,如果有些代码实际上负责的是一些重复性的工作,那么合理利用循环不仅可以完成同样的功能,还可以使代码量更少。
var createXHR = function() {
var xhr;
try {
xhr = new ActiveXObject('MSXML2.XMLHttp.6.0');
} catch (e) {
try {
xhr = new ActiveXObject('MSXML2.XMLHttp.3.0');
} catch (e) {
xhr = new ActiveXObject('MSXML2.XMLHttp');
}
}
return xhr;
};
var xhr = createXHR();
运用循环:
var createXHR = function() {
var versions = ['MSXML2.XMLHttp.6.0ddd', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'];
for (var i = 0, version; version = versions[i++];) {
try {
return new ActiveXObject(version);
} catch (e) {}
}
};
var xhr = createXHR();
提前让函数退出代替嵌套条件分支
许多程序员都有这样一种观念:“每个函数只能有一个入口和一个出口。”现代编程语言都会限制函数只有一个入口。但关于“函数只有一个出口”,往往会有一些不同的看法。
var del = function(obj) {
var ret;
if (!obj.isReadOnly) { // 不为只读的才能被删除
if (obj.isFolder) { // 如果是文件夹
ret = deleteFolder(obj);
} else if (obj.isFile) { // 如果是文件
ret = deleteFile(obj);
}
}
return ret;
};
嵌套的条件分支语句绝对是代码维护者的噩梦,对于阅读代码的人来说,嵌套的 if、 else语句相比平铺的 if、 else,在阅读和理解上更加困难,有时候一个外层 if 分支的左括号和右括号之间相隔 500 米之远。
但实际上,如果对函数的剩余部分不感兴趣,那就应该立即退出。引导阅读者去看一些没有用的 else 片段,只会妨碍他们对程序的理解。
于是我们可以挑选一些条件分支,在进入这些条件分支之后,就立即让这个函数退出。要做到这一点,有一个常见的技巧,即在面对一个嵌套的 if 分支时,我们可以把外层 if 表达式进行反转。重构后的 del 函数如下:
var del = function(obj) {
if (obj.isReadOnly) { // 反转 if 表达式
return;
}
if (obj.isFolder) {
return deleteFolder(obj);
}
if (obj.isFile) {
return deleteFile(obj);
}
};
传递对象参数代替过长的参数列表
有时候一个函数有可能接收多个参数,而参数的数量越多,函数就越难理解和使用。使用该函数的人首先得搞明白全部参数的含义,在使用的时候,还要小心翼翼,以免少传了某个参数或者把两个参数搞反了位置。
这时我们可以把参数都放入一个对象内,然后把该对象传入 相应函数, 相应函数需要的数据可以自行从该对象里获取。现在不用再关心参数的数量和顺序,只要保证参数对应的 key 值不变就可以了
尽量减少参数数量
如果调用一个函数时需要传入多个参数,那这个函数是让人望而生畏的,我们必须搞清楚这些参数代表的含义,必须小心翼翼地把它们按照顺序传入该函数。而如果一个函数不需要传入任何参数就可以使用,这种函数是深受人们喜爱的。在实际开发中,向函数传递参数不可避免,但我们应该尽量减少函数接收的参数数量。
少用三目运算符
有一些程序员喜欢大规模地使用三目运算符,来代替传统的 if、 else。理由是三目运算符性能高,代码量少。不过,这两个理由其实都很难站得住脚。
即使我们假设三目运算符的效率真的比 if、 else 高,这点差距也是完全可以忽略不计的。
同样,相比损失的代码可读性和可维护性,三目运算符节省的代码量也可以忽略不计。
如果条件分支逻辑简单且清晰,这无碍我们使用三目运算符。
但如果条件分支逻辑非常复杂,那我们最好的选择还是按部就班地编写if、 else。 if、 else 语句的好处很多,一是阅读相对容易,二是修改的时候比修改三目运算符周围的代码更加方便
合理使用链式调用
经常使用 jQuery 的程序员相当习惯链式调用方法,在 JavaScript 中,可以很容易地实现方法的链式调用,即让方法调用结束后返回对象自身,如下代码所示:
var User = function() {
this.id = null;
this.name = null;
};
User.prototype.setId = function(id) {
this.id = id;
return this;
};
User.prototype.setName = function(name) {
this.name = name;
return this;
};
console.log(new User().setId(1314).setName('sven'));
使用链式调用的方式并不会造成太多阅读上的困难,也确实能省下一些字符和中间变量,但节省下来的字符数量同样是微不足道的。链式调用带来的坏处就是在调试的时候非常不方便,如果我们知道一条链中有错误出现,必须得先把这条链拆开才能加上一些调试 log 或者增加断点,这样才能定位错误出现的地方。
如果该链条的结构相对稳定,后期不易发生修改,那么使用链式调用无可厚非。但如果该链条很容易发生变化,导致调试和维护困难,那么还是建议使用普通调用的形式。
分解大型类
面向对象设计鼓励将行为分布在合理数量的更小对象之中
用 return 退出多重循环
假设在函数体内有一个两重循环语句,我们需要在内层循环中判断,当达到某个临界条件时退出外层的循环。我们大多数时候会引入一个控制标记变量:
var func = function() {
var flag = false;
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
flag = true;
break;
}
}
if (flag === true) {
break;
}
}
};
第二种做法是设置循环标记:
var func = function() {
outerloop: for (var i = 0; i < 10; i++) {
innerloop: for (var j = 0; j < 10; j++) {
if (i * j > 30) {
break outerloop;
}
}
}
};
这两种做法无疑都让人头晕目眩,更简单的做法是在需要中止循环的时候直接退出整个方法:
var func = function() {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return;
}
}
}
};
当然用 return 直接退出方法会带来一个问题,如果在循环之后还有一些将被执行的代码呢?如果我们提前退出了整个方法,这些代码就得不到被执行的机会:
var func = function() {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return;
}
}
}
console.log(i); // 这句代码没有机会被执行
};
为了解决这个问题,我们可以把循环后面的代码放到 return 后面,如果代码比较多,就应该把它们提炼成一个单独的函数:
var print = function(i) {
console.log(i);
};
var func = function() {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return print(i);
}
}
}
};
func();
设计模式概述和比较
所谓的设计模式,及封装变化。
通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。这也是设计模式的意义所在。
设计模式分别被划分为创建型模式、结构型模式和行为型模式。
创建型模式
要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。
-
原型模式
原型模式是一种设计模式,也是一种编程泛型,它构成了 JavaScript 这门语言的根本。
原型模式提供了一种创建对象的方式,通过克隆对象,我们就不用再关心对象的具体类型名字。
JavaScript 本身是一门基于原型的面向对象语言,它的对象系统就是使用原型模式来搭建的,在这里称之为原型编程范型也许更合适。 -
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。 -
简单工厂模式(静态工厂方法)
由一个工厂对象来决定直接创建某一种产品对象类的实例,主要用来创建同一类对象。 -
工厂方法模式
通过对产品类的抽象(创建父类工厂)使其创建对象,主要负责创建多类产品的实例。
工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象;
这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。 -
抽象工厂模式
通过子类对父类(一般为抽象类)继承,产生多个抽象类(生成抽象工厂)
子类继承父类,增强子类功能,然后再通过子类具体实例化对象
简单工厂生产实例 ,工厂方法模式生产实例的接口,抽象工厂生产的是工厂
- 建造者模式
建造者模式将一个复杂对象的构建层(创建结果)与其表示层(具体创建过程)相互分离,同样的构建过程(创建对象)可采用不同的表示。
工厂模式(包括抽象工厂)主要是为了创建对象实例或者类簇,关心的是最终产出(创建)的是什么,而不关心创建的过程。
建造者模式的目标任务也是创建对象,但该模式更多关心的是对象创建的整个过程,甚至关心到对象创建的每一个细节。
建造者模式,创建的对象更为复杂,其结果是一个复合对象。
模式 | 作用 | 优点 | 缺点 |
简单工厂模式 | 静态工厂方法,由一个工厂对象决定创建某一种产生对象的实例 | 代码复用 | |
工厂方法模式 | 对产品类的抽象使其创建业务主要负责用户创建多累产品的实例,采用安全模式创建的工厂类有利于创建对的对象 | 轻松创建多个类的实例对象 | |
抽象工厂模式 | 对类的工厂抽象使其业务用于对产品类簇的创建,而不负责创建某一类产品的实例 | 制定类类的结构,也就区别与简单工厂模式创建单一对象 | |
建造者模式 | 将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示 | 每一个模块都可以得到灵活的运用与高质量的复用 | |
原型模式 | 指向创建对象的类,适用于创建新的对象的类共享原型对象的属性和方法 | 让多个对象分享同一个原想对象的属性和方法 | |
单例模式 | 单体模式,是只允许实例化一次的对象类, | 只允许实例化一次的对象类,有可以节省空间 |
结构型模式
结构型模式封装的是对象之间的组合关系。
-
代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式,当一个对象不能直接引用另一个对象时,此时就需要通过代理对象在这两个对象之间起到中介作用。代理模式,在解决跨域问题时应用较为广泛。 -
组合模式
组合模式,又称“部分-整体”模式,把对象组合成树形结构,以表示出“部分-整体”的层次结构。组合模式,使得用户对单个对象和组合对象的使用具有一致性。 -
享元模式
享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。
通过分离内部状态(共享数据和方法)和外部状态(外部数据和方法),需要的时候组合为对象。 -
适配器模式
为不同接口提供统一的适配接口 -
装饰者模式
通过装饰函数,为对象动态加入新职责
不同于代理模式(静态关系),装饰者模式是动态织入(动态关系,关系不确定);
代理模式,适配器模式,装饰者模式几种模式都属于包装模式,都是由一个对象来包装另一个对象。区别它们的关键仍然是模式的意图。
代理只是一层引用,装饰可能有长长装饰链;
适配器模式通常只包装一次; -
桥接模式
桥接模式(Bridge),将抽象部分与实现部分分离,使他们可以独立的变化。桥接模式需要一个 桥,来连接抽象部分和实现部分。
桥接模式,在系统中沿着多个维度变化,不仅不会增加系统的复杂度,还可以达到解耦的目的。
抽象:在面向对象就是将对象共同的性质抽取出去而形成类的过程。
(JavaScript没有类的概念,可以理解为对象,进一步理解为函数,因为函数为一等公民也是对象)实现:针对抽象化给出的具体实现。它和抽象化是一个互逆的过程,实现化是对抽象化事物的进一步具体化。
(JavaScript没有类的概念,也就不需要具体化的过程,可以理解为对象具体调用的业务逻辑,或者对象被调用的具体上下文环境)
行为型模式
行为型模式封装的是对象的行为变化
-
策略模式
定义一系列的算法(或者业务规则),把它们一个个封装起来,并且使它们可以相互替换。策略模式和模板方法模式是一对竞争者。在大多数情况下,它们可以相互替换使用。模板方法模式基于继承的思想,而策略模式则 偏重于组合和委托。
从结构上看,策略模式和状态模式很像,也是在内部封装一个对象,然后通过返回的接口对象来实现对内部对象的调用。不同的是,策略模式不需要管理状态,状态之间也没有依赖关系,策略之间可以相互替换,策略对象内部保存的是相互独立的算法。
-
迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。分为内部迭达器和外部迭达器:
内部迭达器:迭达函数,内部已经定义好规则,只需要外部调用进行调用即可;外界不用关心迭代器内部的实现。
外部迭达器:迭达函数,内部已经定义好规则,但是必须显示的请求下一次迭达调用;外部迭代器将遍历的权利转移到外部,因此在调用的时候拥有了更多的自由性。 -
发布-订阅模式
定义了对象一对多的依赖关系,一个对象的状态改变,通知依赖对象,JS中利用事件模型替换传统发布-订阅模式。可对具体对象添加订阅-发布功能;每个对象都将重复定义监听和触发方法,和缓存列表,订阅对象还保有发布对象的引用(因为订阅对象利用发布对象设置监听),造成一定的耦合性。(这种方式为:观察者模式)
可设置一个全局的订阅发布对象,全局通用;全局订阅发布对象类似于一个中介对象,可使用发布对象和订阅对象解耦,且公用部分对象(方法)。(这种方式为:传统意义上的订阅与发布模式)
-
命令模式
命令模式会把请求封装为一个 command 对象,利用 command 对象去调用实际接收者,从而达到发送者和请求接收者松耦合的目的。命令模式与策略模式有些类似, 在 JavaScript 中它们都是隐式的。都是将请求委托给相应的执行对象,不同的是命令模式大都引有接收者的引用,因为有接受者的引用,可以做撤销,重做,以及队列等复杂操作;而策略模式是直接返回相应操作。
-
模板方法模式
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
模板方法模式的核心在于对方法的重用,它将核心方法封装在基类中,让子类继承基类的方法,实现对基类方法的共享。这是一种行为的约束。
-
职责链模式
将一系列可能会处理请求的对象连接成一条链,请求对象在这些处理对象之间依次传递,直到遇到一个可以处理它的对象。职责链模式解耦请求对象和接收对象,所有接收对象将被包装为节点对象(可通过对象包装或者AOP的方式包装),形成一条职责链。
职责链模式需要手动设置节点数量和节点顺序,所以节点数量和顺序是可以自由变化的。
-
中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。
增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系。中介者模式本质上是对多个对象模块之间复杂交互的封装。
-
状态模式
将事物内部的每个状态分别封装成类, 内部状态改变会产生不同行为。状态模式和策略模式像一对双胞胎,它们都封装了一系列的算法或者行为
相同点:它们都有一个上下文(Context )、一些策略或者状态类,上下文把请求委托给这些类来执行。
区别:
策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系,所以客户必须熟知这些策略类的作用,以便客户可以随时主动切换算法;而在状态模式中,状态和状态对应的行为是早已被封装好的,状态之间的切换也早被规定完成,“改变行为”这件事情发生在状态模式内部。对客户来说,并不需要了解这些细节。这正是状态模式的作用所在。
-
备忘录模式
备忘录模式,在不破坏对象封装性的前提下,在对象之外捕获并保存该对象的内部状态,以便日后在该对象使用时恢复到之前的某个状态。随时记录一个对象的状态变化;随时可以恢复之前的某个状态;状态对象和使用对象分开,解耦;
备忘录模式的主要缺点是资源消耗过大,如果需要保存的状态太多,就不可避免的需要占用大量的存储空间
自我总结
创建型模式
结构型模式
- 代理模式 : 包装对象:当不方便直接访问内部对象,为内部对象做包装,返回包装对象。
- 适配器模式 : 包装对象:抹平不同对象的访问接口,让对象访问接口一致。
- 装饰者模式 : 包装对象:动态给对象织入功能,维持着一条包装链。
- 组合模式 : 组合对象;将一组相关的对象,组合为‘部分-整体’的结构。
- 享元模式 :组合对象;将一群类似的对象抽离出内部状态和外部状态,内部状态对象为共享,外部状态根据实际情况和内部状态进行连接。
- 桥连模式 组合对象:分离抽象部分和实现部分,通过桥接的方式连接对象;沿着多个维度变化不会增加系统的复杂度,还可以达到抽象和实现解耦的目的。
行为型模式
单个对象行为封装:
- 策略模式 :受外部状态影响改变对象行为;封装不同的对象行为,返回一个行为接口
- 状态模式 :受内部状态影响改变对象行为;封装不同的状态行为(包含对象行为)
- 模版方法模式 :通过继承的方式,共享一类共同的行为框架,具体的对象行为实现,由子类具体实现。
- 迭达器模式 :提供一个接口,访问一个对象内的所有行为(属性),接口可提供多种访问顺序。
- 备忘录模式 :为对象行为提供一个快照功能,能随时提供返回。
依赖对象行为封装:
- 命令模式 :对象行为一对一;分离请求和接收对象行为的分离;并可在其中添加其他操作,因为命令对象已经持有接受者的引用。
- 订阅与发布模式 :对象行为一对多(发散:多个对象行为无关联);分离发布和多个订阅对象的行为分离。
- 职责链模式 :对象行为一对多(串行:多个对象行为有关联);分离请求和多个接收对象行为的分离。
- 中介者模式 :对象行为多对多;使网状的多对多关系变成了相对简单的一对多关系;本质上是对多个对象模块之间复杂交互的封装