Skip to content
On this page

设计模式

本期主要学习5个经典的设计模式,分别是单例模式观察者模式适配器模式策略模式装饰器模式
以及学习观察者模式发布-订阅者模式在Vue中的应用。

单例模式

创建型模式,提供了一种创建对象的最佳方式。
指一个类永远只有一个活跃的实例,确保只有单个对象被创建。
本质是初始化时,判断是否有某个实例属性(当前实例对象this),
有则永远返回这个实例对象,没有就添加到属性上。
可以减少内存的占用,常常用于管理全局状态和数据

js
class Logger {
	constructor(){
		if(Logger.instance){
			return Logger.instance
			// 如果有实例了,返回这个实例给新对象。
		}
		Logger.instance = this;
	}
	log(){
		console.log('111')
	}
}

const log1 = new Logger();
const log2 = new Logger();
console.log(log1 === log2); //true
log1.log() //111

例如: VuexRedux就是基于Flux, 采用全局单例模式.
很多第三方库,例如lodash,jquery等等,也是一样,多次引用只会使用同一个对象

观察者模式

定义了对象间的一对多依赖关系,
当一个对象发生改变时,依赖于它的对象会立即得到通知并自动更新。 其中,就是被观察者,负责通知各个观察者数据更新了。
被观察者将保存所有依赖于它的观察者们,以便于通知。

js
// 定义一个被观察者类 -- Subject 类似于广播站
class Subject{
	constructor(){
		this.observers = [];
		// 定义一个属性,数组, 用于保存依赖于它的观察者们。
	}
	// 定义对应的方法
	addObserver(obj){
		this.observers.push(obj)
	}
	removeObserver(obj){
		let index = this.observers.indexof(obj);
		this.observers.splice(index,1);
	}
	notifyObserver(msg){
		this.observers.forEach(e => {
			// 通知每一个观察者,要更新了
			// 遍历执行观察者的更新方法。
			e.updated(msg);
		})
	}
}

// 定义一个观察者类
class Observer{
	// 自己的更新方法
	updated(msg){
		console.log(msg);
	}
}


const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
// 添加依赖关系  ---  多个观察者依赖于一个被观察者
subject.addObserver(observer1)
subject.addObserver(observer2)

// 通知所有观察者,消息变了
subject.notifyObserver('要更新了');
// 要更新了 x2  打印两次

适配器模式

开发了新的接口以后,为了兼容旧的接口,可以使用适配器模式。
本质是将旧接口的实例化对象,作为新接口的一个属性
这样就可以调用该属性,使用旧接口方法。

js
class OldApp{
	doingsomethingOld(){
		console.log('旧接口中做旧的事情')
	}
}

class NewApp{
	constructor(){
		this.oldInstance = new OldApp();
	}
	doingsomethingNew(){
		console.log('新接口中做新的事情');
		this.oldInstance.doingsomethingOld();
	}
}

const newapp = new NewApp();
newapp.doingsomethingNew(); 
//调用的新接口,但是可以借助属性(继承对象),间接调用旧接口中的方法。

策略模式

本质是将多个策略类的实例,放置到一个控制类中的属性上,
运行控制类的同一个方法时,通过控制类的设置方法切换策略类实例,
灵活的切换策略类,进而触发不同策略类的同名方法。

js
// 策略类A
class StrategyA{
	sayHello(){
		console.log('StrategyA...')
	}
}

// 策略类B
class StrategyB{
	sayHello(){
		console.log('StrategyB...')
	}
}

// 控制类,外界使用
class Consroller{
	// 构造函数上,传入一个实例对象
	// 将其他策略纳入,作为一个属性使用。
	constructor(strategy){
		this.strategy = strategy
	}
	// 一个切换策略的方法
	setStrategy(strategy){
		this.strategy = strategy
	}
	// 一个使用策略类中方法的方法
	sayHello(){
		this.strategy.sayHello();
	}
}

const strategya = new StrategyA();
const strategyb = new StrategyB();
const consroller = new Consroller(strategya);
// 默认是策略a
consroller.sayHello();  //StrategyA...
// 切换策略类
consroller.setStrategy(strategyb);
// 再次调用时,因为切换了属性对应的实例对象,
// 就调用策略b的方法了。
consroller.sayHello(); //StrategyB...

