blog icon indicating copy to clipboard operation
blog copied to clipboard

设计模式 --装饰者模式

Open aermin opened this issue 4 years ago • 0 comments

为啥需要装饰者模式

在程序开发中,许多时候都并不希望某个类天生就非常庞大,一次性包含许多职责。那么我们就可以使用装饰者模式。装饰者模式可以动态地给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象。装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象 动态地添加职责。

在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活, 还会带来许多问题

  1. 会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之改变;
  2. 继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的, 在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。
  3. 在完成一些功能复用的同时,有可能创建出大量的子类, 使子类的数量呈爆炸性增长。比如有4种型号的自行车,每种自行车都是一个单独的类,给每种自行车都装上前灯、尾 灯和铃铛这 3 种配件(三个类),那就要继承这个三个类。每个自行车类要继承三次配件类,最后就产生4 * 3 = 12个类。但是如果把前灯、尾灯、铃铛这些对象动态组 合到自行车上面,则只需要额外增加 3 个类。
// 原始的飞机类
const Plane = function () {};

Plane.prototype.fire = function () { console.log('发射普通子弹'); };

const MissileDecorator = function (plane) {
  this.plane = plane;
};
// 增加两个装饰类,分别是导弹和原子弹
MissileDecorator.prototype.fire = function () {
  this.plane.fire();
  console.log('发射导弹');
};

const AtomDecorator = function (plane) {
  this.plane = plane;
};
AtomDecorator.prototype.fire = function () {
  this.plane.fire();
  console.log('发射原子弹');
};

let plane = new Plane();
plane = new MissileDecorator(plane);
plane = new AtomDecorator(plane);

plane.fire(); // 分别输出: 发射普通子弹、发射导弹、发射原子弹

以上代码思路就是把对象放入装饰者中,然后装饰者会在某个方法中除了执行原对象的该方法外另外执行其他添加(装饰)的东西。装饰器相当于拓展了某个方法,给改方法添加了一些额外的功能

这种给对象动态增加职责的方式,并没有真正地改动对象自身,而是将对象放入另一个对象 之中,这些对象以一条链的方式进行引用,形成一个聚合对象

装饰者模式是啥

装饰者也是包装器

从功能上而言,decorator 能很好地描述这个模式,但从结构上看,wrapper 的说法更加贴切。装饰者模式将一个对象嵌入另一个对象 之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链依次传递到所有的对象,每个对象都有处理这条 请求的机会。

装饰函数

想要不改动某个函数源代码的情况下,给该函数添加一些额外的功能,可以通过保存原引用的方式来改写

const _getElementById = document.getElementById;

document.getElementById = function() {
  alert(1);
  return _getElementById.apply(document, arguments);
};

const button = document.getElementById("button");

这样不是很方便,用AOP给函数动态增加功能将更完美

用AOP装饰函数

把行为依照职责分成粒度更细的函数,随后通过装饰把它们合并到一起,这有助于我 们编写一个松耦合和高复用性的系统。

数据统计上报

分离业务代码和数据统计代码,无论在什么语言中,都是 AOP 的经典应用之一

Function.prototype.after = function (afterFn) {
  const __self = this; // This 等于Function实例
  return function () {
    const ret = __self.apply(this, arguments); // 执行自身方法
    afterFn.apply(this, arguments); // 执行所拓展的功能
    return ret; // 返回自身方法执行后的结果 这边为undefined
  };
};

let showLogin = function () {
  console.log('打开登录浮层');
};

const log = function () {
  console.log(`上报标签为: ${this.getAttribute('tag')}`);
};

// 打开登录浮层之后上报数据
showLogin = showLogin.after(log); // 这时after方法内的this为function showLogin() { console.log('打开登录浮层');} 因为是showLogin调用after

document.getElementById('button').onclick = showLogin; // 这时button element的onclick函数等于showLogin等于after执行后return的东西function () { const ret = __self.apply(this, arguments); log.apply(this, arguments); return ret; }。因为是document.getElementById('button')调用,after方法内的this指向document.getElementById('button'),也就是"<button tag='login' id='button'>点击打开登录浮层</button>"

用AOP动态改变函数的参数

例1:通过 Function.prototype.before 方法给函数 func 的参数 param 动态地 添加属性 b

let func = function (param) {
  console.log(param);
};

func = func.before((param) => { param.b = 'b'; });

func({ a: 'a' }); // 输出: {a: "a", b: "b"}

例2:

常见版本 => 如果这个函数移植到其他项目,或者开源供他人使用,token参数就是冗余非必须的

const ajax = function (type, url, param) {
  param = param || {};
  Param.Token = getToken(); // 发送 ajax 请求的代码略...
};

AOP版本 => 用 AOP 的方式给 ajax 函数动态装饰上 Token 参数,保证了 ajax 函数是一个相对纯净的函数,提高了 ajax 函数的可复用性,它在被迁往其他项目的时候,不需要做任何修改。

let ajax = function (type, url, param) {
  console.log(param); // 发送 ajax 请求的代码略
};

const getToken = function () { return 'Token'; };

ajax = ajax.before((type, url, param) => { param.Token = getToken(); });

ajax('get', 'http:// xxx.com/userinfo', { name: 'sven' });

插件式的表单验证

常规写法:

let ajax = function (type, url, param) {
  console.log(param); // 发送 ajax 请求的代码略
};

const username = document.getElementById('username');
const password = document.getElementById('password');
const validDate = function () {
  if (username.value === '') {
    alert('用户名不能为空'); return false;
  } if (password.value === '') {
    alert('密码不能为空');
    return false;
  }
};
const formSubmit = function () {
  if (validDate() === false) { return; } const param = {
    // 校验未通过

    username: username.value, password: password.value

  };
  ajax('post', 'http:// xxx.com/login', param);
};

AOP装饰者写法:

校验输入和提交表单的代码完全分离开来, 它们不再有任何耦合关系, formSubmit = formSubmit.before( validDate )这句代码,如同把校验规则动态接在 formSubmit 函数 之前,validDate 成为一个即插即用的函数,它甚至可以被写成配置文件的形式,这有利于我们分 开维护这两个函数。再利用策略模式稍加改造,我们就可以把这些校验规则都写成插件的形式, 用在不同的项目当中

const username = document.getElementById('username');
const password = document.getElementById('password');

Function.prototype.before = function (beforeFn) {
  const __self = this;
  return function () {
    if (beforeFn.apply(this, arguments) === false) { // beforeFn 返回 false 的情况直接 return,不再执行后面的原函数
      return;
    }
    return __self.apply(this, arguments);
  };
};

const validDate = function () {
  if (username.value === '') {
    alert('用户名不能为空'); return false;
  } if (password.value === '') {
    alert('密码不能为空');
    return false;
  }
};

let formSubmit = function () {
  const param = { username: username.value, password: password.value };
  ajax('post', 'http:// xxx.com/login', param);
};

formSubmit = formSubmit.before(validDate);

aermin avatar Dec 11 '19 15:12 aermin