JavaScript设计模式(三)--结构型设计模式

外观模式

为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更加容易。在JavaScript中有时也会用于对底层结构兼容性做统一封装来简化用户使用。
比如,点击事件,当我们采用document.onclick的写法时此时为DOM0级事件,当再次编辑时间函数的时候函数会被重写,之前定义的函数会被覆盖,而如果我们通过DOM2级事件处理程序提供的方法addEventListener来实现可以避免这个问题,但是这个方法IE9以下的不支持,需要用attachEvent方法,此时我们可以用函数将其封装成一个方法:

// 外观模式实现
function addEvent(dom, type, fn) {
  // 对于支持DOM2级事件处理程序addEventListener方法的浏览器
  if (dom.addEventListener) {
    dom.addEventListener(type, fn, false);
  } else if (dom.attachEvent) {
    // 对于不支持addEventListener方法但支持attachEvent方法的浏览器
    dom.attachEvent('on' + type, fn);
  } else {
    // 对于既不支持addEventListener也不支持attachEvent方法,但支持on+'事件名'的浏览器
    dom['on' + type] = fn;
  }
}

很多代码库通过外观模式来封装多个功能,简化底层操作方法,如下面的例子,简单实现获取元素属性样式的简单方法库

// 简约版样式库方法库
var A = {
  // 通过id获取元素
  g: function (id) {
    return document.getElementById(id);
  },
  // 设置元素css属性
  css: function (id, key, value) {
    document.getElementById(id).style[key] = value;
  },
  // 设置元素属性
  attr: function (id, key, value) {
    document.getElementById(id)[key] = value;
  },
  html: function (id, html) {
    document.getElementById(id).innerHTML = html;
  },
   // 为元素绑定事件
  on: function (id, type, fn) {
    document.getElementById(id)['on' + type] = fn;
  }
 };

具体使用方法:

A.css('box', 'background', 'red'); // 设置css样式
A.attr('box', 'className', 'box'); // 设置class
A.html('box', '这是添加的内容'); // 设置内容
A.on('box', 'click', function () { // 绑定事件
  A.css('box', 'width', '500px');
});

总结:
外观模式用于简化底层接口复杂性;用于解决浏览器兼容性。
这种方式封装的接口方法不需要接口的具体实现,只要按照接口使用规则即可,者可以看成是对系统与客户之间的一种松散耦合,使得系统和客户之间不会因结构的变化而相互影响。
对于外观模式,我们可以直接使用,而不去了解其内部结构。
缺点:
这种方式在对接口的二次封装增加一些资源开销以及程序的复杂度
当通过外观模式构建的直接代码库不能满足新需求,想要引入一个更加先进的代码库时,两个存在不兼容的代码在这个模式中无法解决。

适配器模式

将一个类(对象)的接口(方法或属性)转化成另一个接口,以满足用户需求,使类(对象)之间的不兼容问题通过适配器得以解决。
其最重要的意义是解决了前后端数据依赖的问题,前端程序不在为后端传递的数据所束缚。后端如果因为架构改变导致传递的数据结构发生改变,此时我们只需要写一个适配器。
具体参考如下:

// 为简化模型,这里使用jQuery的ajax方法 理想数据是一个一维数组
function ajaxAdapter (data) {
  // 处理数据并返回新数据
  return [data['key1'], data['key2'], data['key3']]
}

$.ajax({
  url: 'someAdress.php',
  success: function (data, status) {
    if (data) {
      // 使用适配后的数据 返回的对象
    }
  }
})

另外,适配器的应用更多的是在对象之间,为了使对象可用,通常我们会将对象拆分并重新包裹,这种情况下我们需要了解适配器内部结构,这里需要我们注意它和外观模式的区别。
其与外观模式相同的地方在于两者都解决了对象之间的耦合度问题。

代理模式

当一个对象不能直接引用另一个对象时需要代理对象在在两个对象之间起到中介的作用。
例如站长平台,有时站长平台统计页面访问量可能需要跨域来发送请求,此时我们只需要利用img之类的标签,通过src属性可以向其他域下的服务器发送请求,尽管这类请求只是get请求,并且是单向的,没有数据响应,但这已经能够满足我们的需求。其原理就是在用户的页面触发一些动作的时候,向站长平台发送这类img请求,然后他们会对你的请求做统计,然而用户此时并不知道相关信息已经得到统计。通过new Image()创建图片不增加DOM的渲染时间,只是发送请求,符合我们开发的规范。(提示:日常开发中也常常用new Image()方法来做图片的缓存)

