设计模式是什么?
设计模式并非是软件开发的专业术语。实际上,“模式”最早诞生于建筑学。20世纪70年代,
哈佛大学建筑学博士Christopher Alexander和他的研究团队花了约20年的时间,研究了为解决同一个问题而设计出的不同建筑结构,从中发现了那些高质量设计中的相似性,并且用“模式”来指代这种相似性。
受Christopher Alexander工作的启发,Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides四人(人称Gang Of Four ,GoF)把这种“模式”观点应用于面向对象的软件设计中, 并且总结了23种常见的软件开发设计模式,录入《设计模式:可复用面向对象软件的基础》一书。
设计模式是在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案。
《设计模式》一书完全是从面向对象设计的角度出发的,通过对封装、继承、多态等技术的反复使用,提炼出一些可重复使用的面向对象设计技巧。但设计模式实际上是解决某些问题的一种思想,与具体使用的语言无关。在函数式或者其他编程范型的语言中,设计模式依然存在。
设计模式的分类
《设计模式》一书提出:
考虑你的设计中哪些地方可能变化,这种方式与关注会导致重新设计的原因相反。它不是考虑什么时候会迫使你的设计改变,而是考虑你怎样才能够在不重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题。
即“找到变化并封装之”。《设计模式》一书中共归纳总结了23 种设计模式。从意图上区分,这23 种设计模式分别被划分为创建型模式、结构型模式和行为型模式。
创建型设计模式
拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。创建型设计模式就是用于描述“怎么创建对象”。它的主要特点是“将对象的创建与使用分离”。
结构设计模式
结构型模式封装的是对象之间的组合关系,用于描述“如何将类或对象按某种布局组成更大的结构”。
行为设计模式
行为型模式封装的是对象的行为变化,用于描述“类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责”。
JavaScript中的创建型设计模式
虽然JavaScript是一门面向对象的语言,但有些适合静态类型语言的模式并不一定完全适合JavaScript。
比如有人为了模拟JavaScript版本的工厂方法(Factory Method)模式,而生硬地把创建对象的步骤延迟到子类中。在Java等静态类型语言中,让子类来“决定”创建何种对象的原因是为了让程序迎合依赖倒置原则(DIP)。在这些语言中创建对象时,先解开对象类型之间的耦合关系非常重要,这样才有机会在将来让对象表现出多态性。而在JavaScript这种类型模糊的语言中,对象多态性是天生的,没有必要刻意去把对象“延迟”到子类创建,也就是说,JavaScript实际上是不需要工厂方法模式的。
模式的存在首先是能为我们解决什么问题,这种牵强的模拟只会让人觉得设计模式既难懂又没什么用处。
1.原型模式(Prototype)
为JavaScript设计面向对象系统之初,Brendan Eich 就没有打算在JavaScript中加入类的概念。
在以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来。而在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来, 一个对象是通过克隆另外一个对象所得到的。
原型模式是用于创建对象的一种模式,如果我们想要创建一个对象, 一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式选择了另外一种方式,我们 不再关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。
原型模式的实现关键,是语言本身是否提供了clone 方法。ECMAScript 5 提供了Object.create
方法,可以用来克隆对象。
但实际上,不需要调用Object.create,我们在JavaScript 遇到的每个对象,都是从Object.prototype 对象克隆而来的,Object.prototype 就是它们的原型。比如下面的obj1 对象和obj2 对象:
var obj1 = new Object();
var obj2 = {};
在JavaScript 语言里,我们并不需要关心克隆的细节,因为这是引擎内部负责实现的。我们所需要做的只是显式地调用var obj1 = new Object() 或者 var obj2 = {}。此时,引擎内部会从
Object.prototype上面克隆一个对象出来,我们最终得到的就是这个对象。
再来看看如何用new 运算符从构造器中得到一个对象,下面的代码我们再熟悉不过了:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var a = new Person( 'sven' )
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype );
我们调用了new Person() ,但在这里Person并不是类,而是函数构造器,JavaScript的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用new 运算符来调用函数时,此时的函数就是一个构造器。
在Chrome 和Firefox 等向外暴露了对象__proto__属性的浏览器下,我们可以通过下面这段代码来理解new运算的过程:
function Person( name ){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var objectFactory = function(){
var obj = new Object(), // 从Object.prototype 上克隆一个空的对象
Constructor = [].shift.call( arguments ); // 取得外部传入的构造器,此例是Person
obj.__proto__ = Constructor.prototype; // 指向正确的原型
var ret = Constructor.apply( obj, arguments ); // 借用外部传入的构造器给obj 设置属性
return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象
};
var a = objectFactory( Person, 'sven' );
console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true
对于原型,我们很清楚的一点是当一个对象无法响应某个请求的时候,它会顺着原型链把请求传递下去,直到遇到一个可以处理该请求的对象为止。
2.单例模式(Singleton)
单例模式,即保证一个类仅有一个实例,并提供一个访问它的全局访问点。
想象一下,假如我们要实现点击按钮就跳出登陆弹窗这个功能。我们有两种实现方式,第一种弹窗在一开始就创建好,但是display为none,在点击时我们再把它显示为可见。第二种方法,我们监听点击事件,动态的生成节点,并添加到DOM里去。
对于第一种方法,有些人点击网页只是想到处看看,并没有想登录,如果弹窗一开始就设计好,可能白白浪费一些DOM节点。而对于第二种,我们需要注意的一点是,对于反复点击,并不应该重复生成弹窗,无论点击多少次按钮,这个弹窗应该只会被创建一次,而这种情况就适合单例模式。
const getSingle = (fn) => {
let result;
return (...rest) => {
return result || (result = fn.apply(this, rest))
}
}
const createLoginLayer = () => {
const div = document.createElement('div');
div.innerHTML = "登陆弹窗";
div.style.display = 'none';
document.appendChild(div);
return div
}
const createSingleLoginLayer = getSingle(createLoginLayer);
document.getElementById('loginBtn').onclick = () => {
const loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
}
我们将只创建一次这个过程抽象出来,createSingleLoginLayer函数利用了闭包,保证了只在第一次创建的时候生成节点,对于反复点击,每次返回的只是第一次生成的节点。
参考资料
《设计模式:可复用面向对象软件的基础》
《JavaScript设计模式与开发实践》
23种设计模式——创建型设计模式(5种)
Learning JavaScript Design Patterns