为什么要了解用设计模式
1.写代码要有良好的封装,要高内聚,低耦合
2.设计模式为了解决在软件设计、开发过程中,针对特定场景、特定问题的较优解决方案
3.不使用设计模式进行开发容易造成,因设计缺陷、代码实现缺陷,给后期维护、开发、迭代带来了麻烦。
什么是高内聚,低耦合?
即五大基本原则(SOLID)的简写
高层模块不依赖底层模块,即为依赖反转原则。
内部修改关闭,外部扩展开放,即为开放封闭原则。
聚合单一功能,即为单一功能原则。
低知识要求,对外接口简单,即为迪米特法则。
耦合多个接口,不如独立拆分,即为接口隔离原则。
合成复用,子类继承可替换父类,即为里式替换原则
为什么要封装代码?
我们可以观察React、Vue、EventEmitter、Axios等等这些优秀的源码,会发现其实他们封装的模块都是有迹可循的。这些规律总结起来就是设计模式。
- 良好的封装,不会让内部变量污染外部
- 封装好的代码可以作为一个模块给外部调用。外部无需了解细节,只需按约定的规范调用。
- 对扩展开放,对修改关闭,即开放关闭原则。外部不能修改内部代码,保证了内部的正确性;又留出扩展接口,提高了灵活性。
模式类型
模式类型 | 设计模式 |
---|---|
创建型模式 | 单例模式、工厂模式、建造者模式 |
结构型模式 | 适配器模式、装饰器模式、代理模式 |
行为型模式 | 策略模式、观察者模式、发布订阅模式、职责链模式、中介者模式 |
观察者模式
特征
观察者模式:定义了对象间一种一对多的依赖关系,当目标对象 Subject 的状态发生改变时,所有依赖它的对象 Observer 都会得到通知。
一个目标者对象 Subject, 拥有方法: 添加 / 删除 / 通知 Observer;
多个观察者对象 Observer, 拥有方法: 接收 Subject 状态变更通知并处理;
目标对象 Subject 状态变更时, 通知所有 Observer。
Subject 添加一系列 Observer, Subject 负责维护与这些 Observer 之间的联系,“ 你对我有兴趣, 我更新就会通知你”。
优缺点
优点 目标变化就会通知观察者,这是观察者模式最大的优点 缺点 不灵活。目标和观察者是耦合在一起的,要实现观察者模式,必须同时引入被观察者和观察者才能达到响应式的效果。
观察者模式示例:
// 目标者类
class Subject {
constructor() {
this.observers = []; //观察者列表
}
// 添加
add(observer) {
this.observers.push(observer);
}
// 删除
remove(observer) {
let idx = this.observers.findIndex(item => item === observer);
idx > -1 && this.observers.splice(idx, 1);
}
//通知
notify() {
for (let observer of this.observers) {
observer.update();
}
}
}
// 观察者类
class Observer {
constructor(name) {
this.name = name;
}
//目标对象更新时触发的回调
update() {
console.log(`目标者通知我更新了,我是:${this.name}`)
}
}
// 实例化目标者
let subject = new Subject();
console.log(subject, 'subject')
// 实例化两个观察者
let obs1 = new Observer('前端开发者');
let obs2 = new Observer('后端开发者');
// 向目标者添加观察者
subject.add(obs1);
subject.add(obs2);
console.log(subject, 'subject2')
// 目标者通知更新
subject.notify();
问题:不能单独通知一个比如只通知前端开发者,两个观察者接收目标者状态变更通知后,都执行了 update(),并无区分。所以进阶版的观察这模式也可称为发布订阅者模式,可以解决这个问题
例子:领导(被观察者)在台上介绍防疫政策,底下的工作人员(观察者)“观察”领导说的防疫政策的变化,当政策变化时,通知(update)到街道。
let observerIds = 0;
//被观察者
class Subject {
constructor() {
this.observers = [];
}
//添加观察者
addObserver(observer) {
this.observers.push(observer)
}
//移除观察者
removeObserver(observer) {
this.observers = this.observers.filter((obs) => {
return obs.id !== observer.id;
})
}
//通知notify观察者
notify() {
this.observers.forEach((observer) => {
observer.update(this)
})
}
}
//观察者
class Observer {
constructor() {
this.id = observerIds++
}
update(Subject) {
// 更新
console.log(`领导通知我更新了,政策变化了,我是:工作人员${this.id}`)
}
}
let subject = new Subject();
console.log(subject, 'subject')
// 实例化两个观察者
let obs1 = new Observer();
let obs2 = new Observer();
// 向目标者添加观察者
subject.addObserver(obs1);
subject.addObserver(obs2);
console.log(subject, 'subject2')
// 目标者通知更新
subject.notify();
发布订阅者模式
基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象
顺序 :先订阅后发布时才通知(常规)
使用场景
1.可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。 示例:
// 事件中心
let pubSub = {
list: {},
// 订阅
subscribe: function(key, fn) {
if (!this.list[key]) {
this.list[key] = [];
}
this.list[key].push(fn);
console.log(this.list, 'this.list')
},
// 发布
publish: function(key, ...arg) {
for (let fn of this.list[key]) {
fn.call(this, ...arg)
}
},
//取消订阅
unSubscribe: function(key, fn) {
let fnList = this.list[key];
if (!fnList) return false;
if (!fn) {
// 不传入指定的取消订阅方法,则青空所有key下的订阅
fnList && (fnList.length = 0)
} else {
fnList.forEach((item, index) => {
if (item === fn) {
fnList.splice(index, 1);
}
})
}
}
}
// 订阅
pubSub.subscribe('onwork', time => {
console.log(`上班了:${time}`)
})
pubSub.subscribe('offwork', time => {
console.log(`下班了:${time}`)
})
pubSub.subscribe('oneat', time => {
console.log(`吃饭了:${time}`)
})
// 发布
pubSub.publish('onwork', '18:00:00')
pubSub.publish('oneat', '12:00:00')
// 取消订阅
pubSub.unSubscribe('onwork')
2.DOM 事件监听也是 “发布订阅模式” 的应用:
3.阅后可获取过往以后的发布通知 (QQ离线消息,上线后获取之前的信息)
4.流行库的应用 jQuery 的 on 和 trigger,$.callback();
Vue 的双向数据绑定;
Vue 的父子组件通信 emit
5.我们微信会关注很多公众号,公众号有新文章发布时,就会有消息及时通知我们文章更新了。
这个时候公众号为发布者,用户为订阅者,用户将订阅公众号的事件注册到事件调度中心,当发布者发布新文章时,会发布事件至事件调度中心,调度中心会发消息告诉订阅者。
示例代码:
class Event {
constructor() {
this.eventEmitter = {}
}
// 订阅
on(type, fn) {
if (!this.eventEmitter[type]) {
this.eventEmitter[type] = []
}
this.eventEmitter[type].push(fn)
}
// 取消订阅
off(type, fn) {
if (!this.eventEmitter[type]) {
return;
}
this.eventEmitter[type] = this.eventEmitter[type].filter((event) => {
return event !== fn
})
}
// 发布
emit(type) {
if (!this.eventEmitter[type]) {
return
}
this.eventEmitter[type].forEach(event => {
event();
});
}
}
观察者模式和发布订阅模式的区别
1.观察者模式中,观察者和目标直接进行交互,观察者由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理。
2.观察者模式和发布订阅模式都是为了解耦,减少代码的冗余,不同的是,观察者模式中观察者必须知道被观察者,而发布订阅模式解耦更彻底,订阅者与发布者不需要相互知道,只需要向事件大厅订阅和发布即可。
3.发布订阅模式与观察者模式的不同,“第三者” (事件中心)出现。目标对象并不直接通知观察者,而是通过事件中心来派发通知。
- 发布订阅模式中,订阅者各自实现不同的逻辑,且只接受自己对应的事件通知。实现你想要的 “不一样”
单例模式
;
单例模式也称为单体模式,规定一个类只有一个实例,并且提供可全局访问点;
单例模式的特点是”唯一“和”全局访问“,那么我们可以联想到JavaScript中的全局对象,全局对象是最简单的单例模式;
(1)一个实例只生产一次 (2)保证一个类仅有一个实例,并提供一个访问它的全局访问点
Singleton
:特定类,这是我们需要访问的类,访问者要拿到的是它的实例;
instance
:单例,是特定类的实例,特定类一般会提供getInstance
方法来获取该单例;
getInstance
:获取单例的方法; 示例代码
// 单例模式
class Singleton {
static _instance = null;
static getInstance() {
if (!Singleton._instance) {
Singleton.instance = new Singleton();
}
//如果这个唯一的实例已经存在,则直接返回
return Singleton._instance
}
}
const s1 = Singleton.getInstance();
const s2 = Singleton.getInstance();
console.log(s1 === s2)
作用
优缺点
优点: 节约资源,保证访问的一致性。
缺点: 扩展性不友好,因为单例模式一般自行实例化,没有接口。
应用场景
如果一个类实例化过程消耗资源比较多,可以使用单例避免性能浪费
需要公共状态,可以使用单例保证访问一致性 (1)登录页面中的浮窗
(2)Vuex:实现了一个全局的store用来存储应用的所有状态。这个store的实现就是单例模式的典型应用。
let Vue // instance 实例
export function install (_Vue) {
// 判断传入的Vue实例对象是否已经被install过(是否有了唯一的state)
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// 若没有,则为这个Vue实例对象install一个唯一的Vuex
Vue = _Vue
// 将Vuex的初始化逻辑写进Vue的钩子函数里
applyMixin(Vue)
}
通过这种方式,可以保证一个 Vue 实例只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store。
其它
四种单例模式的实现可参考 www.jb51.net/article/210…
工厂模式
根据不同的参数,返回不同类的实例。
核心思想:将对象的创建与对象的实现分离。实现复杂,但使用简单。工厂会给我们提供一个工厂方法,我们直接去调用即可。
工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。 工厂模式是最常见的一种开发模式,将new操作单独封装,当遇到new是应考虑工厂模式。创建对象,不暴露代码逻辑,把逻辑写到函数里面。这个函数就是工厂模式。优点,简单。 缺点就是,每增加构造函数,都要修改函数里面的代码逻辑。
(1) 简单工厂模式 简单工厂模式又叫静态工厂模式,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。
在实际的项目中,我们常常需要根据用户的权限来渲染不同的页面,高级权限的用户所拥有的页面有些是无法被低级权限的用户所查看。所以我们可以在不同权限等级用户的构造函数中,保存该用户能够看到的页面。在根据权限实例化用户。代码如下:
class SuperAdmin{
constructor(){
this.name = "超级管理员";
this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理'];
}
}
class Admin{
constructor(){
this.name = "管理员";
this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理'];
}
}
class NormalUser{
constructor(){
this.name = "普通用户";
this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理'];
}
}
//工厂方法类
class UserFactory {
getFactory(role){
switch (role) {
case 'superAdmin':
return new SuperAdmin();
break;
case 'admin':
return new Admin();
break;
case 'user':
return new NormalUser();
break;
default:
throw new Error('参数错误, 可选参数:superAdmin、admin、user');
}
}
}
//调用
let uesr =new UserFactory();
uesr.getFactory('superAdmin');
uesr.getFactory('admin');
uesr.getFactory('user');
class User {
//构造器
constructor(opt) {
this.name = opt.name;
this.viewPage = opt.viewPage;
}
//静态方法
static getInstance(role) {
switch (role) {
case 'superAdmin':
return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
break;
case 'admin':
return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
break;
case 'user':
return new User({ name: '普通用户', viewPage: ['首页', '通讯录', '发现页'] });
break;
default:
throw new Error('参数错误, 可选参数:superAdmin、admin、user')
}
}
}
//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
let normalUser = User.getInstance('user');
优点:你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。但是在函数内包含了所有对象的创建逻辑(构造函数)和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码。当我们的对象不是上面的3个而是30个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。
使用场景
(1)Vue、React 源码中的工厂模式document.createElement
创建 DOM
元素。这个方法采用的就是工厂模式,方法内部很复杂,但外部使用很简单。只需要传递标签名,这个方法就会返回对应的 DOM
元素。
(2)使用者只需要知道产品名字就可以拿到实例,不关心创建过程。所以我们可以把复杂的过程封装在一块,更便于使用。
例如:我们去环球影城的餐厅吃饭,点了一份“牛肉拉面”、“馄饨云吞面”,面煮好了,就直接端到桌子上,我们只管吃,不用在乎煮面的过程。
这个过程中,我们扮演访问者的角色,餐厅扮演的就是工厂的角色,“xxx”面就是产品。
class restaurant {
constructor() {
this.menuData = {}
}
// 获取菜品
getDish(dish) {
if (!this.menuData[menu]) {
console.log('菜品不存在,获取失败');
return;
}
return this.menuData[menu]
}
//添加菜品
addMenu(menu, description) {
if (this.menuData[menu]) {
console.log('菜品存在,请勿重复添加')
return;
}
this.menuData = menu;
}
//移除菜品
removeMenu(menu) {
if (!this.menuData[menu]) {
console.log('菜品不存在,移除失败')
}
delete this.menuData[menu]
}
}
class Dish {
constructor(name, description) {
this.name = name;
this.description = description;
}
eat() {
console.log(`i m eating ${this.name},its ${description
}`)
}
}