想必大家都听说过DRY原则,其实就是Don't repeat yourself(不要重复你自己),意思就是不要重复写一样的代码,换句话说就是要提高代码的复用性。那什么样的代码才算有好的复用性呢?

对象可以重复利用。这个其实有点像我们关系型数据库的设计原则,数据表和关系表是分开的,数据表就是单纯的数据,没有跟其他表的关系,也没有业务逻辑,关系表才是存储具体的对应关系。当我们需要某个数据时,直接读这个表就行,而不用担心这个表会有其他的业务在里面。类似设计的还有 redux,redux 的 store 里面就是单纯的数据,并不对应具体的业务逻辑,业务如果需要改变数据需要发 action 才行。正是因为这种数据很单纯,所以我们需要的地方都可以拿来用,复用性非常高。所以我们设计数据或对象时,也要尽量让他可以复用。重复代码少。如果你写的代码重复度很高的话,说明你代码的抽象度不够。很多时候我们重复代码的产生都是因为我们可能需要写一个跟已经存在的功能类似的功能,于是我们就把之前的代码拷贝过来,把其中两行代码改了完事。这样做虽然功能实现了,但是却制造了大量重复代码,本文要讲的几种设计模式就是用来解决这个问题的,提高代码的抽象度,减少重复代码。模块功能单一。这意味着一个模块就专注于一个功能,我们需要做一个大功能时,就将多个模块组合起来就行。这就像乐高积木,功能单一的模块就像乐高积木的一小块,我们可以用 10 个小块拼成一个小汽车,也可以用 20 个小块拼成一个大卡车。但是如果我们模块本身做复杂了,做成了小汽车,我们是不能用两个小汽车拼成一个大卡车的,这复用性就降低了。

提高复用性的设计模式主要有桥接模式享元模式模板方法模式,下面我们分别来看下。

桥接模式

桥接模式人如其名,其实就相当于一个桥梁,把不同维度的变量桥接在一起来实现功能。假设我们需要实现三种形状(长方形,圆形,三角形),每种形状有三种颜色(红色,绿色,蓝色),这个需求有两个方案,一个方案写九个方法,每个方法实现一个图形:

function redRectangle() {}
function greenRectangle() {}
function blueRectangle() {}
function redCircle() {}
function greenCircle() {}
function blueCircle() {}
function redTriangle() {}
function greenTriangle() {}
function blueTriangle() {}

上述代码虽然功能实现了,但是如果我们需求变了,我们要求再加一个颜色,那我们就得再加三个方法,每个形状加一个。这么多方法看着就很重复,意味着他有优化的空间。我们仔细看下这个需求,我们最终要画的图形有颜色和形状两个变量,这两个变量其实是没有强的逻辑关系的,完全是两个维度的变量。那我们可以将这两个变量拆开,最终要画图形的时候再桥接起来,就是这样:

function rectangle(color) {     // 长方形
  showColor(color);
}

function circle(color) {     // 圆形
  showColor(color);
}

function triangle(color) {   // 三角形
  showColor(color);
}

function showColor(color) {   // 显示颜色的方法
  
}

// 使用时,需要一个红色的圆形
let obj = new circle('red');

使用桥接模式后我们的方法从3 * 3变成了3 + 1,而且如果后续颜色增加了,我们只需要稍微修改showColor方法,让他支持新颜色就行了。如果我们变量的维度不是 2,而是 3,这种优势会更加明显,前一种需要的方法是x * y * z个,桥接模式优化后是x + y + z个,这直接就是指数级的优化。所以这里桥接模式优化的核心思想是观察重复代码能不能拆成多个维度,如果可以的话就把不同维度拆出来,使用时再将这些维度桥接起来。

实例:毛笔和蜡笔

桥接模式其实我最喜欢的例子就是毛笔和蜡笔,因为这个例子非常直观,好理解。这个例子的需求是要画,三种型号的线,每种型号的线需要 5 种颜色,如果我们用蜡笔来画就需要 15 支蜡笔,如果我们换毛笔来画,只需要 3 支毛笔就行了,每次用不同颜色的墨水,用完换墨水就行。写成代码就是这样,跟上面那个有点像:

// 先来三个笔的类
function smallPen(color) {
  this.color = color;
}
smallPen.prototype.draw = function() {
  drawWithColor(this.color);    // 用color颜色来画画
}

