导言
设计模式最先是由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides 4 位计算机科学家在1994年出版的一本名为《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)书中提出,这本书系统地介绍了23种常用的设计模式,被认为是设计模式领域的里程碑,对软件设计和开发产生了深远的影响。这些设计模式经过实践证明,在软件设计和开发中可以提高代码的可复用性、灵活性和可维护性,因此受到了广泛的应用。
你可能不知道这23种设计模式,但可能已经在编码中已经用到了。因为设计模式并非新的领域或技术,只是对编程实践的总结,通俗的说就是编程的套路吧。
套路。
江湖上有个解释是说:套路是被反复验证了的方法。
解释很确切。同理,设计模式其实也是前辈大师们在编程过程中不断总结出来的套路。在武侠小说中,每一套武功大致就是一套设计模式。
设计模式共23种,可以分为三大类:创建型、结构性和行为型。如下图所示:
创建型设计模式
创建型设计模式主要解决的是对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。常用的是单例模式、工厂模式和创造者模式。
单例模式是用来创建全局唯一的对象。工厂模式可以细分成简单工厂、工厂方法和抽象工厂这3种。其中抽象工厂模式一般用的较少。建造者模式构建较复杂对象,接收参数来定制对象生成。
单例模式
单例模式,全称单例设计模式(Singleton Design Pattern),它指的是一个类中只允许创建一个对象或实例,那这个类就是单例类,这种设计模式就称为单例设计模式。
唯一性
刚在单例模式定义中说一个类中值允许存在一个对象或实例,那这个类的范围是什么呢,指的是在同一个进程中只存在一个对象或实例,还是在同一个线程中?答案是后者。也就是单例模式创建的对象是进程内唯一的。
进程和线程之前也介绍过。我们编写的代码,通过编译组织在一起,生成一个可执行文件,例如Windows中的.exe文件。我们双击启动这个程序,系统就会启动一个新的进程,将文件从磁盘加载到进程的空间地址,每个进程被分配的空间地址是独立的。接着,就会执行一条条程序的语句,当执行到User user = new User的时候,就会在进程的空间地址创建个user临时变量和User对象。如果在旧进程执行过程中fork了个新进程,系统会给新的进程重新分配地址空间,彼此独立。
在JavaScript中
从单例模式的定义中也能看出,主要应用于面向对象的类中。但JavaScript其实没有真正意义上的类(虽然ES6新增了类的语法糖,但一直被诟病),如果生搬硬套单例模式的概念,没有什么意义。在JavaScript中创建对象非常简单,可以不需要用到类。如:
let obj = {}
单例模式的核心是仅拥有一个对象,并可以全局访问。在JavaScript中,上面简单的全局对象obj,也符合单例模式的特点,但在应用中,都这样定义,那就会造成全局污染,可能会出现覆盖的可能。在JavaScript中,我们应用单例模式的思路大体是在对象中定义一个对象变量,当对象存在时,则直接返回,不存在则会新建。
let CreateDiv = function(html) {
this.html = html
this.init()
}
CreateDiv.prototype.init = function () {
let _div = document.createElement('div')
_div.innerHtml = this.html
document.body.appendChild(_div)
}
// 代理
let ProxySingletonCreateDiv = (function (html) {
let instance = null
return function (html) {
if (!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()
const a = new ProxySingletonCreateDiv('a')
const b = new ProxySingletonCreateDiv('b')
console.log(a === b) // true
工厂方法模式
工厂模式,重点在工厂,好像在说废话,哈哈。在我们物质社会,工厂的作用就是批量生产出某一类或几类商品的地方,每类商品都有其固定的属性和形状之类的。同理,我们编程中的工厂模式,也是希望能够快速生成某个对象,并且对象中包含有某些特定的属性和方法。
一般来说,工厂模式可以细分为:简单工厂、工厂方法和抽象工厂三种模式。在GoF的《设计模式》一书中,他将简单工厂定义为工厂方法的一种特例,所以,工厂模式只被分成了工厂方法和抽象工厂两种模式。而抽象工厂原理稍微复杂,实际应用中场景不多,我们主要看看工厂方法模式。
具体该如何理解,我们先来看段JavaScript代码:
function createPerson (name, age, job) {
let obj = new Object()
obj.name = name
obj.age = age
obj.job = job
obj.sayName = function () {
console.log(this.name)
}
return obj
}
const person1 = createPerson('zhangsan', 36, 'Doctor')
const person2 = createPerson('lisi', 28, 'teacher')
person1.sayName() // 'zhangsan'
person2.sayName() // 'lisi'
定义了一个方法,该方法拥有name、age、job属性和sayName方法,通过不同的传参可以定制生成不同属性的对象,但类型和结构是一致的。如果不接收传参,函数里的属性或方法是固定的,执行生成的对象属性方法都相同,这就是简单工厂设计模式,也叫做静态工厂方法模式。
箭头函数其实也是一种工厂模式,当箭头函数整体由单个表达式组成的话,在函数创建时会间接返回一个对象,也算是一种小型工厂模式。
const user = (userName) => ({userName})
user('zhangsan') // userName: "zhangsan"
抽象工厂模式
有时候工厂方法中处理的逻辑很复杂,可能会根据不同入参类型来生成差异对象,如果差异尽在一个维度的时候,还比较好处理,可以通过if判断或后面说的策略模式来实现,但如果有多个维度,就很复杂了,这种情况就需要引入抽象工厂模式,在具体生成对象的的方法上来抽象一层,代码就会清晰很多。抽象工厂模式在实际开发中应用场景不多,我们先了解一下。
结构性设计模式
结构性设计模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。比较常用的有代理模式、桥接模式、装饰器模式和适配器模式。
代理模式主要作用是控制访问,而非加强功能,这也是和装饰器模式最大的不同之处。装饰器对复杂的继承解构,用组合来替代继承,给原始类增强功能。桥接模式更通用的理解就是“组合由于继承”的原则。适配器模式更多的是一种因设计缺陷补漏来对类或对象的适配。
行为型设计模式
行为型设计模式主要解决的就是“类或对象之间的交互”问题。比较常用的有观察者模式、模板模式、策略模式、职责链模式、状态模式和迭代器模式。
观察者模式将观察者和被观察者代码解耦。应用场景非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。模板模式比较好理解,它既能提供功能的复用,又可以有度的扩展。策略模式定义一组策略算法,应用于不同场景下,也可以利用它来优化if else等冗长的判断。职责链模式就是进行分工,各功能独立开,可配合执行。状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。迭代器模式主要作用是解耦容器代码和遍历代码。大部分编程语言都提供了现成的迭代器可以使用,我们不需要从零开始开发。
这篇主要简单介绍了三大类常用的设计模式。下一篇开始,我们来详细介绍每一种设计模式。