// 统计代理
var Count = (function () {
  var img = new Image();
  // 返回统计函数
  return function (param) {
    // 请求统计字符串
    var str = 'http:// www.count.com/a.gif?';
    // 拼接请求字符串
    for(var i in param) {
     str += i + '=' + param[i];
    }
    // 发送统计请求
    _img.src = str;
  }
})()

// 测试用例, 统计sum
Count({num: 10});

另一种常用的代理模式是JSONP,在日常我们引入script标签时,需要的代理对象是对页面与浏览器间通信的,而跨域时,我们利用script标签的src属性实现get请求,即在src指向的URL上面添加一些字段信息,然后服务器获取这些字段,在相应的生成一份内容。
参考如下的例子

// 前端浏览器页面


// 另一个域下服务器接口

代理模板
其解决思路是:既然不同域之间相互调用对方的页面是有限制的,那么自己域中的两个页面之间相互调用时可以的,即代理页面B调用被代理的页面A中对象的方式是可以的。那么要实现这种方式我们只需在被访问的域中请求返回的Header重定向到代理页面,并在代理页面中处理被代理的页面A就可以了。
即我们需要在自己的域中有A、B两个页面。此时我们可以将自己的域称为X域,另外的域称为Y域,在X域中要有一个被代理的页面A,在页面A中有三个部分:第一部分是发送请求的模块,如form表单提交,负责向Y域发送请求,并提供额外的两组数据,其一是要执行的回调函数名称,其二是X域中代理模板所在的路径,并将target目标指向内嵌框架;第二部分是内嵌框架,如iframe,负责提供第一部分中form表单响应目标target的指向,并将嵌入X域中的代理页面作为子页面,即B页面。第三部分就是回调函数,负责处理返回的数据。
X域中被代理A页面



X域中代理页面B,主要负责将自己页面的URL中的searcher部分的数据解析出来。将数据重新组装好,调用A页面里的回调函数,将组装好的数据作为参数传入父页面中定义的回调函数中并执行。


最后是Y域中被请求的接口文件C,它的主要工作是将X域过来的请求的数据解析并获取回调函数字段与代理模版路径字段数据,打包返回,并将自己的Header重定向为X域的代理模版B所在的路径。


代理模式除了应用于跨域,当对象实例化对资源开销比较大,如页面加载初期加载的文件很多,此时能够延迟加载一些图片对页面首屏加载时间收益还是很大的;在比如图片预览页面,页面中有很多图片面对这么多图片如果一一加载对资源的开销也大,所以通常是当用户点击某张图片时加载这张图片,但如果该图片源文件也很大,此时我们常用的做法是先代理加载一张预览图片,然后讲加载原图替换这张图片,成为虚拟代理。
总结:
代理对象可以完全解决被代理对象与外界对象之间的耦合;从被代理的页面角度看来是一种保护代理;从服务器角度看是一种远程代理。
代理模式也可以解决系统资源开销较大的问题,通过代理对象可以保护被代理对象,使被代理对象拓展性不受外界影响。也可以解决某一交互或某一需求中造成大量系统开销。
缺点:
无论代理模式在处理系统、对象之间的耦合度问题还是解决系统资源开销问题,它都会构建出一个复杂的代理对象,增加系统复杂度,同时增加一定的系统开销,当然这些开销有时是可以接受的。

装饰者模式

在不改变原对象的基础上通过对其进行包装拓展(添加属性或者方法)使得原有对象可以满足用户更复杂的需求。
例如:更改大量点击事件回调函数的时候,我们可以避免逐一修改。

// 装饰者
var decorator = function (input, fn) {
  // 获取事件源
  var input = document.getElementById(input);
  // 若事件源已经绑定事件
  if (typeof input.onclick === 'function') {
    // 缓存事件源原有回调函数
    var oldClickFn = input.onclick;
    // 为事件源定义新的事件
    input.onclick = function () {
      // 事件源原有回调函数
      oldClickFn();
      fn();
    }
  } else {
    // 事件源未绑定事件,直接为其绑定事件
    input.onclick = fn;
  }
  // ...
}

