在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案,即为设计模式。

举个例子,假设在某大型商场下有个空地拿来停车,如果空地上没有按一定的规则划分好车位,则来一辆车就会看哪里有空间直接停下来,久而久之可能里面的车就杂乱无章了。而按照一定的规则划分好车位,按车位进行停车,虽然增加了一定的成本,但是为后续的维护带来了极大的便利,这就是一种设计模式。

学习设计模式,有助于在软件开发中写出可复用和可维护性高的程序代码。

设计原则

在讲JavaScript中常见的设计模式之前,我们先来讲讲设计的原则是什么。

单一职责原则(SRP)

一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。应该把对象或方法划分成较小的粒度,使其在后续的使用过程中拥有更高的可复用性。

最少知识原则(LKP)

一个软件实体应当尽可能少地与其他实体发生相互作用,应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的联系,可以转交给第三方进行处理,降低模块之间的耦合度。

开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以扩展的,但不可修改的。当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,而尽量避免改动程序的源代码,防止影响原系统的稳定。在模块的设计和封装中,需要充分考虑模块的API接口的设计是否合理和可扩展。

常见设计模式

单例模式

单例模式就是保证一个类仅有一个实例,并提供一个访问它的全局访问点。要想实现单例模式,即在实现方法中先判断实例是否存在,如果存在则直接返回,如果不存在就创建一个再返回。

举个例子,例如只能定义一个管理员,则不管调用几次都只能生成一个管理员:

class Singleton {
  constructor(name) {
    this.name = name;
    this.getName();
  }
  getName() {
    return this.name;
  }
}

let getInstance = (function(){
        var instance = null;
        return function(name){
            if(!instance){
                instance = new Singleton(name);
            }
            return instance;
        }
    }
)()

const a = getInstance('Tom');
const b = getInstance('Jack');

console.log(a===b); //true

策略模式

策略模式就是定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。其核心是将算法的使用和算法的实现分离开来。

一个基于策略模式的程序至少由两部分组成:第一部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程;第二部分是环境类Context,Context接受客户的请求,随后把请求委托给某一个策略类,要做到这点,说明Context中要维持对某个策略对象的引用。

举个例子,例如要买什么样的车需要花多少的钱:

const Car = {
  Benz() {
    return 100+'万元';
  },
  Volvo() {
    return 60+'万元';
  },
  QQ() {
    return 10+'万元';
  }
};

let needMoney = function(carType) {
  return `要想买${carType}汽车,需要花费${Car[carType]}。`;
}

console.log(needMoney('QQ'));

代理模式

代理模式就是为一个对象提供一个代用品或占位符,以便控制对它的访问。

举个例子,例如图片的懒加载:

let imgFunc = (function() {
    let imgNode = document.createElement('img');
    document.body.appendChild(imgNode);
    return {
        setSrc(src) {
            imgNode.src = src;
        }
    }
})();

let proxyImage = (function() {
    let img = new Image();
    img.onload = function() {
        imgFunc.setSrc(this.src);
    }
    return {
        setSrc(src) {
            imgFunc.setSrc('loading,gif');
            img.src = src;
        }
    }
})();

proxyImage.setSrc('pic.png');

发布-订阅模式

发布-订阅模式也被称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JavaScript中通常使用注册回调函数的形式来订阅。

举个例子,JavaScript中的事件就是经典的发布-订阅模式的实现:

// 订阅
document.body.addEventListener('click', function(){
  console.log('trigger click1.');
}, false);

document.body.addEventListener('click', function(){
  console.log('trigger click2.');
}, false);

// 发布
document.body.click();// trigger click1. trigger click2.

适配器模式

适配器模式就是解决两个软件实体间的接口不兼容的问题、对不兼容的部分进行适配。

举个例子,第三方接口只接受数组作为参数,则需要对原始数据进行处理:

// 第三方接口
function thirdInterface(arr) {
  arr.forEach(function(item){
    console.log(item);
  });
}

// 将对象转为数组
function objToArr(data) {
  let temp = [];
  for(let i in data) {
    temp.push(data[i]);
  }
  return temp;
}

const data = {
  0: 'A',
  1: 'B',
  2: 'C'
};
thirdInterface(objToArr(data));

迭代器模式

迭代器模式是指提供一种方法,顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

举个例子,使用迭代器模式实现一个类似map的方法:

function each(obj, callback) {
  let value;
  
  if (Array.isArray(obj)) {
    for (let i=0; i<obj.length; i++) {
      value = callback.call(obj[i], i, obj[i]);
      
      if (value === false) {
        break;
      }
    }
  } else {
    for (let i in obj) {
      value = callback.call(obj[i], i, obj[i]);
      
      if (value === false) {
        break;
      }
    }
  }
}

each([1,2,3], function(index, value) {
  console.log(index, value);
});

each({a:1, b:2, c:3}, function(index, value) {
  console.log(index, value);
});

中介者模式

中介者模式是指所有的相关对象都通过中介者对象来通信,而不是互相引用。所以当一个对象发生改变时,只需要通知中介者对象即可。

举个例子,通过中介者来计算排名:

const A = {
  score: 10,
  changeTo(score) {
    this.score = score;
    // 自己获取排名
    this.getRank();
  },
  getRank() {
    let scores = [this.score, B.score, C.score].sort(function(a,b) {
      return a < b;
    })
    
    console.log(scores.indexOf(this.score) + 1);
  }
};

const B = {
  score: 20,
  changeTo(score) {
    this.score = score;
    // 通过中介者获取排名
    rankMediator(B);
  }
};

const C = {
  score: 30,
  changeTo(score) {
    this.score = score;
    // 通过中介者获取排名
    rankMediator(C);
  }
};

// 中介者,计算排名
function rankMediator(person) {
  let scores = [A.score, B.score, C.score].sort(function(a,b) {
    return a < b;
  });
  
  console.log(scores.indexOf(person.score) + 1);
}

// A通过自身处理来获取排名
A.changeTo(100);

// B和C则有中介者进行处理
B.changeTo(200);
C.changeTo(50);

总结

其实设计模式已经融入到我们开发的方方面面了,只不过我们没有注意到而已。本文讲述了一些常用的设计模式,还有很多的设计模式如命令模式、状态模式、修饰者模式等没有阐述,感兴趣的读者可以通过《JavaScript设计模式》自行学习。通过学习设计模式,我们可以在平时的开发中产出更简洁优雅的代码、更高可复用的代码和更高可维护性的代码。