设计模式大家都有了解,网上有很多系列教程。
这里只分享 装饰者模式 以及如何使用 ES7 的 decorator
概念。
装饰模式和适配器模式都是 包装模式 (Wrapper Pattern),它们都是通过封装其他对象达到设计的目的的,但是它们的形态有很大区别。
更多区别参见:设计模式——装饰模式(Decorator)
装饰模式经典的应用是 AOP 编程,比如“日志系统”,日志系统的作用是记录系统的行为操作,它在不影响原有系统的功能的基础上增加记录环节 —— 好比你佩戴了一个智能手环,并不影响你日常的作息起居,但你现在却有了自己每天的行为记录。
更加抽象的理解,可以理解为给数据流做一层filter
,因此 AOP 的典型应用包括 安全检查、缓存、调试、持久化等等。可参考Spring aop 原理及各种应用场景 。
ES7 中增加了一个 decorator
属性,它借鉴自 Python,请参考文章Decorators in ES7。
下面我们以 钢铁侠 为例讲解如何使用 ES7 的 decorator。
以钢铁侠为例,钢铁侠本质是一个人,只是“装饰”了很多武器方才变得那么 NB,不过再怎么装饰他还是一个人。
我们的示例场景是这样的
Man
类,它的抵御值 2,攻击力为 3,血量为 3;创建 Man 类:
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
toString(){
return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
}
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:2,攻击力:3,血量:3
代码直接放在 http://babeljs.io/repl/ 中运行查看结果,记得勾选
Experimental
选项和Evaluate
选项
创建 decorateArmour 方法,为钢铁侠装配盔甲——注意 decorateArmour
是装饰在方法init
上的。
function decorateArmour(target, key, descriptor) {
const method = descriptor.value;
let moreDef = 100;
let ret;
descriptor.value = (...args)=>{
args[0] += moreDef;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
toString(){
return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
}
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3
我们先看输出结果,防御力的确增加了 100,看来盔甲起作用了。
初学者这里会有两个疑问:
decorateArmour
方法的参数为啥是这三个?可以更换么?decorateArmour
方法为什么返回的是descriptor
这里给出个人的解答作为参考:
descriptor = decorator(target, key, descriptor) || descriptor;
,点到为止,这里不详细展开了,可自行看看这行代码的上下文(参考文献中也涉及到这句代码的解释)。在上面的示例中,我们成功为 普通人 增加 “盔甲” 这个装饰;现在我想再给他增加 “光束手套”,希望额外增加 50 点防御值。
Step 1:拷贝一份decorateArmour
方法,改名为decorateLight
,同时修改防御值的属性:
function decorateLight(target, key, descriptor) {
const method = descriptor.value;
let moreAtk = 50;
let ret;
descriptor.value = (...args)=>{
args[1] += moreAtk;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
Step 2:直接在init
方法上添加装饰语法:
....
@decorateArmour
@decorateLight
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
...
最后的代码如下:
...
function decorateLight(target, key, descriptor) {
const method = descriptor.value;
let moreAtk = 50;
let ret;
descriptor.value = (...args)=>{
args[1] += moreAtk;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
@decorateLight
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
...
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
//输出:当前状态 ===> 防御力:102,攻击力:53,血量:3
在这里你就能看出装饰模式的优势了,它可以对某个方法进行叠加使用,对原类的侵入性非常小,只是增加一行@decorateLight
而已,可以方便地增删;(同时还可以复用)
按文章 装饰模式所言,装饰模式有两种:纯粹的装饰模式 和 半透明的装饰模式。
上述的两个 demo 中所使用的应该是 纯粹的装饰模式,它并不增加对原有类的接口;下面要讲 demo 是给普通人增加“飞行”能力,相当于给类新增一个方法,属于 半透明的装饰模式,有点儿像适配器模式的样子。
Step 1:增加一个方法:
function addFly(canFly){
return function(target){
target.canFly = canFly;
let extra = canFly ? '(技能加成:飞行能力)' : '';
let method = target.prototype.toString;
target.prototype.toString = (...args)=>{
return method.apply(target.prototype,args) + extra;
}
return target;
}
}
Step 2:这个方法将直接去装饰类:
...
// 3
function addFly(canFly){
return function(target){
target.canFly = canFly;
let extra = canFly ? '(技能加成:飞行能力)' : '';
let method = target.prototype.toString;
target.prototype.toString = (...args)=>{
return method.apply(target.prototype,args) + extra;
}
return target;
}
}
@addFly(true)
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
@decorateLight
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
...
}
...
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:53,血量:3(技能加成:飞行能力)
作用在方法上的 decorator
接收的第一个参数(target )是类的 prototype
;如果把一个 decorator
作用到类上,则它的第一个参数 target 是 类本身。(参考 Decorators in ES7 )
关于如何用现有标准的原生 JS 实现的装饰模式,可参考译文
JavaScript设计模式:装饰者模式,这是一篇值得一读的文章,深入浅出。
这里用 ES5 重写一下上面的 Demo 1的场景,简略说一下关键点:
最后代码是:
// 首先我们要创建一个基类
function Man(){
this.def = 2;
this.atk = 3;
this.hp = 3;
}
// 装饰者也需要实现这些方法,遵守 Man 的接口
Man.prototype={
toString:function(){
return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
}
}
// 创建装饰器,接收 Man 对象作为参数。
var Decorator = function(man){
this.man = man;
}
// 装饰者要实现这些相同的方法
Decorator.prototype.toString = function(){
return this.man.toString();
}
// 继承自装饰器对象
// 创建具体的装饰器,也是接收 Man 作对参数
var DecorateArmour = function(man){
var moreDef = 100;
man.def += moreDef;
Decorator.call(this,man);
}
DecorateArmour.prototype = new Decorator();
// 接下来我们要为每一个功能创建一个装饰者对象,重写父级方法,添加我们想要的功能。
DecorateArmour.prototype.toString = function(){
return this.man.toString();
}
// 注意这里的调用方式
// 构造器相当于“过滤器”,面向切面的
var tony = new Man();
tony = new DecorateArmour(tony);
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3
AOP 的经典应用就是 日志系统 了,那么我们也用 ES7 的语法给钢铁侠打造一个日志系统吧。
下面是最终的代码:
/**
* Created by jscon on 15/10/16.
*/
let log = (type) => {
return (target, name, descriptor) => {
const method = descriptor.value;
descriptor.value = (...args) => {
console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);
let ret;
try {
ret = method.apply(target, args);
console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);
} catch (error) {
console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
}
return ret;
}
}
}
class IronMan {
@log('IronMan 自检阶段')
check(){
return '检查完毕';
}
@log('IronMan 攻击阶段')
attack(){
return '击倒敌人';
}
@log('IronMan 机体报错')
error(){
throw 'Something is wrong!';
}
}
var tony = new IronMan();
tony.check();
tony.attack();
tony.error();
// 输出:
// (IronMan 自检阶段) 正在执行: check() = ?
// (IronMan 自检阶段) 成功 : check() => 检查完毕
// (IronMan 攻击阶段) 正在执行: attack() = ?
// (IronMan 攻击阶段) 成功 : attack() => 击倒敌人
// (IronMan 机体报错) 正在执行: error() = ?
// (IronMan 机体报错) 失败: error() => Something is wrong!
Logger 方法的关键在于:
const method = descriptor.value;
将原有方法提取出来,保障原有方法的纯净;ret = method.apply(target, args);
在调用之前之后分别进行日志汇报;return ret;
原始的调用结果相信这套思路会给后续我们实现 AOP 模式提供良好的借鉴。
当你想要一个有 3 种功能的钢铁侠,就得用 new 操作符 创建 4 个对象。这么做单调乏味又烦人,所以我们打算只调用一个方法就能创建出一部拥有所有功能的钢铁侠。
这就需要 工厂模式 了,工厂模式的官方定义是:在子类中对一个类的成员对象进行实例化。比如定义 decorateIronMan(person,feature) 方法,里面接受一个 Person 对象(而不是自己初始化),相当于流水线生产了。
如何 结合装饰模式和工厂模式 提高代码效能,这篇优秀的译文 JavaScript设计模式:工厂模式 给出了详细的方法,这里不再赘述 ,强烈推荐阅读此文。
decorator
目前还只是一个提议,但是感谢 Babel ,我们现在就可以体验它了。首先,安装 babel:
npm install babel -g
然后,开启 decorator:
babel --optional es7.decorators foo.js > foo.es5.js
babel 也提供了一个在线的 REPL ,勾选 experimental 选项,就可以了。
Step 1 :首先全局安装babel
组件模块
npm install -g babel
Step 2 :设置 scope (这一步可以省略)
命名 scope:
将文件添加到当前 scope:
Step 3 :设置 ES 版本
Step 4 :添加 watcher
arguments 内可以填写:
$FilePathRelativeToProjectRoot$ --stage --out-file $FileNameWithoutExtension$-es5.js $FilePath$
.
如果需要 source-map,需要添加
--source-map
选项,同时在Output paths to refresh
中填写$FileNameWithoutExtension$-es5.js:$FileNameWithoutExtension$-es5.js.map
更多设置参考babel cli
虽然它是 ES7 的特性,但在 Babel 大势流行的今天,我们可以利用 Babel 来使用它。我们可以利用 Babel 命令行工具,或者 grunt、gulp、webpack 的 babel 插件来使用 Decorators。
上述的代码都可以直接放在 http://babeljs.io/repl/ 中运行查看结果;
关于 ES7 Decorators 的更有意思的玩法,你可以参见牛人实现的常用的 Decorators:core-decorators。以及 raganwald 的 如何用 Decorators 来实现 Mixin。
traits-decorator
模块;