引言
在软件开发领域,设计模式是一种被广泛接受且经过验证的最佳实践方法。设计模式提供了解决常见问题的可复用方案,帮助程序员更好地组织和设计代码。本文旨在介绍设计模式的基本概念、作用以及常见的设计模式,并探讨它们在提高代码质量和开发效率方面的优势。
一、软件开发常见设计模式
- Singleton模式(单例模式):确保一个类只有一个实例,并提供一个全局访问点。
- Builder模式(建造者模式):用于创建复杂对象,通过一步一步的构建,将创建逻辑与表示分离。
- Factory模式(工厂模式):通过工厂类来创建对象,隐藏了对象的具体创建过程。
- Observer模式(观察者模式):定义了对象之间一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。
- Strategy模式(策略模式):定义了一簇算法,并使其相互之间可以互换,使算法的变化独立于使用算法的客户端。
- Decorator模式(装饰者模式):动态地给对象添加额外的责任,可以在不改变其结构的情况下,增强对象的功能。
二、设计模式详细解析
Singleton模式(单例模式)
定义:单例模式是一种创建型设计模式,旨在确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
理解: 皇帝制度,只有一个皇帝执掌全局,一声令下所有相关地方都要变化
图解:
优点:全局只提供一个实例,便于管理,且避免重复创建相同对象的开销
缺点:难扩展,难以进行单元测试,违反单一职责原则,高度耦合
使用场景:需要做全局统一控制的场景
使用举例(前端方向):
- 状态管理器 (Vuex,Redux)
它将应用的状态存储在一个全局的 Store 对象中,并通过提供全局访问点的方式,使所有组件都能够访问和更新这个状态。 - 全局事件总线 (bus)
通过向全局创建一个事件总线对象$bus,其他组件可以通过订阅和发布事件来进行通信。 - 配置管理器
保证系统中只有一个配置文件对象,方便访问和管理配置信息。
Builder模式(建造者模式)
定义:建造者模式是一种创建型设计模式,它将对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
理解:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
图解:
优点:封装性较强,可扩展性强,灵活构建和调用
缺点:增加了系统的复杂度,构建的对象要遵循一些步骤
使用场景:需要生成的对象有复杂的内部结构,部分属性相互依赖
代码举例(前端方向TS):
kotlin
class ProductPageBuilder {
constructor() {
this.productImage = null;
this.productName = "";
}
setProductImage(imageUrl) {
this.productImage = imageUrl;
return this;
}
setProductName(name) {
this.productName = name;
return this;
}
build() {
const productPage = new ProductPage();
productPage.setProductImage(this.productImage);
productPage.setProductName(this.productName);
return productPage;
}
}
Factory模式(工厂模式)
定义:工厂模式是一种创建型设计模式,它提供了一种创建对象的接口,但具体实现由子类决定。工厂模式将对象的实例化过程推迟到子类中进行,从而避免了在客户端代码中直接使用具体类进行对象的创建。
分类:
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
理解:批量的产出一些对象,不去关心具体的实现细节,就是根据模具(接口)生成即可,工厂类包含一个用于创建对象的方法,该方法会根据传入的参数或条件返回相应的具体对象实例。
图解:
优点:封装了对象的创建过程,可进行批量的创建,提供了可扩展性,降低了代码的耦合度
缺点:可能会增加系统的复杂度,增加内存的占用,在项目初期不太建议使用该模式
使用场景:
- 客户端无需关心对象的实例化过程,只需要通过一个公共接口获取所需对象
- 当需要通过不同的方式创建、初始化或配置对象时,可以使用工厂模式来封装这些复杂的创建过程。
- 当系统需要扩展新的产品时,可以通过添加新的具体工厂类来实现,而无需修改已有代码。
使用举例(前端方向):
typescript
// 定义一个组件工厂类
class ComponentFactory {
// 创建组件的方法
createComponent(type, props) {
switch (type) {
case 'button':
return new ButtonComponent(props);
case 'input':
return new InputComponent(props);
default:
throw new Error('Invalid component type.');
}
}
}
// 定义一个按钮组件类
class ButtonComponent {
constructor(props) {
// 按钮组件的初始化逻辑
this.text = props.text;
this.onClick = props.onClick;
}
render() {
// 按钮组件的渲染逻辑
const button = document.createElement('button');
button.innerText = this.text;
button.addEventListener('click', this.onClick);
return button;
}
}
// 定义一个输入框组件类
class InputComponent {
constructor(props) {
// 输入框组件的初始化逻辑
this.placeholder = props.placeholder;
this.onInput = props.onInput;
}
render() {
// 输入框组件的渲染逻辑
const input = document.createElement('input');
input.setAttribute('placeholder', this.placeholder);
input.addEventListener('input', this.onInput);
return input;
}
}
Observer模式(观察者模式)
定义:观察者模式是一种行为设计模式,它定义了对象之间的一对多依赖关系,使得当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。
角色:观察者 与 被观察者
理解:观察者通过观察,来确定被观察者有哪些变化,前端写样式的时候,就是一种观察者模式的体现
图解:
优点:
- 松耦合:观察者模式将目标和观察者分离,使它们之间的依赖关系变得松散,目标对象不需要知道观察者的具体细节。
- 可扩展性:可以方便地增加新的观察者,因为目标对象只需要维护一个观察者列表,并通知所有观察者即可。
- 易于维护:观察者模式将逻辑分散到多个观察者中,每个观察者只负责处理自己感兴趣的事件,使代码更加清晰、易于维护。
缺点:
- 过多的通知:如果观察者较多或者观察者的处理逻辑较复杂,目标对象每次状态变化时需要通知所有观察者,可能会导致性能问题。
- 循环依赖:在设计观察者模式时,需要注意避免观察者和目标对象之间的循环依赖,否则可能导致系统崩溃或循环调用。
使用场景:
- 当一个对象的改变需要同时改变其他对象,且它不知道具体有多少个对象有待改变时
- 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面时
使用举例(前端方向):
- 消息订阅与发布
- 通过代码看页面视图的更新
策略模式(Strategy Pattern)
定义:策略模式是一种行为设计模式,它定义了一系列的算法,并将它们封装起来,使它们可以互相替换。策略模式可以让算法的变化独立于使用算法的客户端。
理解:定义不同类型的方法,来实现不同场景下的同一个需求
图解:
角色:
- Context(上下文):它是策略模式的核心,负责维护一个对策略对象的引用,并将具体的请求委托给策略对象来处理。
- Strategy(策略):定义了一个公共接口,所有具体策略都必须实现这个接口。
- Concrete Strategies(具体策略):实现了策略接口,提供了不同的算法实现。 优点:
- 算法的独立性:策略模式将算法封装成独立的策略类,使得它们可以独立地变化和复用,易于维护和扩展。
- 可替换性:由于策略模式将算法与使用者解耦,因此可以动态地切换不同的策略,满足不同的需求,而无需修改客户端代码。
- 简化条件语句:策略模式避免了使用大量的条件语句来选择不同的算法,使代码更加清晰、易于理解。
缺点:
- 增加类的数量:引入策略模式会增加类的数量,每个具体策略都需要一个对应的策略类。
- 客户端必须了解不同的策略:客户端在使用策略模式时,需要了解不同的策略,并选择合适的策略,增加了一定的使用复杂性。
使用场景:
- 当一个系统中有多个类只有一个行为不同的情况下,可以考虑使用策略模式,将这些行为封装到不同的策略类中。
- 当需要在运行时动态地选择算法时,可以使用策略模式。
使用举例(前端方向):普通登陆与第三方登录
Decorator模式(装饰者模式)
定义:装饰者模式是一种结构设计模式,它允许在不改变已有对象的情况下,动态地将新功能添加到对象上。
理解:给某个物品添加包装,物品本身不变,只是添加了一些修饰,让其更加美观
图解:
优点:
- 灵活性:装饰者模式允许在运行时动态地添加或移除功能,可以灵活地组合不同的装饰类,实现各种组合效果。
- 单一职责原则:装饰者模式将功能划分到不同的装饰类中,每个装饰类只关注自己特定的功能,符合单一职责原则,使代码更加清晰。
- 可扩展性:由于装饰者模式不依赖于继承关系,而是通过对象的包装来扩展功能,因此更易于扩展和维护。
缺点:
- 多层嵌套:使用过多的装饰者可能会导致类的数量增多,增加代码的复杂性。
- 顺序依赖:装饰者模式中装饰者的调用顺序是固定的,需要注意装饰者之间的相互影响和顺序依赖。
使用场景:
- 当需要动态地为对象添加或移除功能时
- 当不希望通过继承来扩展功能或生成子类时
使用举例:动态的给某个按钮添加样式
三、设计模式六大原则
- 单一职责原则(Single Responsibility Principle)
解释
: 每个类应该只负责一个单一的功能或职责
- 开闭原则(Open Closed Principle)
解释
: 软件实体(类、模块、函数等)应该对扩展是开放的,但对修改是封闭的
- 里氏替换原则(Liskov Substitution Principle)
解释
: 子类应该能够替换掉父类并且不产生任何错误或异常,即任何可以使用父类对象的地方都可以使用子类对象
- 最少知道原则(Law of Demeter)
解释
: 一个对象应该与其他对象保持最少的了解,即一个对象应该尽量减少与其他对象之间的交互,只与直接的朋友发生通信
- 接口隔离原则(Interface Segregation Principle)
解释
: 客户端不应该依赖于它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上,尽量精简接口,避免臃肿的接口
- 依赖倒置原则(Dependence Inversion Principle)
解释
: 高层模块不应该依赖于低层模块,它们都应该依赖于抽象接口。
抽象不应该依赖于具体实现细节,具体实现应该依赖于抽象