function middlePen(color) {
  this.color = color;
}
middlePen.prototype.draw = function() {
  drawWithColor(this.color);    // 用color颜色来画画
}

function bigPen(color) {
  this.color = color;
}
bigPen.prototype.draw = function() {
  drawWithColor(this.color);    // 用color颜色来画画
}

// 再来一个颜色类
function color(color) {
  this.color = color;
}

// 使用时
new middlePen(new color('red')).draw();    // 画一个中号的红线
new bigPen(new color('green')).draw();     // 画一个大号的绿线

上述例子中蜡笔因为大小和颜色都是他本身的属性,没法分开,需要的蜡笔数量是两个维度的乘积,也就是 15 支,如果再多一个维度,那复杂度是指数级增长的。但是毛笔的大小和颜色这两个维度是分开的,使用时将他们桥接在一起就行,只需要三只毛笔,5 瓶墨水,复杂度大大降低了。上面代码的颜色我新建了一个类,而上个例子画图形那里的颜色是直接作为参数传递的,这样做的目的是为了演示即使同一个设计模式也可以有不同的实现方案。具体采用哪种方案要根据我们实际的需求来,如果要桥接的只是颜色这么一个简单变量,完全可以作为参数传递,如果要桥接一个复杂对象,可能就需要一个类了。另外上述代码的三个笔的类看着就很重复,其实进一步优化还可以提取一个模板,也就是笔的基类,具体可以看看后面的模板方法模式。

实例:菜单项

这个例子的需求是:有多个菜单项,每个菜单项文字不一样,鼠标滑入滑出时文字的颜色也不一样。我们一般实现时可能这么写代码:

function menuItem(word) {
  this.dom = document.createElement('div');
  this.dom.innerHTML = word;
}

var menu1 = new menuItem('menu1');
var menu2 = new menuItem('menu2');
var menu3 = new menuItem('menu3');

// 给每个menu设置鼠标滑入滑出事件
menu1.dom.onmouseover = function(){
  menu1.dom.style.color = 'red';
}
menu2.dom.onmouseover = function(){
  menu1.dom.style1.color = 'green';
}
menu3.dom.onmouseover = function(){
  menu1.dom.style1.color = 'blue';
}
menu1.dom.onmouseout = function(){
  menu1.dom.style1.color = 'green';
}
menu2.dom.onmouseout = function(){
  menu1.dom.style1.color = 'blue';
}
menu3.dom.onmouseout = function(){
  menu1.dom.style1.color = 'red';
}

上述代码看起来都好多重复的,为了消除这些重复代码,我们将事件绑定和颜色设置这两个维度分离开:

// 菜单项类多接收一个参数color
function menuItem(word, color) {
  this.dom = document.createElement('div');
  this.dom.innerHTML = word;
  this.color = color;        // 将接收的颜色参数作为实例属性
}

// 菜单项类添加一个实例方法,用于绑定事件
menuItem.prototype.bind = function() {
  var that = this;      // 这里的this指向menuItem实例对象
  this.dom.onmouseover = function() {
    this.style.color = that.color.colorOver;    // 注意这里的this是事件回调里面的this,指向DOM节点
  }
  this.dom.onmouseout = function() {
    this.style.color = that.color.colorOut;
  }
}

// 再建一个类存放颜色,目前这个类的比较简单,后面可以根据需要扩展
function menuColor(colorOver, colorOut) {
  this.colorOver = colorOver;
  this.colorOut = colorOut;
}

// 现在新建菜单项可以直接用一个数组来循环了
var menus = [
  {word: 'menu1', colorOver: 'red', colorOut: 'green'},
  {word: 'menu2', colorOver: 'green', colorOut: 'blue'},
  {word: 'menu3', colorOver: 'blue', colorOut: 'red'},
]

for(var i = 0; i < menus.length; i++) {
  // 将参数传进去进行实例化,最后调一下bind方法,这样就会自动绑定事件了
  new menuItem(menus[i].word, new menuColor(menus[i].colorOver, menus[i].colorOut)).bind();
}

上述代码也是一样的思路,我们将事件绑定和颜色两个维度分别抽取出来,使用的时候再桥接,从而减少了大量相似的代码。