与适配器模式进行对比
1.适配器是对原有对象适配,添加的方法与原有方法功能上大致相似;
装饰器模式提供的方法与原来的方法功能项有有一定的区别。
2.适配器新增的方法是要调用原来的方法的,很多时候是对对象内部结构的重构,了解自身结构十分必要;
装饰器中不需要了解对象原有功能,对原方法正常使用,是对对象的一种良性拓展。

桥接模式

在系统沿着多个维度变化的同时,又不增加其复杂度并以达到解耦。
应用场景:
有时候页面中一些小细节改变常常因逻辑相似导致大片臃肿代码,此时我们看看有采用桥接模式。
在日常写代码的时候我们一定要注重对相同逻辑作抽象处理,这能是我们的代码更加简洁,重用率很大,可读性更高。而在桥接模式正是先抽象提取公共部分,然后将实现与抽象通过桥接方法连接在一起来实现解耦的。
例如抽出特效绑定的公用部分

// 抽象
function changeColor(dom, color, bg) {
  // 设置元素的字体颜色
  dom.style.color = color;
  // 设置元素的背景颜色
  dom.style.background = bg;
}

var spans = document.getElementsByTagName('span');
spans[0].onmouseover = function () {
  changeColor(this, 'red', '#ddd');
}
spans[0].onmouseout = function () {
  changeColor(this, '#333', '#f5f5f5');
}

想上面的方式编写模式,当我们在想对需求做任何修改我们只需修改changeColor的内容即可,不必到每个回调中去修改,并且这种方式看起来更加清晰,这是以新增一个桥接函数(此处为匿名的回调函数)为代价实现的。
桥接模式的强大之处也在于其也适用多维的变化。比如用canvas写一个跑步游戏的时候,对游戏中的人、小精灵、小球等一系列的实物都有动作单元,而他们的动作实现起来又是统一的,比如人、精灵、球的运动其实就是x、y的变化,球的颜色与精灵的色彩的绘制方式相似等,这样我们可以将这些多维的变化部分提取出来作为一个抽象运动单元进行保存,而当我们创建实体时,将需要的每个抽象动作单元通过桥接,链接在一起运作,这样他们之间不会相互影响并且该方法降低了它们之间的耦合。

// 多维变量类
// 运动单元
function Speed (x, y) {
  this.x = x;
  this.y = y;
}
Speed.prototype.run = function () {
  // ...
 }

// 着色单元
function Color (cl) {
  this.color = cl;
}
Color.prototype.draw = function () {
  // ...
}

// 变形单元
function Shape (sp) {
  this.shape = sp;
}
Shape.prototype.change = function () {
  // ...
}

// 说话单元
function Speek (wd) {
  this.word = wd;
}
Speek.prototype.say = function () {
  // ...
}

其使用方法如下

function Ball (x, y, c) {
  // 实现运动单元
  this.speed = new Speed(x, y);
  // 实现着色单元
  this.color = new Color(c);
}
Ball.prototype.init = function () {
  // 实现运动
  this.speed.run();
  // 实现着色
  this.color.draw();
}

实例化

var ball = new Ball(10, 20, 16);
ball.init();

总结:
桥接模式最主要的特点是将实现层(如元素绑定的事件)与抽象层(如装饰页面UI逻辑)解耦分离,使两部分可以独立变化,由此看出桥接模式主要是对结构之间的连接。这种方式能够有效避免需求的改变造成对内部对象的修改,体现了面向对象对拓展的开放及修改的关闭原则。
缺点:
有时会增加开发成本,性能可能会受到影响。

组合模式

将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。当然这种模式中要求这些“部分”的接口统一,在JavaScript中我们可以通过继承同一个虚拟类来实现。
例如,针对文字新闻,直播新闻和图片新闻等类型新闻都继承一个新闻虚拟父类News,参考如下代码:

var News = function () {
  // 子组件容器
  this.children = [];
  // 当前组件元素
  this.element = null;
}

News.prototype = {
  init: function () {
    throw new Error('请重写你的方法');
  },
  add: function () {
    throw new Error('请重写你的方法');
  },
  getElement: function () {
    throw new Error('请重写你的方法');
  }
}

