作者 | DD菜
原文 | https://zhuanlan.zhihu.com/p/43283016
设计模式就是软件开发过程中形成的套路,就如同你在玩lol中的“正方形打野”,“四一分推”,又或者篮球运动中的“二夹一”,“高位单打”一样,属于经验的总结。
熟悉设计模式有什么好处呢?
让你在编程过程中更有自信,使用经过无数前人印证过的最好的设计,自然底气十足
提升编程效率,避免开发过程中的犹豫
更能掌控项目,方便预估开发时间,方便对团队成员进行管理
由于设计模式和软件开发的语言,平台都没有关系,因此,前端工程师对设计模式也是有需求的。
设计模式是对人类工程历史总结,而不单单只是软件工程。
现在大家谈的前端工程化,如果脱离设计模式,只能算徒有其表,设计模式才是工程化的灵魂。当然,既然是经验和历史总结,有时候并不需要系统地进行学习,口口相传也是可以的,但是单独系统地讲解设计模式,就是要将“公共知识”转变为“共有知识”,戳破皇帝的新衣,让大家真正能言之有物,交流通畅。
类型分类
可以将设计模式分为三种类型,分别为创建型,结构型,和行为型。
创建型模式主要解决对象创建什么,由谁创建,何时创建的3w问题,对类的实例化进行了抽象,分离概念和实现,使得系统更加符合单一职责原则。
结构型模式描述如何将类或者对象组合在一起,形成更大的数据结构,因此也可以分为类结构型和对象结构型。
行为型模型对不同的对象划分责任和算法的抽象,关注类和对象之间的相互作用,同样也分为类和对象。
可以看到三种类型的模式正好解决了编程中的数据结构从哪里来?如何组合?如何交流?的问题。
创建型模式
创建型模式一共有4个,分别为工厂(工厂,工厂方法,抽象工厂合并),建造者,原型,单例。
工厂模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐ 命名建议:xxxFactory,FactoryOfxxx
工厂模式简而言之,就是要替代掉“new操作符”!
为什么需要替代new操作符?
因为有时候创建实例时需要大量的准备工作,而将这些准备工作全部放在构造函数中是非常危险的行为,有必要将创建实例的逻辑和使用实例的逻辑分开,方便以后扩展。
举个例子:
class People {
constructor(des) {
// 出现异步不能使用async await
// 函数调用时可能还未完成初始化
get('someUrl').then(data => {
this.name = data.name
get('someUrl?name=' + this.name).then(data => {
this.age = data.age
})
})
// 非成员函数耦合性变大
this.des = handleDes(des)
}
}
而使用Typescript,配合工厂模式,实现如下:
// 还真别说,形式上好看的代码,质量一般都比较高
class People {
name: string = ''
age: number = 0
des: string = ''
constructor(name: string, age: number, des: string) {
this.name = name
this.age = age
this.des = des
}
}
async function peopleFactory(description:any){
const name = await get('someUrl')
const age = await get('someUrl?name='+name)
const des = handle(description)
return new People(name,age,des)
}
这样的封装,能清楚地分离对象的创建和使用。同时,如果之后的类的定义发生了改变,可以直接修改People,创建类的准备数据发生了改变,则修改工厂函数。
但是,选择工厂模式的原因是因为构造函数足够复杂或者对象的创建面临巨大的不确定性,只需要传入变量即可构造的情况下,用工厂函数实际上是得不偿失的。
几乎所有的设计模式都会带来代码可读性下降的风险,因此需要找到代码可读性降低和可维护性,稳定性之间的平衡!
你也可以用函数根据参数返回相应的工厂函数,又或者用一个类集中管理工厂函数来处理复杂度。
建造者模式
重要程度:⭐⭐⭐⭐ 难度:⭐⭐ 命名建议:xxxBuilder
建造者模式用于直接构建复杂对象,比如上例中的构造函数参数,如果采用一个结构表示:
constructor(peopleConfig:any) {
this.name = peopleConfig.name
this.age = peopleConfig.age
this.des = peopleConfig.des
}
那么有必要将这个人对象的构建单独封装起来:
class PeopleConfigBuilder{
name: string = ''
age: number = 0
des: string = ''
async buildName(){
this.name = await get('someUrl')
}
async buildAge(){
await get('someUrl?name='+this.name)
}
async buildDes(description: any){
this.des = handleDes(description)
}
}
class People {
name: string = ''
age: number = 0
des: string = ''
constructor(peopleConfig: PeopleCofigBuilder) {
this.name = peopleConfig.name
this.age = peopleConfig.age
this.des = peopleConfig.des
}
}
async function peopleFactory(description:any){
const builder = new PeopleConfigBuilder()
builder.buildName()
builder.buildAge()
builder.buildDes()
return new People(builder)
}
当然,仅仅三个属性的对象,远远没有达到复杂对象的程度,因此,只有在对象十分复杂的时候,才需要应用到建造者模式。
原型模式
重要程度:⭐⭐ 难度:⭐ 命名建议:xxxPrototype
创建新对象时是基于一个对象的拷贝,而不是重新实例化一个类。
举例说明,比如上例中的peopleConfig,其实peopleConfig应该是有固定模板的:
function peopleConfigPrototype (){
return {
name: '',
age: 0,
des: ''
}
}
这样每次返回的都是新的对象,也可以相当于是对象的拷贝,但是如果直接拷贝对象,应该怎么写呢?
const peopleConfigPrototype = {
name: '',
age: 0,
des: ''
}
const peopleConfig = Object.create(peopleConfigPrototype)
// 采用Object.create方法,当前对象将被复制到peopleConfig的__proto__上
还有另一种方式进行对象拷贝,但是会丢掉对象中的函数:
const peopleConfig = JSON.parse(JSON.stringfy(peopleConfigProtytype))
注意JSON操作会阻塞线程,导致性能急剧下降,一般不考虑这种方式。
单例模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐ 命名建议:xxxSingle,xxxSingleton,xxxUnum
单例模式的目的是限制一个类只能被实例化一次,防止多次实例化。其中,根据类被实例化的时间,又被分为懒汉单例和饿汉单例。懒汉单例是指在第一次调用实例的时候实例化,饿汉单例是指在类加载的时候就实例化。
/* 懒汉单例 */
class PeopleSingle{
// 静态成员instance
static instance = null
// 私有构造函数
private constructor(){ }
public static getInstance(){
if(PeopleSingle.instance === null){
PeopleSingle.instance = new PeopleSingle()
}
return PeopleSingle.instance
}
}
PeopleSingle.getInstance()
/* 饿汉单例 */
class PeopleSingle{
static instance = new PeopleSingle()
private constructor(){ }
}
PeopleSingle.instance
四种创建模式都有其使用场景,需要针对使用场景进行组合,才能写出高质量的代码。
结构型模式
结构型模式一共有7种:适配器,桥接,组合,装饰,外观,享元,代理
适配器模式
重要程度:⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxAdapter,xxxWraper
想想你的转接头,实际上就是被适配对象(adaptee)上套上一层封装,将其接口与目标对象(target)相匹配,所以适配器又叫wraper(包皮)。
比如,有一个目标类UsbC:
class UsbC{
slowCharge(){
console.log('slow charging')
}
superCharge(){
console.log('super charging')
}
}
有一个被适配目标MicroUsb:
class MicroUsb{
slowCharge(){
console.log('slow charging')
}
}
所以adapter是如此:
// 精髓在implements target上
class MicroToCAdapter implements UsbC{
microUsb: MicroUsb
constructor(microUsb: MicroUsb){
this.microUsb = microUsb
}
slowCharge(){
this.microUsb.slowCharge()
}
superCharge(){
console.log('cannot super charge, slow charging')
}
}
// 这样就可以直接
new MicroTOCAdapter(new MicroUsb()).superCharge()
适配器模式对多个不同接口的匹配非常有效,实际情况中没有必要完全使用类来封装,一个函数也可以搞定。
桥接模式
重要程度:⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxBridge,xxx(具体实现)
桥接模式的主要目的,是将抽象与实现解耦,使得二者可以独立地进行变化,以应对不断更细的需求。
其实通俗地来说,就是将所有概念想象成“灵魂——肉体”,凡是能用这个概念代入的,都可以用桥接模式重构。
比如汽车这个概念和颜色这个概念,可以将颜色作为汽车的成员变量,但是当颜色变得更加复杂时,比如渐变,模糊,图案等属性加入,不得不将其解耦,桥接模式就很重要了。
我们先定义抽象类Car和Color(Ts的抽象类功能对于实现之一模式非常重要):
abstract class Color {
color: string
abstract draw(): void
}
abstract class Car {
color: Color
abstract setColor(color: Color): void
}
再定义其实例:
class Red extends Color {
constructor() {
super()
}
draw() {
this.color = 'red'
}
}
class Van extends Car {
constructor() {
super()
}
setColor(color: Color) {
this.color = color
}
}
抽象类和实现是解耦的,这时候我们如果要利用所有的类,就需要一个桥接类:
class PaintingVanBridge {
van: Car
red: Color
constructor() {
this.red = new Red()
this.red.draw()
this.van = new Van()
this.van.setColor(this.red)
}
}
桥接模式会增加大量代码,所以一定要在使用之前对功能模块有一个恰当的评估!
装饰模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐ 命名建议:xxxDecorator,xxx(具体实现)
装饰模式是在现有类或对象的基础上,添加一些功能,使得类和对象具有新的表现。
还是之前的Car和Color的问题,可以直接继承Car,添加颜色,这是一个装饰模式:
class Car {
name: string
constructor(name: string) {
this.name = name
}
}
class Benz extends Car {
color: string
constructor(name: string, color: string) {
super(name)
this.color = color
}
}
但是采用继承的方式是静态的,而且会导致在继承复用的过程中耦合,比如Car2继承Car,在创建新的子类时错把Car2作为父类,结果就很容易出错了。
为了解决这个问题,可以采用Ts的装饰器特性:
function colorDecorator<T extends { new(...args: any[]): {} }>(color: string) {
return function (constructor: T) {
return class extends constructor {
name = 'shit'
color = color
}
}
}
@colorDecorator<Car>('red')
class Car {
name: string
constructor(name: string) {
this.name = name
}
}
装饰器会拦截Car的构造函数,生成一个继承自Car的新的类,这样更加灵活(但是注意这个过程只发生在构造函数阶段)。
外观模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐ 命名建议:xxx(具体实现)
简单一句话总结:“封装复杂,接口简单”,为所有的子系统提供一致的接口,比如轮胎,方向盘和车。
class Tyre{
name: string
constructor(name: string){
this.name = name
}
}
class Steering{
turnRight(){}
turnLeft(){}
}
interface CarConfig{
tyreName: string
ifTurnRight: boolean
}
class Car{
tyre:Tyre
steering:Steering
constructor(carConfig: CarConfig){
this.tyre = new Tyre(carConfig.name)
this.steering = new Steering()
if(carConfig.ifTurnRight){
this.steering.turnRight
}
}
}
可以活用Typescript的接口功能实现这一模式。
享元模式
重要程度:⭐ 难度:⭐⭐ 命名建议:xxx(具体实现)
享元模式避免重新创建对象,其实只要有缓存对象的意思,并且共用一个对象实例,就是享元模式。
比如需要对一个Car的实例进行展示(可以搭配工厂模式):
class Car{
name: string
color: string
changeColor(color: string){
this.color = color
}
changeName(name: string){
this.name = name
}
}
class CarFactory{
static car: Car
static getCar():Car{
if(CarFactory.car === null){
CarFactory.car = new Car()
}
return CarFactory.car
}
}
CarFactory.getCar().changeColor('red')
注意,由于是使用的同一个引用,因此会存在修改的问题。
代理模式
重要程度:⭐⭐⭐⭐ 难度:⭐ 命名建议:xxxProxy
对接口进行一定程度的隐藏,用于封装复杂类。
比如Car有很多属性,我们只需要一个简单的版本:
class Car{
a: number = 1
b: number = 2
c: number = 3
d: number = 4
name: string = 'name'
test(){
console.log('this is test')
}
}
class CarProxy{
private car: Car
name: number
constructor(){
if(this.car === null){
this.car = new Car
}
this.name = this.car.name
}
test(){
this.car.test()
}
}
行为型模式
行为型模式一共有5种:命令,中介者,观察者,状态,策略
命令模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐ 命名建议:xxxCommand
命令模式的主要目的是让请求者和响应者解耦,并集中管理。
比如大家常用的请求,其实可以这样封装:
function requestCommand(command: string){
let method = 'get'
let queryString = ''
let data = null
let url = ''
const commandArr = command.split(' ')
url = commandArr.find(el=>el.indexOf('http'))
const methods = commandArr.filter(el=>el[0]==='-')
methods[0].replace('-','')
method = methods[0]
const query = commandArr.filter(el=>el.indexOf('='))
if(query.length > 0){
queryString = '?'
query.forEach(el=>{
queryString += el + '&'
})
}
const dataQuery = commandArr.filter(el=>el[0]==='{')
// 对json的判断还不够细致
data = JSON.parse(dataQuery)
if(method === 'get' || method === 'delete'){
return axios[method](url+query)
}
return axios[method](url+query,data)
}
requestCommand('--get https://www.baidu.com name=1 test=2')
requestCommand('--post https://www.baidu.com {"name"=1,"test":2}')
注意命令模式需要提供详尽的文档,并且尽可能集中管理。
中介模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐⭐⭐ 命名建议:xxxCotroller,xxxMiddleWare,xxx(具体实现)
全权负责两个模块之间的通讯,比如MVC,MVVM就是非常典型的中介模式。
中介模式,桥接模式,代理模式的区别是:
代理模式一对一,只能代理特定类和对象,是对其的扩展或是约束。
桥接模式一对多,是对类或对象成员或属性的扩展。
中介模式多对多,全权承包所有两个概念间的关系。
比如4s店,车,和买家之间的关系:
class Car{
name: string = 'Benz'
}
class Buyer{
name: string = 'Sam'
buy(car: Car){
console.log(`${this.name}购买了${car.name}`)
}
}
class FourSShop{
constructor(){
const benz = new Car()
const sam = new Buyer()
sam.buy(benz)
}
}
可以想象中介模式是一个立体的概念,可以理解成是两个概念发生关系的地点。
观察者模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxObserver,xxxEventHandler
观察者模式的目的是为了“检测变更”,既然要检测变更,自然需要记录之前的信息:
class Observer{
states: string[] = []
update(state: string){
this.states.push(state)
}
}
class People{
state: string = ''
observer: Observer
// 可以用getter setter优化
setState(newState: string){
if(this.state !== newState){
this.state = newState
this.notify(this.state)
}
}
notify(state: string){
if(this.observer !== null){
this.observer.update(state)
}
}
setObserver(observer: Observer){
this.observer = observer
}
}
const observer = new Observer()
const people = new People().serObserver(observer)
people.setState('shit')
console.log(observer.state)
可以把观察者模式看成是“记录事件”,这对于理解观察者模式和状态模式区别很有帮助。
实际上前端很多事件处理,就是基于观察者模式的,在上例中的update中的state,就是事件名称,js的事件循环会轮流处理states的状态变化。
状态模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxState
与观察者模式相对,表示的是“记录状态”,只要状态变更,表现即不同,这是设计数据驱动的基础。
class State{
tmp: string
set store(state: string){
if(this.tmp !== state){
// do something
this.tmp = state
}
}
get store(): string{
return this.tmp
}
}
class People{
state: State
constructor(state: State){
this.state = state
}
}
const state = new State()
const people = new People(state)
state.store = 1
console.log(people.state.store)
当然,如果一个数据接口既能记录事件,又能记录状态,可以么?
这就是传说中的响应式数据流,也就是大家平时使用的ReactiveX。
策略模式
重要程度:⭐⭐⭐ 难度:⭐⭐⭐⭐ 命名建议:xxxStratege
策略模式表示动态地修改行为,而行为有时候是一系列方法和对象的组合,与命令模式的区别也在这里。
比如从中国到罗马,可以如此封装:
class Location{
position: string
constructor(poosition: string){
this.position = position
}
}
class Stratege{
locations: Location [] = []
constructor(...locations){
this.locations = locations
console.log('路线经过了')
this.locations.forEach(el=>{
console.log(el.position+',')
})
}
}
class Move{
start: Location
end: Location
stratege: Stratege
constructor(){
this.start = new Location('1 1')
this.end = new Location('0 0')
const sea = new Location('0 1')
const land = new Location('1 0')
this.stratege = new Stratege(this.start,sea,this.end)
}
}
设计模式根植于面向对象思想,也就是任何实现都要区分概念(类)和实例(对象),也就是要分清楚白马和马,这样才能竟可能减轻扩展和团队协作的负担。
但是任何东西有利就有弊,扬长避短才是我们应该在意的方向。