结构型模式封装的是对象之间的组合关系,用于描述“如何将类或对象按某种布局组成更大的结构”。常见的模式包括代理模式(Proxy)
、组合模式(Composite)
、享元模式(flyweight)
、装饰器模式(Decorator)
、适配器模式(Adapter)
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
现实生活中有很多使用代理模式的场景。比如,要请明星来一场商演,这时候你需要去联系他的经纪人;当你和经纪人洽谈好后,经纪人才会把合同交给明星。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
代理的类型主要包括保护代理
和虚拟代理
。保护代理
用于控制不同权限的对象对目标对象的访问,而虚拟代理
则把开销大的对象延迟到真正需要它的时候才去创建。在Javascript中更常用的是虚拟代理。
举一个例子,在网页开发中,经常会遇到图片加载的问题。由于图片过大或是网络较差,常常会导致图片未加载出来而显示一片空白。常见的做法是先用一张 loading 图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到 img 节点里,这种场景就很适合使用虚拟代理。
var myImage = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
}
}
})();
var proxyImage = (function () {
var img = new Image;
img.onload = function () {
myImage.setSrc(this.src);
}
return {
setSrc: function (src) {
myImage.setSrc('./loading.gif');
img.src = src;
}
}
})();
proxyImage.setSrc('http://xxxxx.jpg');
组合模式
将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性。
文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以包含其他文件夹,最终可能组合成一棵树。我们使用组合模式来模拟文件夹扫描的过程,首先分别定义好文件夹Folder和文件File这两个类。
var Folder = function (name) {
this.name = name;
this.files = [];
};
Folder.prototype.add = function (file) {
this.files.push(file);
};
Folder.prototype.scan = function () {
console.log('开始扫描文件夹: ' + this.name);
for (var i = 0, file, files = this.files; file = files[i++];) {
file.scan();
}
};
var File = function (name) {
this.name = name;
};
File.prototype.add = function () {
throw new Error('文件下面不能再添加文件');
};
File.prototype.scan = function () {
console.log('开始扫描文件: ' + this.name);
};
接下来创建一些文件夹和文件对象,并且让它们组合成一棵树。
var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var folder2 = new Folder('jQuery');
var file1 = new File('JavaScript 设计模式与开发实践');
var file2 = new File('精通jQuery');
var file3 = new File('重构与模式')
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);
通过这个例子,我们再次看到客户是如何同等对待组合对象和叶对象。在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹。新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树里已有的对象一起工作。
运用了组合模式之后,扫描整个文件夹的操作也是轻而易举的,我们只需要操作树的最顶端对象。
folder.scan();
享元(flyweight)模式
是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
假设有个内衣工厂,目前的产品有50 种男式内衣和50种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。不使用享元模式的情况下,在程序里也许会这样写:
var Model = function (sex, underwear) {
this.sex = sex;
this.underwear = underwear;
};
Model.prototype.takePhoto = function () {
console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};
for (var i = 1; i <= 50; i++) {
var maleModel = new Model('male', 'underwear' + i);
maleModel.takePhoto();
};
for (var j = 1; j <= 50; j++) {
var femaleModel = new Model('female', 'underwear' + j);
femaleModel.takePhoto();
};
要得到一张照片,每次都需要传入sex和underwear参数,如上所述,现在一共有50种男内衣和50种女内衣,所以一共会产生100个对象。如果将来生产了10000种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。
下面我们来考虑一下如何优化这个场景。虽然有100种内衣,但很显然并不需要50个男模特和50个女模特。其实男模特和女模特各自有一个就足够了,他们可以分别穿上不同的内衣来拍照。
var Model = function (sex) {
this.sex = sex;
};
Model.prototype.takePhoto = function () {
console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};
var maleModel = new Model('male'),
femaleModel = new Model('female');
for (var i = 1; i <= 50; i++) {
maleModel.underwear = 'underwear' + i;
maleModel.takePhoto();
};
for (var j = 1; j <= 50; j++) {
femaleModel.underwear = 'underwear' + j;
femaleModel.takePhoto();
};
享元模式要求将对象的属性划分为内部状态与外部状态。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。
内部状态存储于对象内部。
内部状态可以被一些对象共享。
内部状态独立于具体的场景,通常不会改变。
外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。
在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰器模式。装饰器模式
可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。
假设我们在编写一个飞机大战的游戏,随着经验值的增加,我们操作的飞机对象可以升级成更厉害的飞机,一开始这些飞机只能发射普通的子弹,升到第二级时可以发射导弹,升到第三级时可以发射原子弹。
下面来看代码实现。
var Plane = function () { }
Plane.prototype.fire = function () {
console.log('发射普通子弹');
}
var MissileDecorator = function (plane) {
this.plane = plane;
}
MissileDecorator.prototype.fire = function () {
this.plane.fire();
console.log('发射导弹');
}
var AtomDecorator = function (plane) {
this.plane = plane;
}
AtomDecorator.prototype.fire = function () {
this.plane.fire();
console.log('发射原子弹');
}
var plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);
plane.fire();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象之中,这些对象以一条链的方式进行引用,形成一个聚合对象。这些对象都拥有相同的接口(fire方法),当请求达到链中的某个对象时,这个对象会执行自身的操作,随后把请求转发给链中的下一个对象。
JavaScript语言动态改变对象相当容易,我们可以直接改写对象或者对象的某个方法,并不需要使用“类”来实现装饰者模式。
var plane = {
fire: function () {
console.log('发射普通子弹');
}
}
var missileDecorator = function () {
console.log('发射导弹');
}
var atomDecorator = function () {
console.log('发射原子弹');
}
var fire1 = plane.fire;
plane.fire = function () {
fire1();
missileDecorator();
}
var fire2 = plane.fire;
plane.fire = function () {
fire2();
atomDecorator();
}
plane.fire();
// 分别输出: 发射普通子弹、发射导弹、发射原子弹
通过保存原引用的方式就可以改写某个函数,这样的代码当然是符合开放封闭原则的,我们在增加新功能的时候,确实没有修改原来的代码,但是这种方式存在以下两个问题。
必须维护中间变量。
this被劫持。
而这可以通过AOP解决:
Function.prototype.before = function (beforefn) {
var __self = this; // 保存原函数的引用
return function () { // 返回包含了原函数和新函数的"代理"函数
beforefn.apply(this, arguments); // 执行新函数,且保证this不被劫持,新函数接受的参数
// 也会被原封不动地传入原函数,新函数在原函数之前执行
return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
// 并且保证this 不被劫持
}
}
Function.prototype.after = function (afterfn) {
var __self = this;
return function () {
var ret = __self.apply(this, arguments);
afterfn.apply(this, arguments);
return ret;
}
};
var fire = function () {
console.log('发射普通子弹');
}
var missileDecorator = function () {
console.log('发射导弹');
}
var atomDecorator = function () {
console.log('发射原子弹');
}
var fire = fire.after(missileDecorator).after(atomDecorator);
fire();
适配器模式
的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
假设我们要用googleMap和baiduMap分别以各自的方式在页面中展现地图。
var googleMap = {
show: function () {
console.log('开始渲染谷歌地图');
}
};
var baiduMap = {
display: function () {
console.log('开始渲染百度地图');
}
};
var renderMap = function (map) {
if (map.show instanceof Function) {
map.show();
}
};
由于两个地图的展示方法不同,所以不能直接调用renderMap函数,因此我们要为百度地图提供一个适配器。这样我们就可以同时渲染2个地图了。
var baiduMapAdapter = {
show: function () {
return baiduMap.display();
}
};
renderMap(googleMap); // 输出:开始渲染谷歌地图
renderMap(baiduMapAdapter); // 输出:开始渲染百度地图
作者:fortda
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。