本文将提供一系列的设计模式,有些可能晦涩难懂。没关系,我会用日常例子类比,还精心提供了demo案例和一些实用场景,确保你能轻松驾驭。相信你读完一定醍醐灌顶,会有一种马上重构代码的冲动。快快快,跟上脚步一起来看!
设计原则
先来说说设计原则,设计原则是指导软件设计的基本准则,帮助开发者做出更好的设计决策,而设计模式是这些原则在特定情境下的具体实现方案。设计原则提供了思考问题的框架,设计模式则提供了可复用的解决方案,二者结合能够有效提升代码的质量、可维护性和可扩展性。常见的设计原则包括:
SOLID
- 单一职责原则 (SRP) :一个类应该只有一个职责,避免类的功能过于复杂。
- 开放/关闭原则 (OCP) :软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 里氏替换原则 (LSP) :子类对象应该能够替换父类对象而不影响程序的正确性。
- 接口隔离原则 (ISP) :不应该强迫一个类依赖于它不需要的接口。
- 依赖倒转原则 (DIP) :高层模块不应该依赖低层模块,二者都应该依赖于抽象。
设计模式
设计模式则是针对特定软件设计问题的成熟解决方案,它们为我们提供了可重用的结构和方法。通过使用设计模式,我们能够以更清晰的方式组织代码,提高可读性和可维护性。这使得我们在应对复杂的开发挑战时,能够更高效地找到合适的解决方案。
观察者模式 (Observer)
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,使得当一个对象的状态发生变化时,所有依赖于它的对象都能自动收到通知并更新。这就像是一个新闻发布者和订阅者的关系:当新闻发布者发布新消息时,所有订阅者都会第一时间收到更新。
- 低耦合性:观察者和主题之间是松耦合的,观察者无需知道主题的具体实现。
- 动态性:可以在运行时添加或移除观察者,灵活性高。
- 广播通信:主题可以同时通知多个观察者,适合事件驱动的系统。
使用场景:
- 当一个对象的状态变化需要影响其他多个对象时。
- 当一个对象(主题)需要通知多个对象(观察者),而这些观察者又可能会在运行时动态添加或删除。
小试牛刀
// 主题类
class Subject {
constructor() {
this.observers = []; // 存储观察者
}
// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}
// 移除观察者
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
// 通知所有观察者
notifyObservers(data) {
this.observers.forEach(observer => observer.update(data));
}
}
// 观察者类
class Observer {
constructor(name) {
this.name = name;
}
// 更新方法,接收主题通知
update(data) {
console.log(`${this.name} 收到更新: ${data}`);
}
}
// 使用示例
const subject = new Subject();
const observer1 = new Observer('观察者 1');
const observer2 = new Observer('观察者 2');
// 注册观察者
subject.addObserver(observer1);
subject.addObserver(observer2);
// 主题状态变化,通知观察者
subject.notifyObservers('新数据可用!');
// 移除一个观察者
subject.removeObserver(observer1);
// 再次通知观察者
subject.notifyObservers('又一次更新!');
上个栗子
- React:在组件之间传递状态时,使用了类似观察者模式的概念,尤其是在使用状态管理库(如 Redux)时。
- Vue.js:Vue 的响应式系统也是基于观察者模式,当数据变化时,所有依赖于该数据的组件都会自动更新。
发布订阅模式 (Publish-Subscribe)
发布订阅模式是一种消息传递模式,它允许一个或多个发布者(Publisher)向一个或多个订阅者(Subscriber)发送消息,而不需要知道彼此的具体实现。这种模式通过一个中介(通常称为事件总线或消息中心)来解耦发布者和订阅者,使得它们之间的沟通更加灵活和高效。
- 解耦:发布者和订阅者之间没有直接联系,降低了系统的耦合度。
- 灵活性:可以动态添加或移除订阅者,适应性强。
- 广播能力:支持一对多的消息传递,适合事件驱动的架构。
使用场景:
- 当系统中存在多个组件需要对同一事件做出反应时。
- 当需要将事件的处理逻辑与事件的产生逻辑分离时。
小试牛刀
// 发布订阅中心
class EventBus {
constructor() {
this.listeners = {}; // 存储事件及其对应的监听器
}
// 注册事件监听器
on(event, listener) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(listener);
}
// 移除事件监听器
off(event, listener) {
if (!this.listeners[event]) return;
this.listeners[event] = this.listeners[event].filter(l => l !== listener);
}
// 发布事件
emit(event, data) {
if (!this.listeners[event]) return;
this.listeners[event].forEach(listener => listener(data));
}
}
// 使用示例
const eventBus = new EventBus();
const listener1 = (data) => {
console.log(`监听者 1 收到消息: ${data}`);
};
const listener2 = (data) => {
console.log(`监听者 2 收到消息: ${data}`);
};
// 注册监听器
eventBus.on('消息事件', listener1);
eventBus.on('消息事件', listener2);
// 发布事件
eventBus.emit('消息事件', '新消息到达!');
// 移除监听器
eventBus.off('消息事件', listener1);
// 再次发布事件
eventBus.emit('消息事件', '又一条消息!');
上个栗子
- Node.js:在 Node.js 中,事件模块(EventEmitter)就是使用发布订阅模式实现的,允许开发者轻松地创建和管理事件。
- Vue.js:Vue 的事件系统也基于发布订阅模式,组件之间通过事件进行通信,增强了组件的复用性和灵活性。
工厂模式 (Factory)
工厂模式是一种创建对象的设计模式,它提供了一种创建对象的接口,而不需要指定具体的类。通过工厂模式,客户端可以通过工厂类来获取对象,而不需要知道对象的具体实现细节。这种模式通常用于需要创建大量相似对象的场景,能够有效地管理对象的创建过程。
就像是一个披萨店。在披萨店里,你只需要告诉服务员你想要什么类型的披萨,比如意大利风味、夏威夷风味或是素食披萨,而不需要关心披萨的具体制作过程。服务员(工厂)根据你的需求(输入)来准备相应的披萨(输出),而你只需享受美味的披萨就好。
- 封装对象创建:将对象的创建逻辑封装在工厂类中,简化了客户端的代码。
- 提高扩展性:可以通过增加新的工厂类来支持新类型的对象,符合开闭原则。
- 降低耦合度:客户端与对象的具体实现解耦,便于维护和修改。
使用场景:
- 当系统中有多个相似的对象需要创建时。
- 当创建对象的逻辑复杂,或者需要根据不同条件创建不同类型的对象时。
小试牛刀
// 产品类
class Car {
constructor(model) {
this.model = model;
}
drive() {
console.log(`正在驾驶 ${this.model}!`);
}
}
// 工厂类
class CarFactory {
static createCar(model) {
return new Car(model);
}
}
// 使用示例
const myCar = CarFactory.createCar('特斯拉 Model 3');
myCar.drive(); // 输出: 正在驾驶 特斯拉 Model 3!
上个栗子
- JavaScript 的构造函数:在 JavaScript 中,构造函数本身就可以视为一种简单的工厂模式,使用
new
关键字创建对象。 - React 的组件:在 React 中,组件的创建可以通过工厂函数来实现,允许根据不同的条件返回不同类型的组件,提升了灵活性。
- jQuery 的元素创建:在 jQuery 中,你可以使用
$(selector)
来创建和选择 DOM 元素。
单例模式 (Singleton)
单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。这个模式常用于需要全局共享资源的场景,比如配置管理、日志记录等。
就像是一个公司只有一个总经理。虽然公司可能有很多部门和员工,但总经理只有一个,所有人都需要通过他来获取决策和指示。这个总经理就像单例模式中的唯一实例,确保了资源的统一管理。
- 全局访问:单例模式提供一个全局访问点,方便在整个应用程序中使用。
- 节省内存:由于只创建一个实例,减少了内存的使用。
- 控制实例数量:确保类只有一个实例,避免了不必要的资源浪费。
使用场景:
- 当需要一个全局共享的资源时,例如配置管理。
- 当需要控制对某个特定资源的访问时,例如日志记录或数据库连接。
小试牛刀
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
this.timestamp = new Date();
Singleton.instance = this;
}
getTimestamp() {
return this.timestamp;
}
}
// 使用示例
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1.getTimestamp()); // 输出当前时间
console.log(instance1 === instance2); // 输出 true,证明只有一个实例
上个栗子
- 配置管理:在一个应用中,配置文件通常只需要加载一次,单例模式可以确保配置类只有一个实例。
- 日志记录:日志记录器通常需要在整个应用中共享,单例模式可以确保所有日志都通过同一个实例进行处理。
- 数据库连接:在需要与数据库进行交互时,单例模式可以确保只有一个数据库连接实例,避免了多次连接造成的资源浪费。
装饰器模式 (Decorator)
装饰器模式是一种结构型设计模式,允许在不改变对象自身的情况下,动态地给对象添加额外的功能。通过使用装饰器,可以在运行时对对象进行增强,提供更灵活的功能扩展。
就像是在咖啡店点咖啡时,你可以选择基础的黑咖啡,然后再根据自己的口味添加牛奶、糖或香料。每添加一种配料,咖啡的风味就会有所不同,但基础的黑咖啡并没有改变。装饰器模式就是通过这种方式,为对象添加新的行为或状态。
- 灵活性:可以在运行时动态添加或删除装饰,提供更高的灵活性。
- 单一职责:每个装饰器都只负责特定的功能,符合单一职责原则。
- 可扩展性:可以通过组合多个装饰器来扩展对象的功能,增强了系统的可扩展性。
使用场景:
- 当需要在不修改对象代码的情况下,给对象添加额外的功能时。
- 当需要动态地给对象添加或删除功能时。
小试牛刀
class Coffee {
cost() {
return 5; // 基础黑咖啡的价格
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 2; // 添加牛奶的价格
}
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1; // 添加糖的价格
}
}
// 使用示例
let myCoffee = new Coffee();
myCoffee = new MilkDecorator(myCoffee); // 添加牛奶
myCoffee = new SugarDecorator(myCoffee); // 添加糖
console.log(myCoffee.cost()); // 输出 8,基础价格5 + 牛奶2 + 糖1
上个栗子
- UI 组件:在前端开发中,可以使用装饰器模式为组件添加额外的功能,比如添加样式、事件处理等,而无需修改原组件的代码。
- 日志记录:可以通过装饰器模式为函数添加日志功能,记录函数的调用情况,而不影响函数的核心逻辑。
- 权限控制:在用户权限管理中,可以使用装饰器模式动态地为用户对象添加不同的权限,而不需要创建多个用户类。
策略模式 (Strategy)
策略模式是一种行为型设计模式,定义了一系列算法,将每个算法封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户,从而提高了系统的灵活性和可扩展性。
想象一下,你在选择出行方式时,可以选择开车、骑自行车或乘坐公共交通。每种出行方式都有不同的优缺点,具体选择哪种方式取决于你的需求和场景。策略模式就像是为你提供了不同的出行策略,你可以根据需要灵活切换。
- 灵活性:可以在运行时动态选择算法,增强了系统的灵活性。
- 避免条件语句:通过将算法封装在不同的策略类中,避免了在客户端代码中出现大量的条件语句。
- 扩展性:可以很容易地添加新的策略,而不需要修改现有的代码。
使用场景:
- 当需要在多个算法之间进行选择时。
- 当需要避免使用大量的条件语句来选择算法时。
小试牛刀
class Strategy {
execute(a, b) {
throw new Error("此方法会被重载!");
}
}
class AddStrategy extends Strategy {
execute(a, b) {
return a + b;
}
}
class SubtractStrategy extends Strategy {
execute(a, b) {
return a - b;
}
}
class MultiplyStrategy extends Strategy {
execute(a, b) {
return a * b;
}
}
class Context {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executeStrategy(a, b) {
return this.strategy.execute(a, b);
}
}
// 使用示例
const context = new Context(new AddStrategy());
console.log(context.executeStrategy(5, 3)); // 输出 8
context.setStrategy(new SubtractStrategy());
console.log(context.executeStrategy(5, 3)); // 输出 2
context.setStrategy(new MultiplyStrategy());
console.log(context.executeStrategy(5, 3)); // 输出 15
上个栗子
- 支付方式:在电商应用中,可以使用策略模式来处理不同的支付方式,如信用卡、支付宝、微信支付等。用户可以根据自己的需求选择合适的支付策略。
- 排序算法:在数据处理时,可以使用策略模式来选择不同的排序算法(如快速排序、归并排序等),根据数据的特点灵活切换。
- 游戏角色行为:在游戏开发中,不同角色可以有不同的攻击策略,使用策略模式可以让角色根据情况选择不同的攻击方式,而不需要修改角色的核心代码。
可插拔模式 (Plug-in Pattern)
可插拔模式是一种结构型设计模式,它允许在运行时向系统中动态添加新功能。通过使用可插拔模式,开发者可以在不修改原有代码的情况下,灵活地扩展系统的功能。这种模式通常在插件架构中使用,允许用户根据需要选择和加载不同的插件。
想象一下,你在使用一个文本编辑器,它支持通过插件来扩展功能。你可以安装拼写检查插件、代码高亮插件等,而不需要改动编辑器的核心代码。可插拔模式就像这个插件系统,允许用户根据需求自由选择和添加功能。
- 灵活性:可插拔模式提供了灵活的扩展方式,用户可以根据需求选择不同的插件。
- 解耦:插件与主程序之间是解耦的,插件可以独立开发和维护。
- 动态加载:可以在运行时动态加载和卸载插件,提高了系统的可维护性。
使用场景:
- 当需要为应用程序提供可扩展的功能时。
- 当希望支持第三方开发者为系统开发插件时。
小试牛刀
class Plugin {
constructor(name) {
this.name = name;
}
execute() {
console.log(`${this.name} 插件执行`);
}
}
class PluginManager {
constructor() {
this.plugins = [];
}
addPlugin(plugin) {
this.plugins.push(plugin);
}
runPlugins() {
this.plugins.forEach(plugin => plugin.execute());
}
}
// 使用示例
const pluginManager = new PluginManager();
const spellCheckPlugin = new Plugin("拼写检查");
const codeHighlightPlugin = new Plugin("代码高亮");
pluginManager.addPlugin(spellCheckPlugin);
pluginManager.addPlugin(codeHighlightPlugin);
pluginManager.runPlugins();
// 输出:
// 拼写检查 插件执行
// 代码高亮 插件执行
上个栗子
- 文本编辑器插件:在文本编辑器中,用户可以根据需求安装不同的插件,如拼写检查、语法高亮等,灵活地扩展编辑器的功能。
- 图像处理软件:在图像处理软件中,用户可以添加滤镜插件、特效插件等,以增强图像处理的能力。
- Web 应用程序:在Web应用中,可以使用可插拔模式来动态加载不同的功能模块,如数据可视化模块、用户认证模块等,提升应用的灵活性和可扩展性。
代理模式 (Proxy)
代理模式是一种结构型设计模式,为其他对象提供一种代理以控制对这个对象的访问。代理对象通常会在客户端和真实对象之间起到中介的作用,可以在不改变真实对象的情况下,添加额外的功能。
想象一下,你在一个大型公司工作,公司的门口有一个前台接待员。你不能直接进入办公室,而是需要通过前台接待员来联系办公室。接待员就是一个代理,他会根据你的请求来决定是否允许你进入办公室。代理模式就像这个接待员,可以控制对真实对象的访问。
- 控制访问:可以在代理中添加访问控制逻辑,确保只有符合条件的请求才能访问真实对象。
- 延迟加载:可以在代理中实现延迟加载,只有在需要时才创建真实对象,节省资源。
- 功能增强:可以在代理中添加额外的功能,如日志记录、安全检查等。
使用场景:
- 当需要控制对对象的访问时。
- 当需要实现延迟加载或资源管理时。
小试牛刀
class RealSubject {
request() {
console.log("真实对象的请求");
}
}
class Proxy {
constructor(realSubject) {
this.realSubject = realSubject;
}
request() {
this.preRequest(); // 代理前的操作
this.realSubject.request(); // 调用真实对象的方法
this.postRequest(); // 代理后的操作
}
preRequest() {
console.log("代理前的操作");
}
postRequest() {
console.log("代理后的操作");
}
}
// 使用示例
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);
proxy.request();
// 输出:
// 代理前的操作
// 真实对象的请求
// 代理后的操作
上个栗子
- 图片加载代理:在图像处理应用中,可以使用代理模式来实现图像的懒加载。当用户请求显示一张图像时,代理先检查是否已经加载了真实图像。如果没有,代理就会去加载真实图像并缓存它,以便下次快速访问。这种方式可以提高应用的性能,减少不必要的资源消耗。
- 权限控制:在某些系统中,可能需要对特定操作进行权限控制。可以使用代理模式创建一个代理对象,在调用真实对象的方法之前,先检查用户是否具有执行该操作的权限。如果没有权限,代理可以拒绝请求或抛出异常。
适配器模式 (Adapter)
适配器模式是一种结构型设计模式,允许将一个类的接口转换成客户端所期望的另一种接口。适配器模式使得原本由于接口不兼容而无法一起工作的类可以一起工作。
想象一下,你有一个充电器可以为手机充电,但如果你的手机使用的是不同的接口,你就需要一个适配器来连接充电器和手机。适配器模式就像这个充电器的适配器,帮助不同接口的设备能够互相连接。
- 接口兼容性:可以让不兼容的接口之间进行协作,增强了系统的灵活性。
- 复用现有代码:可以通过适配器重用现有的类,而不需要修改它们的代码。
- 解耦:适配器将客户端与被适配的类解耦,使得客户端不需要了解被适配类的具体实现。
使用场景:
- 当需要使用一些现有的类,但它们的接口不符合你的需求时。
- 当希望通过接口来解耦系统中的不同部分时。
小试牛刀
class Target {
request() {
console.log("目标接口的请求");
}
}
class Adaptee {
specificRequest() {
console.log("被适配者的请求");
}
}
class Adapter extends Target {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}
request() {
this.adaptee.specificRequest(); // 调用被适配者的方法
}
}
// 使用示例
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
adapter.request(); // 输出:被适配者的请求
上个栗子
- 数据格式转换:在前端开发中,API返回的数据格式可能与应用需求不符,适配器模式可以用来转换数据格式,使其符合预期。
- 第三方库整合:在使用第三方库时,可能会遇到接口不一致的情况,适配器模式可以帮助将第三方库的接口适配到项目中。
- UI 组件适配:在构建组件库时,可能需要将不同的UI框架(如React、Vue等)中的组件进行适配,适配器模式可以帮助实现这种兼容性。
迭代器模式 (Iterator)
迭代器模式是一种行为型设计模式,它提供了一种方法来顺序访问一个集合对象中的元素,而无需暴露该对象的内部表示。迭代器模式的核心在于将集合的遍历逻辑与集合的具体实现分离,使得不同的集合可以使用相同的迭代器。
想象一下,你在图书馆里查找书籍。你可以通过图书馆的目录来找到一本书,而不需要了解书架的具体布局。迭代器就像这个目录,它提供了一种简单的方法来访问书籍,而不需要关心书籍存放的具体位置。
- 解耦:迭代器模式将集合的遍历逻辑与集合的实现分离,减少了代码的耦合性。
- 多样性:可以为不同的集合实现不同的迭代器,提供多样的遍历方式。
- 简化代码:使用迭代器可以简化对集合的遍历逻辑,提供统一的接口。
使用场景:
- 当需要访问一个集合对象中的元素,而不想暴露集合的内部结构时。
- 当需要支持多种遍历方式时。
小试牛刀
class Iterator {
constructor(collection) {
this.collection = collection;
this.index = 0;
}
hasNext() {
return this.index < this.collection.length;
}
next() {
return this.collection[this.index++];
}
}
class Collection {
constructor() {
this.items = [];
}
add(item) {
this.items.push(item);
}
getIterator() {
return new Iterator(this.items);
}
}
// 使用示例
const collection = new Collection();
collection.add("书籍1");
collection.add("书籍2");
collection.add("书籍3");
const iterator = collection.getIterator();
while (iterator.hasNext()) {
console.log(iterator.next()); // 输出:书籍1, 书籍2, 书籍3
}
上个栗子
- 购物车迭代器:想象一下一个电商网站的购物车功能。用户可以将商品添加到购物车中并查看。我们可以使用迭代器模式来遍历购物车中的商品,而不需要直接访问购物车的内部结构。这样,购物车的实现细节对用户是透明的。
- 音乐播放列表:在音乐播放器中,可以使用迭代器模式来实现播放列表的遍历。用户可以通过迭代器逐首播放歌曲,而不需要关心歌曲存储的具体方式(如数组、链表等)。
最后
掌握了这些设计原则和设计模式后,我们在日常编码中,尤其是在封装和组织逻辑时,可以将它们作为强有力的过滤器,把我们实现思路先洗一遍。想象一下,经过这样的“洗礼”,代码的质量将会得到质的飞跃,变得更加清晰、易于维护和扩展。什么,不想听我说废话了?那还等什么,改代码去呀!