SOLID
面向对象设计和编程的五个基本原则
- 单一职责原则(Single Responsibility Principle,SRP):一个类(函数)应该只有一个引起变化的原因。每个类(函数)应该只负责一项单一的职责。这样可以提高代码的可维护性和可测试性。
- 开放封闭原则(Open-Closed Principle,OCP):软件实体(类、模块、函数等)应该对扩展是开放的,对修改是封闭的。这意味着我们应该通过添加新代码来扩展功能,而不是修改现有代码。
- 里氏替换原则(Liskov Substitution Principle,LSP):子类对象应该能够替代任何父类对象而不破坏程序的正确性。换句话说,子类应该符合父类所定义的接口,并且不应该改变父类的行为。
- 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该被迫依赖它们不使用的接口。应该将大型接口拆分为更小的、特定于客户端需求的接口,以避免不必要的依赖关系。
- 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象。这意味着我们应该通过抽象接口或类来解耦高层和低层模块之间的依赖关系。
发布订阅模式(Publish-Subscribe Pattern)
发布订阅模式(Publish-Subscribe Pattern)是一种常用的设计模式,它通过引入一个中间件(通常是事件总线或消息队列)来实现发布者(Publisher)和订阅者(Subscriber)之间的解耦。
用途:发布订阅模式允许组件之间通过事件进行通信,而不需要直接相互引用。发布者发送事件,订阅者监听并响应这些事件。
class EventBus {
constructor() {
this.subscribers = {};
}
subscribe(event, callback) {
if (!this.subscribers[event]) {
this.subscribers[event] = [];
}
this.subscribers[event].push(callback);
}
publish(event, ...args) {
const subscribers = this.subscribers[event];
if (subscribers) {
subscribers.forEach((callback) => callback(...args));
}
}
unsubscribe(event, callback) {
const subscribers = this.subscribers[event];
if (subscribers) {
this.subscribers[event] = subscribers.filter((cb) => cb !== callback);
}
}
}
// 使用示例
const eventBus = new EventBus();
// 订阅事件
eventBus.subscribe('message', (msg) => {
console.log(`Received message: ${msg}`);
});
eventBus.subscribe('error', (err) => {
console.error(`Error occurred: ${err}`);
});
// 发布事件
eventBus.publish('message', 'Hello, world!');
eventBus.publish('error', 'Something went wrong.');
// 取消订阅
eventBus.unsubscribe('message', (msg) => {
console.log(`Received message: ${msg}`);
});
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, fn) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(fn);
}
emit(eventName, ...args) {
this.events[eventName]?.forEach((fn) => fn(...args));
}
once(eventName, fn) {
const cb = (...args) => {
fn(...args);
this.off(eventName, cb);
};
this.on(eventName, cb);
}
off(eventName, fn) {
if (!this.events[eventName]) {
return;
}
this.events[eventName] = this.events[eventName].filter((cb) => cb !== fn);
}
}
// 使用示例
const eventEmitter = new EventEmitter();
eventEmitter.on('event', (data) => console.log(`Event triggered with data: ${data}`));
eventEmitter.emit('event', 'Hello World');
优点
-
解耦:
- 发布者和订阅者之间完全解耦,彼此不知道对方的存在。
- 这种解耦使得系统的各个部分更加独立,更容易维护和扩展。
-
灵活性:
- 可以动态添加或移除订阅者,不会影响到其他订阅者或发布者。
- 一个事件可以有多个订阅者,每个订阅者都可以独立处理事件。
-
扩展性:
- 新增事件或订阅者非常简单,只需注册即可。
- 不需要修改现有代码,符合开闭原则(Open-Closed Principle)。
-
异步处理:
- 发布者发送事件后无需等待订阅者的响应,可以立即返回。
- 订阅者可以在事件发生后异步处理,不影响发布者的执行流程。
缺点
-
性能开销:
- 如果订阅者数量较多,发布事件时需要遍历所有订阅者,可能导致性能瓶颈。
- 特别是在高频事件的情况下,性能问题更为突出。
-
循环引用:
- 如果没有妥善处理订阅关系,可能会导致循环引用的问题。
- 需要小心管理订阅者和发布者的关系,避免内存泄漏。
-
调试困难:
- 由于事件的传递是异步的,调试时很难追踪事件的传播路径。
- 特别是在复杂的系统中,事件的流向和处理逻辑可能变得难以理解。
-
过度设计:
- 对于简单的场景,引入发布订阅模式可能会显得过于复杂。
- 如果系统规模较小,使用简单的回调机制可能更为合适。
-
维护成本:
- 发布订阅模式增加了系统的复杂度,维护成本相对较高。
- 需要更多的代码来管理事件和订阅者,增加了开发和维护的工作量。
总结
发布订阅模式通过引入事件总线或消息队列实现了组件之间的解耦,提高了系统的灵活性和扩展性。然而,这种模式也带来了一些潜在的问题,如性能开销、循环引用和调试困难等。因此,在实际应用中需要权衡利弊,选择合适的设计模式。
观察者模式(Observer Pattern)
用途:实现对象之间的解耦,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。
关键点
-
被观察者(Subject) :
- 维护一个观察者列表。
- 提供方法让观察者注册和注销。
- 当状态发生变化时,通知所有注册的观察者。
-
观察者(Observer) :
- 注册到被观察者上。
- 实现
update方法,用于接收通知并作出相应处理。
interface Observer {
update(data: any): void;
}
class Subject {
private observers: Observer[] = [];
addObserver(observer: Observer): void {
this.observers.push(observer);
}
removeObserver(observer: Observer): void {
this.observers = this.observers.filter((obs) => obs !== observer);
}
notify(data: any): void {
this.observers.forEach((observer) => observer.update(data));
}
}
class ConcreteObserver implements Observer {
update(data: any): void {
console.log(`Observer received data: ${data}`);
}
}
// 使用示例
const subject = new Subject();
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notify('Hello, world!');
subject.removeObserver(observer1);
subject.notify('Goodbye, world!');
解释
-
被观察者(Subject) :
addObserver(observer: Observer):将观察者添加到列表中。removeObserver(observer: Observer):从列表中移除观察者。notify(data: any):通知所有观察者,并传递数据。
-
观察者(Observer) :
update(data: any):接收通知并处理数据。
优点
-
解耦:
- 被观察者不知道具体的观察者是谁,只知道有多少个观察者。
- 观察者只知道如何注册到被观察者上,并不需要了解被观察者的具体实现细节。
-
灵活性:
- 可以动态添加或移除观察者,不会影响到其他观察者或被观察者。
- 一个被观察者可以有多个观察者,每个观察者都可以独立处理事件。
-
扩展性:
- 新增观察者非常简单,只需注册即可。
- 不需要修改现有代码,符合开闭原则(Open-Closed Principle)。
-
异步处理:
- 被观察者发送通知后无需等待观察者的响应,可以立即返回。
- 观察者可以在事件发生后异步处理,不影响被观察者的执行流程。
缺点
-
性能开销:
- 如果观察者数量较多,通知时需要遍历所有观察者,可能导致性能瓶颈。
- 特别是在高频事件的情况下,性能问题更为突出。
-
循环引用:
- 如果没有妥善处理观察关系,可能会导致循环引用的问题。
- 需要小心管理观察者和被观察者的关系,避免内存泄漏。
-
调试困难:
- 由于通知的传递是异步的,调试时很难追踪事件的传播路径。
- 特别是在复杂的系统中,事件的流向和处理逻辑可能变得难以理解。
-
过度设计:
- 对于简单的场景,引入观察者模式可能会显得过于复杂。
- 如果系统规模较小,使用简单的回调机制可能更为合适。
-
维护成本:
- 观察者模式增加了系统的复杂度,维护成本相对较高。
- 需要更多的代码来管理事件和观察者,增加了开发和维护的工作量。
总结
观察者模式通过引入观察者和被观察者之间的解耦机制,提高了系统的灵活性和扩展性。虽然观察者和被观察者不是完全解耦,但这种模式仍然能够有效地降低系统的耦合度,提高可维护性和扩展性。
单例模式(Singleton Pattern)
用途:确保一个类只有一个实例,并提供一个全局访问点。
class SingleCase {
show() {
console.log('我是一个单例对象')
}
static getInstance() {
// 判断是否已经new过1个实例
if (!SingleCase.instance) {
// 若这个唯一的实例不存在,那么先创建它
SingleCase.instance = new SingleCase()
}
// 如果这个唯一的实例已经存在,则直接返回
return SingleCase.instance
}
}
// 使用示例
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // 输出 true
let GlobalUser = (function() {
let instance // 闭包保存的唯一实例对象
return function(name) {
if (instance) return instance
// (首次)创建实例
instance = { name: '张三', id: 1003 }
return instance
}
})() // 立即执行,外层函数的价值就是他的闭包变量instance
console.log(new GlobalUser('张三').name) // 张三
console.log(new GlobalUser('李四').name) // 张三,依然是张三,复用了第一次创建的实例
console.log(new GlobalUser() === new GlobalUser()) // true
优点
-
保证唯一性:
- 确保整个应用程序中只有一个实例,避免资源浪费。
- 适用于全局配置类、日志记录器等场景。
-
全局访问点:
- 提供一个全局访问点,方便全局访问。
- 可以简化代码结构,减少重复代码。
-
易于实现:
- 实现简单,易于理解和使用。
缺点
-
违反单一职责原则:
- 单例类通常承担多种职责,容易导致代码难以维护。
- 违反单一职责原则(Single Responsibility Principle)。
-
难以测试:
- 单例模式使得类难以进行单元测试,因为单例类的状态在整个应用程序中是共享的。
- 难以模拟不同的测试环境。
-
限制扩展性:
- 单例模式限制了类的扩展性,一旦单例类需要扩展功能,可能会导致大量代码改动。
- 不容易进行功能扩展和重构。
-
多线程问题:
- 在多线程环境中,如果处理不当,可能会出现线程安全问题。
- 需要额外的同步机制来保证线程安全。
工厂模式
工厂模式就是把对象的创建 —— new() 封装起来,在工厂里实现对象的创建
简单工厂(Simple Factory)
简单工厂模式,也称为静态工厂模式(Static Factory Method),由一个(静态)类统一管理对象的创建,根据一个简单的类型参数创建不同的示例对象。针对少量的、简单的场景,在工厂中统一实现所有商品的创建
let HuaweiPhone = class { name = '华为' }
let XiaomiPhone = class { name = '小米' }
// 手机生产工厂
class PhoneFactory {
static create(type) {
switch (type) {
case 'huawei':
return new HuaweiPhone()
case 'xiaomi':
return new XiaomiPhone()
default:
throw new Error(`不支持生产型号为${type}的设备`)
}
}
}
// 使用,调用工厂方法创建对象实例
let p1 = PhoneFactory.create('xiaomi')
let p2 = PhoneFactory.create('huawei')
console.log({ p1, p2 })
工厂方法(Factory Method)
工厂方法(也叫工厂模式)就是为每一个产品建立单独的工厂,一个工厂只生产一类商品,这样就可以灵活扩展,相互不影响了。
let HuaweiPhone = class { name = '华为' }
let XiaomiPhone = class { name = '小米' }
// 没什么用的工厂基类(JS中可以不要),用来抽象工厂接口
let IFactory = class { creat() { } }
// 华为手机工厂
class HuaweiFactory extends IFactory {
creat() {
return new HuaweiPhone()
}
}
// 小米手机工厂
class XiaomiFactory extends IFactory {
creat() {
return new XiaomiPhone()
}
}
// 使用
const huaweiFactory = new HuaweiFactory()
let p1 = huaweiFactory.creat()
let p2 = huaweiFactory.creat()
抽象工厂(Abstract Factory)
// 模拟手机抽象类
let IPhone = class { name = '手机' }
// 手机产品
let HuaweiPhone = class extends IPhone { name = '华为' + this.name }
let XiaomiPhone = class extends IPhone { name = '小米' + this.name }
// 模拟电视抽象类
let ITV = class { name = '电视' }
// 电视产品
let HuaweiTV = class extends ITV { name = '华为' + this.name }
let XiaomiTV = class extends ITV { name = '小米' + this.name }
// 模拟抽象工厂,含两个抽象方法,生产手机、电视
let AbstractFactory = class { createPhone() { }; createTV() { } }
// 具体工厂:华为工厂
class HuaweiFactory extends AbstractFactory {
createPhone() {
return new HuaweiPhone()
}
createTV() {
return new HuaweiTV()
}
}
// 具体工厂:小米工厂
class XiaomiFactory extends AbstractFactory {
createPhone() {
return new XiaomiPhone()
}
createTV() {
return new XiaomiTV()
}
}
// 使用
let xiaomiFactory = new XiaomiFactory()
let m1 = xiaomiFactory.createPhone();
let m2 = xiaomiFactory.createTV();
console.log(m1, m2) // XiaomiPhone {name: '小米手机'} XiaomiTV {name: '小米电视'}
策略模式 (Strategy Pattern)
其定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。封装的策略算法一般是独立的,策略模式根据输入来调整采用哪个算法。关键是策略的实现和使用分离。
const PriceCalculate = (function() {
/* 售价计算方式 */
const DiscountMap = {
minus100_30: function(price) { // 满100减30
return price - Math.floor(price / 100) * 30
},
minus200_80: function(price) { // 满200减80
return price - Math.floor(price / 200) * 80
},
percent80: function(price) { // 8折
return price * 0.8
}
}
return {
priceClac: function(discountType, price) {
return DiscountMap[discountType] && DiscountMap[discountType](price)
},
addStrategy: function(discountType, fn) { // 注册新计算方式
if (DiscountMap[discountType]) return
DiscountMap[discountType] = fn
}
}
})()
PriceCalculate.priceClac('minus100_30', 270) // 输出: 210
PriceCalculate.addStrategy('minus150_40', function(price) {
return price - Math.floor(price / 150) * 40
})
PriceCalculate.priceClac('minus150_40', 270) // 输出: 230
策略模式中主要有下面概念:
- Context :封装上下文,根据需要调用需要的策略,屏蔽外界对策略的直接调用,只对外提供一个接口,根据需要调用对应的策略;
- Strategy :策略,含有具体的算法,其方法的外观相同,因此可以互相代替;
- StrategyMap :所有策略的合集,供封装上下文调用;
状态模式(State Pattern)
状态模式是一种行为设计模式,用于通过将对象的行为和状态进行解耦,使得对象能够在不同的状态下具有不同的行为。它允许一个对象在其内部状态改变时更改其行为,而无需改变对象本身的结构。
1. **定义状态接口**(`State Interface`):在 JavaScript 中,我们可以使用类或对象字面量来定义状态接口。状态接口定义了状态类的共同方法,每个具体状态类都必须实现这些方法。
// 状态接口
class StateInterface {
handleAction() {
// 具体状态的行为逻辑
}
}
2. **实现具体状态类**(`Concrete State Class`):具体状态类实现状态接口,并定义每个具体状态的行为逻辑。
// 具体状态类
class ConcreteStateA extends StateInterface {
handleAction() {
// 具体状态A的行为逻辑
}
}
class ConcreteStateB extends StateInterface {
handleAction() {
// 具体状态B的行为逻辑
}
}
3. **定义上下文类**(`Context Class`):上下文类包含状态对象,并提供接口供客户端代码调用。上下文类将具体的状态转换逻辑封装在内部,通常会通过改变当前状态来触发不同的行为。
class Context {
constructor() {
this.state = null; // 当前状态
}
setState(state) {
this.state = state;
}
handleAction() {
this.state.handleAction();
}
}
4. **使用状态模式**:在实际应用中,我们可以通过创建具体的状态对象,并将它们赋值给上下文对象来使用状态模式。
const context = new Context();
// 设置初始状态
context.setState(new ConcreteStateA());
// 调用上下文对象的方法进行具体操作
context.handleAction(); // 根据当前状态,执行对应的行为
// 状态切换
context.setState(new ConcreteStateB());
context.handleAction(); // 根据当前状态,执行对应的行为
状态模式与策略模式很相似:
- 策略模式把可以相互替换的策略算法提取出来
- 状态模式把事物的状态及其行为提取出来。
- 状态模式和策略模式都是选择最合适的方法去调用实现
代理模式(Proxy Pattern)
在不改变原始对象的情况下,通过引入代理对象来控制对原始对象的访问。
// 被代理类
class RealSubject {
request() {
console.log('RealSubject: Handling request.');
}
}
// 代理类
class Proxy {
constructor(realSubject) {
this.realSubject = realSubject;
}
request() {
console.log('Proxy: Pre-processing request.');
this.realSubject.request();
console.log('Proxy: Post-processing request.');
}
}
// 使用代理类
const realSubject = new RealSubject
const proxy = new Proxy(realSubject);
proxy.request();
装饰器模式(Decorator Pattern)
在不改变原对象的基础上,通过对其进行包装拓展,使得原有对象可以动态具有更多功能,从而满足用户的更复杂需求
window.onload = function() {
console.log('原先的 onload 事件 ')
}
/* 发送埋点信息 */
function sendUserOperation() {
console.log('埋点:用户当前行为路径为 ...')
}
/* 给原生事件添加新的装饰方法 */
function originDecorateFn(originObj, originKey, fn) {
originObj[originKey] = function() {
var originFn = originObj[originKey]
return function() {
originFn && originFn()
fn()
}
}()
}
// 添加装饰功能
originDecorateFn(window, 'onload', sendUserOperation)
// 输出: 原先的 onload 事件
// 输出: 埋点:用户当前行为路径为 ...
说白了就是给对象增加属性和方法
适配器模式(Adapter Pattern)
适配器模式(Adapter Pattern)是一种结构型设计模式,主要用于解决不同接口之间的兼容性问题。它允许将一个类或对象的接口(方法或属性)转化为另外一个接口,以满足用户需求,使类或对象之间的接口不兼容问题通过适配器得以解决。
// 定义目标接口
class TargetInterface {
request() {
throw new Error("This method should be implemented.");
}
}
// 创建被适配者类
class Adaptee {
specificRequest() {
console.log("Specific request is called.");
}
}
// 创建适配器类
class Adapter extends TargetInterface {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest();
}
}
// 创建被适配者对象
const adaptee = new Adaptee();
// 创建适配器对象
const adapter = new Adapter(adaptee);
// 调用目标接口的方法
adapter.request();
// AliPay接口
class AliPay {
pay(amount) {
console.log(`AliPay: 支付了 ${amount} 元`);
}
}
// WeChatPay接口
class WeChatPay {
transfer(amount) {
console.log(`WeChatPay: 转账了 ${amount} 元`);
}
}
// 适配器
class PaymentAdapter {
constructor(payment) {
this.payment = payment;
}
pay(amount) {
if (this.payment instanceof AliPay) {
this.payment.pay(amount);
} else if (this.payment instanceof WeChatPay) {
this.payment.transfer(amount);
} else {
throw new Error("不支持的支付平台");
}
}
}
// 使用适配器进行支付
const aliPay = new AliPay();
const weChatPay = new WeChatPay();
const aliPayAdapter = new PaymentAdapter(aliPay);
const weChatPayAdapter = new PaymentAdapter(weChatPay);
aliPayAdapter.pay(100); // 输出:AliPay: 支付了 100 元
weChatPayAdapter.pay(200); // 输出:WeChatPay: 转账了 200 元
迭代器模式(Iterator Pattern)
用于顺序地访问聚合对象内部的元素,又无需知道对象内部结构。使用了迭代器之后,使用者不需要关心对象的内部构造,就可以按序访问其中的每个元素。
迭代器模式早已融入我们的日常开发中,在使用 filter、reduce、map 等方法的时候,不要忘记这些便捷的方法就是迭代器模式的应用。当使用迭代器方法处理一个对象时,可以关注与处理的逻辑,而不必关心对象的内部结构,侧面将对象内部结构和使用者之间解耦,也使得代码中的循环结构变得紧凑而优美。
原型模式(Prototype Pattern)
无需多言,就是原型与原型链,使用Object.create(原型链)去创建对象,实例就有了原型上的属性和方法,并且还可以自定义属性和方法。
组合模式(Composite Pattern)
用于将对象组成树形结构,以表示"部分-整体"的层次结构。它允许在一个对象中嵌入其他对象,从而形成一个递归的组合结构。这使得用户可以统一地处理单个对象和组合对象,而无需关心对象的具体类型。
-
组件(Component) :表示组合中的对象,它定义了组合对象和叶子对象共同的接口。可以是一个抽象类或接口,声明了一些公共方法和属性,以及子对象的管理方法。
-
叶子节点(Leaf) :表示组合中的叶子对象,它没有子节点。它实现了组件接口,并定义了自身特有的行为。它是组合模式中的最小单位。
-
组合节点(Composite) :表示组合中的容器对象,它包含了子节点。它实现了组件接口,并定义了管理子对象的方法。组合节点可以包含其他组合节点或叶子节点,形成一个递归的组合结构。
// 定义共同接口或基类
class Component {
constructor(name) {
this.name = name;
}
// 添加子节点
add(component) {}
// 移除子节点
remove(component) {}
// 获取子节点
getChild(index) {}
// 执行操作
operation() {}
}
// 创建组合节点类
class Composite extends Component {
constructor(name) {
super(name);
this.children = [];
}
// 添加子节点
add(component) {
this.children.push(component);
}
// 移除子节点
remove(component) {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
// 获取子节点
getChild(index) {
return this.children[index];
}
// 执行操作
operation() {
console.log(`Composite ${this.name} operation.`);
for (const child of this.children) {
child.operation();
}
}
}
// 创建叶子节点类
class Leaf extends Component {
constructor(name) {
super(name);
}
// 执行操作
operation() {
console.log(`Leaf ${this.name} operation.`);
}
}
// 创建对象层次结构
const root = new Composite("root");
const branch1 = new Composite("branch1");
const branch2 = new Composite("branch2");
const leaf1 = new Leaf("leaf1");
const leaf2 = new Leaf("leaf2");
const leaf3 = new Leaf("leaf3");
root.add(branch1);
root.add(branch2);
branch1.add(leaf1);
branch2.add(leaf2);
branch2.add(leaf3);
// 使用组合模式进行操作
root.operation();
其实就是树形结构,每个叶子节点都可以被操控。
外观模式(Facade Pattern)
外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简单的接口,隐藏了一个复杂系统的内部复杂性,使得客户端可以通过该接口与系统进行交互,而无需了解系统内部的具体实现细节。
外观模式可以将一个复杂系统分解成多个子系统或模块,然后使用一个外观类作为系统的统一接口,为客户端提供所需的功能。客户端只需要与外观类进行交互,而不需要直接与子系统进行交互。
保存多个子系统,暴露统一调用子系统的方法,子系统中的逻辑无需关心。
// 子系统类
class SubSystem1 {
operation1() {
console.log("Subsystem1 operation1");
}
}
class SubSystem2 {
operation2() {
console.log("Subsystem2 operation2");
}
}
// 外观类
class Facade {
constructor(subsystem1, subsystem2) {
this.subsystem1 = subsystem1;
this.subsystem2 = subsystem2;
}
//外观类接口方法
run() {
this.subsystem1.operation1();
this.subsystem2.operation2();
}
runOne(sunSystem) {
this[sunSystem].operation1();
}
}
// 客户端代码调用
const subsystem1 = new SubSystem1();
const subsystem2 = new SubSystem2();
// 外观类
const facade = new Facade(subsystem1, subsystem2);
facade.run();
facade.runOne('subsystem2');
建造者模式
桥接模式
享元模式
模板方法模式
职责链模式
命令模式
访问者模式
中介者模式
备忘录模式
解释器模式