通常虚拟类是定义而不实现的,但是上面代码中在虚拟类的构造函数中定义两个特权变量是因为后面的所有继承子类都要声明这两个变量,为了简化子类我们也可以将这些共有的变量提前声明在父类中。
另外,这里我们还要注意虚拟父类和子类的关系。因为组合模式不仅仅是单层次组合,也可以是多层次的,我们可以将组合后的整体作为一部分继续组合。这样就应该在拆分整体后还能确定他们的层次关系,所以在我们写代码的时候就要注意到这些。
值得注意的是,在组合模式中我们用到了继承。

// 容器类构造函数
var Container = function (id, parent) {
  // 构造函数继承父类
  News.call(this);
  // 模块id
  this.id = id;
  // 模块父容器
  this.parent = parent;
  // 构建方法
  this.init();
}

// 寄生式继承父类原型方法
inheritPrototype(Container, News);

// 容器内构造函数
Container.prototype.init = function () {
  this.element = document.createElement('ul');
  this.element.id = this.id;
  this.element.className = 'new-container';
};

// 添加子元素方法
Container.prototype.add = function (child) {
  // 子元素容器中插入子元素
  this.children.push(child);
  // 插入当前组件元素树中
  this.element.appendChild(child.getElement());
  return this;
}

// 获取当前元素的方法
Container.prototype.getElement = function () {
  return this.element;
}

// 显示方法
Container.prototype.show = function () {
  this.parent.appendChild(this.element);
}

下一层级的行成员集合类及以后的新闻组合体类实现的方式与之相似:

var Item = function (classname) {
  News.call(this);
  this.classname = classname || '';
  this.init();
}

inheritPrototype(Item, News);

Item.prototype.init = function () {
  this.element = document.createElement('li');
  this.element.className = this.classname;
}

Item.prototype.add = function (child) {
  // 在子元素容器中插入子元素
  this.children.push(child);
  // 插入当前组件元素树中
  this.element.appendChild(Child.getElement());
  return this;
}

Item.prototype.getElement = function () {
  return this.element
}

var NewsGroup = function (classname) {
  News.call(this);
  this.classname = classname || '';
  this.init();
}

inheritPrototype(NewsGroup, News);

NewsGroup.prototype.init = function () {
  this.element = document.createElement('div');
  this.element.className = thia.classname;
}

NewsGroup.prototype.add = function(child) {
  // 在子元素容器中插入子元素
  this.children.push(child);
  // 插入当前组件元素树中
  this.element.appendChild(child.getElement());
  return this;
}

NewsGroup.prototype.getElement = function () {
  return this.element
}

具体组合使用,创建新的父类,参考如下代码:

var ImageNews = function (url, href, classname) {
  News.call(this);
  this.url = url || '';
  this.href = href || '#';
  this.classname = classname || 'normal';
  this.init();
}

inheritPrototype(ImageNews , News);

ImageNews.prptotype.init = function () {
  this.element = document.createElement('a');
  var img = new Image();
  img.src = this.url;
  this.element.appendChild(img);
  this.element.classname = 'image-news' + this.classname;
  this.element.href = this.href;
}
ImageNews.prototype.add = function () {}
ImageNews.prototype.getElement = function () {
  return this.element;
}

var IconNews = function (text, href, type) {
  News.call(this);
  this.url = text || '';
  this.href = href || '#';
  this.type = type || 'video';
  this.init();
}

inheritPrototype(IconNews, News);

IconNews.prptotype.init = function () {
  this.element = document.createElement('a');
  this.element.innerHTML = this.text;
  this.element.href = this.href;
  this.element.classname = 'icon' + this.type;
}
IconNews.prototype.add = function () {}
IconNews.prototype.getElement = function () {
  return this.element;
}

var EasyNews = function (text, href) {
  News.call(this);
  this.url = text || '';
  this.href = href || '#';
  this.init();
}

inheritPrototype(EasyNews, News);

EasyNews.prptotype.init = function () {
  this.element = document.createElement('a');
  this.element.innerHTML = this.text;
  this.element.href = this.href;
  this.element.classname = 'text';
}
EasyNews.prototype.add = function () {}
EasyNews.prototype.getElement = function () {
  return this.element;
}

var TypeNews = function (text, href, type, pos) {
  News.call(this);
  this.url = text || '';
  this.href = href || '#';
  this.type = type || '';
  this.pos = pos || '';
  this.init();
}

inheritPrototype(TypeNews, News);