装饰器模式

通过将原始类实例化对象纳入其中,作为装饰器类属性
通过调用装饰器实例对象的方法,间接调用原始类方法
用于动态扩展原始类中的方法,且不与原始类有任何继承关系,不修改原始类。
适配器模式也可以扩展,但是适配器模式是事后补救,且有继承关系。
例外:装饰器模式是通过调用同名方法,触发原始类同名方法
而适配器模式是通过调用新方法,触发旧类的旧方法

js
class Origin{
	sayHello(){
		console.log('Origin...')
	}
}

class Decorator{
	// 注意这里与适配器模式的区别
	/*
		constructor(){
			this.oldInstance = new OldApp();
		}
	*/
	constructor(origin){
		this.origin = origin;
	}
	// 注意这里一般是同名,与适配器有区别
	sayHello(){
		this.origin.sayHello();
		console.log('Decorator...')
	}

}

const origin = new Origin();
const decorator = new Decorator(origin);
decorator.sayHello();
// 'Origin...'
// 'Decorator...'

简要总结

  • 单例模式 概念: 永远只返回一个实例对象 实现思路:
  1. 初始化时,将第一个实例对象作为一个属性(例如instance)。
  2. 判断instance是否有值,有值就返回这个值作为new的对象。 作用:单向数据流,减少空间占用,管理全局状态和数据,Vuex,Redux, jQuery 等库常见。
  • 观察者模式 概念: 一个一对多的依赖关系,设计一个被观察者类
    将所有依赖对象纳入数组中,使用一个属性保存起来。
    被观察者类中,可以便捷的通知所有依赖(观察者)类。 实现思路:
  1. 初始化时,定义一个属性(例如deps),作为保存依赖的数组。
  2. 定义添加/删除依赖方法,接收传入依赖实例,将依赖添加/删除进deps数组。
  3. 有更新时,定义通知依赖方法,遍历通知deps所有依赖。 作用:Vue中的Observer类和Watcher类的处理。
  • 适配器模式 概念:设计一个兼容旧接口的新接口,调用新接口方法时,同时触发旧接口方法。
  1. 初始化时,定义一个属性(例如oldInstance),保存旧接口的实例化对象,本质是new继承。
  2. 定义一个新方法,新方法内调用oldInstance的旧接口方法。 作用:当需要设计新接口时,旧接口不想被移除,采用兼容模式补救。
  • 策略模式 概念:设计一个控制类,以及切换策略的方法。灵活切换策略类。
  1. 初始化时,接收一个默认的策略类实例作为一个属性(例如strategy)
  2. 定义设置策略类的方法,当传入新的策略类实例, 更换属性值。
  3. 定义一个方法,触发多个策略类同名方法中的某一个 作用:类似于开关,或者委托的思想,一个控制类灵活切换,多个策略类中的同名方法
  • 装饰器模式 概念:设计一个装饰器类,动态扩展某个实例对象的属性和方法。
    VS适配器:适配器有继承,内部保存new旧接口,初始化不接收自定义实例,是为兼容而设计。
    VS装饰器:装饰器不含继承关系,是为动态扩展而设计。初始化接收自定义实例
  1. 初始化时,定义一个属性(例如instance), 接收并保存一个自定义实例化对象
  2. 定义一个方法,内部触发instance的同名方法,
  3. 接着方法内部,继续添加instance的别的方法或属性,动态扩展。

观察者模式与发布订阅的区别

观察者模式:

  • 主要关注一对多的关系
    • 即一个对象(主题/被观察者)维护一个或多个依赖对象(观察者)。
  • 当主题对象状态发生变化时,
    • 它会通知所有观察者对象,观察者对象会自动更新。
  • 观察者对象一般不知道其他观察者的存在,它们只与主题对象进行交互。
  • 观察者模式提供了一种松耦合的方式来实现对象间的交互和通信
  • 例子:一个新闻订阅系统,
    • 用户可以订阅不同的新闻频道,当有新的新闻发布时,订阅该频道的用户会收到通知并更新。

