# 【设计模式】前端开发进阶的必经之路
# 为什么要掌握设计模式
- 设计模式作为面向对象编程的最佳实践,在开发过程中我们可以通过设计模式的思想来优化我们的代码,使得我们的代码更加具有可复用性,可扩展性,可维护性,使得代码低耦合高内聚,更加优雅和纯粹
# 设计模式的六大原则
单一职责
- 也就是说一个程序只负责一个功能
- 对于负责的功能需要做到拆分,保持每个程序的独立
开闭原则
- 预留扩展扩展,但是不支持修改暴力修改源代码,在需要扩展的时候不应该修改源代码的内部逻辑
里式置换原则
- 类似于数学中的补集,需要保证子集完全属于父集,在父级的范围内需要子集能完全替换父集
接口独立原则
- 避免杂糅的将多个接口合在一起,保持接口的独立,类似于单一职责,但是这里的注重点在接口
依赖倒置原则
- 抽象不应该依赖于具体类,具体类应当依赖于抽象这个概念类似于我们面向对象编程而不是面向过程编程
迪米特法则
- 在编写代码时候,应该减少代码直接的互相耦合性,跟单一职责类似
# 工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类通过参数来返回不同的实例,被创建的实例通常都具有共同的父类在不暴露创建对象的具体逻辑,而是将逻辑进行封装
class Product {
constructor(name) {
this.name = name;
}
init() {
console.log("init");
}
fun() {
console.log("fun");
}
}
class Factory {
create(name) {
return new Product(name);
}
}
// use
let factory = new Factory();
let p = factory.create("p1");
p.init();
p.fun();
- 优点:
- 用户只需要通过工厂生产产品,不需要了解产品的生产过程
- 若需要新增产品时只需要扩展一个工厂即可
- 缺点
- 每次新增产品的时候都需要新增一个产品类,增加了系统的复杂程度
# 单例模式
单例模式的核心就是创建一个唯一的对象,确保某一个类只有一个实例,如果该类已经创建过实例则直接返回该实例,否则创建一个实例保存并返回,并提供一个访问它的全局访问点
class Shop {
static instance = null;
constructor() {
this.goods = [];
}
buy(goods) {
this.goods = [...this.goods, ...goods];
}
static getInstance() {
if (!this.instance) {
this.instance = new Shop();
}
return this.instance;
}
}
const shop1 = Shop.getInstance();
const shop2 = Shop.getInstance();
shop1.buy(["apple", "mango"]);
shop2.buy(["banana", "tomato"]);
console.log(shop1.goods, shop2.goods);
- 优点:
- 提供了对唯一实例的受控访问
- 内存中只有一个实例,减少了内存的开销
- 缺点:
- 没有抽象层,很难进行扩展,且单点访问,可能导致模块间的强耦合 从而不利于单元测试
# 装饰器模式
不改变原有对象的前提下,动态地给一个对象增加一些额外的功能,使原有对象可以满足用户的更复杂需求,但是不会影响从这个类中派生的其他对象与继承同理
class Man {
run() {
console.log("run");
}
}
class Decorator {
constructor(old) {
this.oldAbility = old.run;
}
fly() {
console.log("fly");
}
newAbility() {
this.oldAbility();
this.fly();
}
}
const man = new Man();
const superMan = new Decorator(man);
superMan.fly();
- 优点:
- 符合“开闭原则”,装饰者和被装饰者可以独立变化,实现了解耦
- 方便动态的扩展功能,且比继承更灵活
- 缺点:
- 装饰者模式需要创建一些具体装饰类,尤其多层的装饰会增加系统的复杂度
# 观察者模式(发布订阅模式)
定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新在设计模式上我们定义为观察者模式,其实发布订阅模式是以观察者模式为思想,属于观察者模式的扩展实践
- 虽然观察者模式和发布订阅模式存在补集关系,但还是存在实践上的差异的:
- 观察者模式(observer):观察者直接订阅消息,当消息更新的时候,会触发观察者里的通知
- 发布订阅模式(subscribe):订阅者
Subscriber
把订阅事件和操作注册到调度中心Event Channel
,当发布者Publisher
触发事件更新时,由>调度中心统一执行订阅者注册到调度中心的处理代码- 实际上发布订阅在异步中用的比较多,而观察者在同步中用的多
class Group {
constructor() {
this.parents = [];
this.msg = "";
}
notify(msg) {
this.msg = msg;
this.parents.forEach((parent) => {
parent.update();
});
}
attach(parent) {
this.parents.push(parent);
}
}
class Parent {
constructor(name, group) {
this.name = name;
this.group = group;
this.group.attach(this);
}
update() {
console.log(this.name, this.group.msg);
}
}
const group = new Group();
const p1 = new Parent("p1", group);
const p2 = new Parent("p2", group);
const p3 = new Parent("p3", group);
group.notify("msg1");
group.notify("msg2");
//发布订阅模式
class Publish {
constructor() {
this.event = {};
}
emit(eventName, payload) {
if (this.event[eventName]) {
this.event[eventName].forEach((callback) => {
callback(payload);
});
}
}
on(eventName, callback) {
if (!this.event[eventName]) {
this.event[eventName] = [];
}
this.event[eventName].push(callback);
}
off(eventName, callback) {
if (!this.event[eventName]) {
return;
}
const index = this.event[eventName].findIndex((cb) => cb === callback);
if (index > -1) {
this.event[eventName].splice(index, 1);
}
}
once(eventName, callback) {
let cb = () => {
callback();
this.off(eventName, cb);
};
this.on(eventName, cb);
}
}
const publish = new Publish();
function callback1() {
console.log("event1");
}
function callback2() {
console.log("event2");
}
publish.on("event1", callback1);
publish.on("event1", callback1);
publish.off("event1", callback1);
publish.once("event2", callback2);
publish.emit("event1");
publish.emit("event2");
publish.emit("event2");
- 优点:
- 观察者模式建立了触发更新机制,会向所有已注册的观察者对象发送通知,在关系上属于抽象耦合的,简化了一对多的流程
- 缺点:
- 不能跟踪所订阅的目标是怎么变化的,可能会导致在逻辑上难以维护和理解
# 职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间耦合在一起,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
//同步职责链
// orderType 1 500定金;2 200定金 、 pay 是否已支付定金、stock 库存
let order500 = (orderType, pay, stock) => {
if (orderType === 1 && pay) {
console.log("500定金,得100优惠券");
} else {
return "nextProcessor";
}
};
let order200 = (orderType, pay, stock) => {
if (orderType === 2 && pay) {
console.log("200定金,得50优惠券");
} else {
return "nextProcessor";
}
};
let orderNormal = (orderType, pay, stock) => {
if (stock) {
console.log("普通购买,不得优惠券");
} else {
console.log("莫得购买");
}
};
class Chain {
constructor(fn) {
this.fn = fn;
this.processor = null;
}
setNextProcessor(processor) {
return (this.processor = processor);
}
passRequest() {
const res = this.fn.apply(this, arguments);
if (res === "nextProcessor") {
return this.processor && this.processor.passRequest(...arguments);
}
return res;
}
next() {
return this.processor && this.processor.passRequest(...arguments);
}
}
const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrderNormal = new Chain(orderNormal);
chainOrder500.setNextProcessor(chainOrder200);
chainOrder200.setNextProcessor(chainOrderNormal);
chainOrder500.passRequest(1, true, 500);
chainOrder500.passRequest(2, true, 500);
chainOrder200.passRequest(1, true, 500);
// 异步职责链
let fn1 = new Chain(() => {
console.log(1);
return "nextProcessor";
});
let fn2 = new Chain(function () {
console.log(2);
setTimeout(() => {
this.next();
}, 1000);
});
let fn3 = new Chain(() => {
console.log(3);
});
fn1.setNextProcessor(fn2).setNextProcessor(fn3);
fn1.passRequest();
- 优点:
- 在处理过程中无需明确发送者和接收者双方的明确信息,不需要知道链中的结构,降低耦合度和对象的复杂度
- 在给对象分派职责时,具有更多的灵活性,可以通过对责任链动态的增加或删除来改变一个请求的职责
- 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接
- 缺点:
- 不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个默认值的接受者节点来处理这种即将离开链尾的请求
- 对于比较长的职责链,请求的处理可能涉及到多个处理对象,在性能消耗方面不是最优解
- 在流程上由于没有明确接收者,不能保证每一条请求都被接受,可能存在请求被遗漏
# 最后
- 当然设计模式不止以上,本章不以列举所有设计模式为目的,只是作为体验设计模式的引导,让自己在码途上有一点进步,对设计模式也感兴趣的小伙伴也可以去看一下《JavaScript 设计模式》