TypeNews.prptotype.init = function () {
  this.element = document.createElement('a');
  if (this.pos === 'left') {
     this.element.innerHTML = '[' + this.type + ']' + this.text;
  } else {
     this.element.innerHTML = this.text + '[' + this.type + ']';
  }
  this.element.href = this.href;
  this.element.classname = 'text';
}
TypeNews.prototype.add = function () {}
TypeNews.prototype.getElement = function () {
  return this.element;
}

使用:

var news1 = new Container('news', document.body);
news1.add(
  new Item('normal').add(
    new IconNews('梅西不拿金球也伟大', '#', 'video')
  )
).add(
  new Item('normal').add(
    new IconNews('保护强国强队用意明显', '#', 'live')
  )
).add(
  new Item('normal').add(
    new NewsGroup('has-img').add(
      new ImageNews('img/1.jpg', '#', 'small')
    ).add(
      new EasyNews('从240斤胖子成功变型男', '#')
    ).add(
      new EasyNews('五大雷人跑步机', '#')
    )
  )
).add(
  new Item('normal').add(
    new TypeNews('AK47不愿为费城打球', '#', 'NBA', 'left')
  )
).add(
  new Item('normal').add(
    new TypeNews('AK47不愿为费城打球', '#', 'CBA', 'right')
  )
)

当然组合模式更多的应用于表单创建:比如注册页面可能有不同的表单提交模块。在这里,我们可以创建基类Base,三个组合类:FormItem、FieldsetItem、Group,以及成员类InputItem、LabelItem、SpanItem、TextareaItem。

var form = new FormItem('FormItem', document.body);
form.add(
  new FieldsetItem('account', '账号').add(
    new Group().add(
      new LabelItem('user_name', '用户名:')
    ).add(
      new InputItem('user_name')
    ).add(
      new SpanItem('4到6位数字或字母')
    )
  ).add(
      new Group().add(
        new LabelItem('user_password', '密 码')
      ).add(
        new InputItem('user_password')
      ).add(
        new SpanItem('6到12位数字或字母')
      )
  ).add(
    // ...
  )
).show();

总结:
组合模式能够给我们提供一个清晰的组成结构,组合对象类通过继承同一个父类使其具有统一的方法,这样也方便恶魔统一管理和使用,当然,此时单体成员与组成体成员行为表现就比较一致了,者也就模糊了简单对象和组合对象的区别。
有时这也是一种对数据的分级式处理。清晰而方便我们对数据的管理和使用。
组合模式虽然对于单体对象的实现简单而又单一,但通过组合将会给我们带来更多使用形式。

享元模式

运用共享技术有效的支持大量的细粒度的对象,避免对象间拥有相同内容造成多余的开销。
它将数据和方法分成内部数据、内部方法、外部数据、外部方法。内部数据和内部方法是指相似或者共有的数据和方法,所以将这一部分提取出来减少开销,提高性能。
通常将内部数据提取出来后要写一个使用他们的方法:

var Flyweight = function () {
  // 已创建的元素
  var created = [];
  // 创建一个新闻包装容器
  function create () {
    var dom = document.createElement('div');
    // 将容器出入新闻列表容器中
    document.getElementById('container').appendChild(dom);
    // 缓存新创建的元素
    created.push(dom);
    // 返回创建的新元素
    return dom
  }
  return {
    // 获取创建新闻元素的方法
    getDiv: function () {
      // 如果已创建的元素小于当前页面总个数,则创建
      if (created.length < 5) {
        return create();
      } else {
         // 获取第一个元素,并插入最后面
        var div = created.shift();
        created.push(div);
        return div;
      }
    }
  }
}();

总结:
享元模式的应用目的是为了提高程序的执行效率与系统的性能,在大型系统开发中应用的比较广泛,百分之一的效率提升有时可能会发生质的改变。它可以避免程序中的数据重复。有时候内存中大量对象,会造成大量内存的占用,所以享元模式来减少内存消耗是很有必要的。不过应用时一定要找准内部状态(数据与方法)和外部状态(数据与方法),这样你才能更合理的提取分离。而在一些小程序中,内存和性能的消耗对程序的执行影响不大时,强行使用享元模式而引入复杂的代码逻辑往往会收到负面效果。

参考文献

张容铭 《JavaScript 设计模式》

你可能感兴趣的:(JavaScript设计模式(三)--结构型设计模式)