装饰器模式
装饰器模式向一个现有的对象添加新的功能,同时又不改变其结构,属于结构性模式。该模式创建了一个装饰类,用来包装原有的类,并给原有的类增加额外的功能。包装类和原有的类对外提供相同名称的接口,访问包装类就像访问原有类一样。如原有类有getNumber方法,则包装类也有getNumber方法,只不过包装类getNumber方法又加了额外的功能。 有一个Notifier类,用于发送email消息。如果想增加其他类型的发送消息, SMS、Facebook或Slash。则通常想到的是通过继承实现,继承email类,创建SMS子类、Facebook子类或Slash类。通过继承能够同时发送email和其他消息的一种。
如果想发送email消息,同时发送SMS、Facebook和Slash这3种消息的2种消息,则需要再创建支持另外两种消息的子类。这样会产生很多子类,导致子类膨胀。
通过聚合的方式解决了子类膨胀问题,每一种消息有各自对应的类,通过依次包装扩展增加消息类型。此时只有email类,SMS类,Facebook类和Slash类。
- 如果想发送email和SMS消息,SMS类包装email类。new SMSNotifier(new emailNotifier())
- 如果想发送email、SMS消息和Facebook消息,SMS类包装email类,Facebook类包装SMS类。 new FacebookNotifier(new SMSNotifier(new emailNotifier()))
装饰器模式在webpack中的应用
下面看下模块在构建过程中涉及的几处XxxSource。 先是在模块的构建时module.doBuild -> runLoaders -> createSource(),createSource可能会返回多种类型的Source赋值给属性_source,假设这里返回OriginalSource。
// NormalModule.js
doBuild(options, compilation, resolver, fs, callback) {
var module = this;
//...
runLoaders({/* */}, function(err, result) {
source = asString(source);
//可能生成多种类型OriginalSource、SourceMapSourced,...
//假设这里生成 OriginalSource
module._source = new OriginalSource(source, module.identifier());
return callback();
});
};
在createChunkAssets生成最终需要输出文件内容的时候,会调用normalModule.source()来生成模块的最终内容,逻辑如下。
source(dependencyTemplates, outputOptions, requestShortener) {
//...
var source = new ReplaceSource(_source);
//...
return new CachedSource(source);
};
那normalModule.source()最终返回的source如下所示:
new CacheSource(new ReplaceSource(new OriginalSource(...));)
CacheSource和ReplaceSource在这里都是装饰类,对传入的source进行能力增强,显然ReplaceSource提供了对原始source进行修改的能力,CacheSource提供了缓存能力。 下面看下作为装饰器的ReplaceSource是如何增强的。
class OriginalSource extends Source {
constructor(value, name) {
super();
this._value = value;
this._name = name;
}
source() {
return this._value;
}
}
function ReplaceSource(source, name) {
Source.call(this);
this._source = source;
this._name = name;
this.replacements = [];
}
ReplaceSource.prototype.source = function(options) {
return this._replaceString(this._source.source());
};
ReplaceSource.prototype.replace = function(start, end, newValue) {
this.replacements.push([start, end, newValue, this.replacements.length]);
};
ReplaceSource.prototype._replaceString = function(str) {
var result = [str];
this.replacements.forEach(function(repl) {
var remSource = result.pop();
var splitted1 = this._splitString(remSource, Math.floor(repl[1] + 1));
var splitted2 = this._splitString(splitted1[0], Math.floor(repl[0]));
result.push(splitted1[1], repl[2], splitted2[0]);
}, this);
result = result.reverse();
return result.join("");
};
使用方式如下:
const originalSource = new OriginalSource('test data');
const replaceSource = new ReplaceSource(originalSource);
// 修改原始内容
replaceSource.replace(...)
// 获取修改后内容
replaceSource.source()
看到ReplaceSource通过包装原有Source(如这里的OriginalSource)实现功能增强,并且由于二者都实现了Source父类,因此对外接口一致。 通常扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。比如这里如果采用继承实现缓存功能,则需要给RawSource、OriginalSource等等都去继承一个子类如CacheRawSource、CacheOriginalSource等,导致子类膨胀。