发布-订阅模式:

  • 通过一个消息中心(或称为事件总线)来实现
    • 发布(发布者/发送者)和订阅(订阅者/接收者)之间的解耦,
    • 发布者和订阅者不直接相互通信,而是通过消息中心进行消息传递。
  • 发布者将消息发布到消息中心,订阅者通过订阅感兴趣的消息类型来接收消息。
    • 当新的消息发布到消息中心时,订阅了相应消息的订阅者会收到这个消息。
  • 发布-订阅模式可以用于解耦复杂的系统组件,让它们彼此独立、灵活地进行通信和交互。
  • 例子:一个购物网站提供了不同类型的促销活动,
    • 用户可以订阅感兴趣的促销活动,当有新的促销活动发布时,订阅了该活动的用户会收到通知。

关键区别:

  • 交互联系:
    • 观察者模式中,观察者直接订阅主题对象,主题对象直接通知观察者;
    • 而发布-订阅模式中,发布者和订阅者之间不直接通信,而是通过消息中心进行交互。
  • 适用场景:
    • 观察者模式更加简单直接,适用于一对多的场景;
    • 而发布-订阅模式可以实现更松散的耦合,适用于更复杂的系统组件交互。
  • 通知的同步异步:
    • 观察者模式的通知是同步的,
      • 即主题对象直接调用观察者对象的方法;
    • 而发布-订阅模式的通知是异步的,
      • 发布者将消息发布到消息中心,订阅者从消息中心接收消息。

总结:

  • 观察者模式注重对象之间的依赖关系,
  • 而发布-订阅模式则以消息中心作为中介实现解耦,适用于更复杂、灵活的系统架构。
  • 根据具体需求选择适合的模式。

Vue中的应用

观察者模式

观察者模式机制大大简化了Vue中的状态管理数据驱动视图的过程。

  1. Vue的响应式系统:
  • 通过使用Vue.observabledata选项定义的数据,
  • 会被转换成可观察的对象,并建立起观察者和依赖之间的关系。
  • Vue通过一个称为依赖收集的过程,来跟踪每个可观察对象的依赖关系。
  • 当数据发生改变时,触发相应的依赖更新过程。
  • 相关的观察者会被通知,并进行更新。

具体细节:

  • 当将数据传递给Vue实例时,Vue会遍历这些数据的属性,
  • 并在每个属性上使用Object.defineProperty来定义getter和setter。
  • 在getter中,Vue会收集当前依赖的观察者,并将其添加到依赖列表中。
  • 当数据发生改变时,setter会通知依赖列表中的观察者进行更新。

发布-订阅者模式

发布-订阅者模式, 允许组件之间进行松耦合的通信状态共享
一些特定的场景下,Vue 也使用了发布-订阅者模式。

  1. Vue 事件系统
  • Vue 提供了一个全局的事件系统,允许组件之间进行通信。
  • 组件可以使用 $emit 方法触发一个自定义事件,
  • 其他组件通过 $on 方法来订阅该事件并接收通知。
  • 这种机制和发布-订阅者模式类似。
  1. VueX State Management
  • VueX 是一个用于状态管理的库,它提供了一个全局的数据仓库,
  • 组件可以通过 store 对象访问和修改数据。
  • 当一个组件修改了 store 中的数据,
  • 其他组件可以通过订阅 store 中的相关数据来获取变化的通知。
  • 这种机制也符合发布-订阅者模式的思想。
  1. Vue Router
  • Vue Router 是 Vue.js 官方的路由管理器,
  • 用于实现单页应用的路由功能。
  • 当路由发生变化时,Vue Router 使用发布-订阅者模式
  • 来通知相关的组件进行相应的路由切换和更新。
  1. Vue 的自定义事件和钩子函数
  • 可以使用自定义事件和生命周期钩子函数,
  • 来实现父子组件之间的通信和组件生命周期的控制。
  • 这些事件和钩子函数的触发和订阅机制, 也符合发布-订阅者模式。

一个陪你成长的前端博客 - XDocs