什么是设计模式?
💡 软件开发中被广泛使用的最佳实践和经验总结。
曾经有一位小编程序员,他正在为一个电商网站开发一个购物车模块。开始时,他的代码看起来非常简单和直观,但随着项目的不断发展,代码变得越来越复杂、难以维护,并且出现了很多重复的代码。
于是,这位小编开始学习 JavaScript 设计模式,并尝试将其应用于购物车模块中。他使用了工厂模式来创建不同类型的商品对象,使用单例模式确保购物车只被实例化一次,使用观察者模式来监听各个组件之间的变化并更新购物车列表等。
通过设计模式,这位小编成功地优化了购物车模块的代码,并使其更加易于维护和扩展。他的代码不再出现大量的重复代码和难以理解的逻辑,同时还变得更加灵活和易于修改。最终,这个购物车模块成功地被集成到电商网站中,让用户可以更加愉快地购物。
所以说,JavaScript 设计模式是一种可重用的解决方案,可以帮助开发者提高代码的可维护性、可读性和可扩展性,减少重复代码的出现,提高代码的复用率。它们是在开发中被广泛使用的最佳实践和经验总结。
那么现在我们知道什么是模式了,那么模式还有一个更重要的概念,也就是 模式的适用性。
模式的适用性
去看篮球比赛的话,我们知道最重要的目的就是进球。所有可以帮助我们进球的方案,都是好的方案。但是如果我们把一些 正确的战术用在了错误的场景下,比如教练可以对奥尼尔使用犯规战术 , 但是如果对方是罚球很准的后卫 , 那么这种战术就是白送二分给对手。
💡 任何设计模式都不是万能的,每个模式都有其独特的优缺点和适用范围。注意选择适合当前场景的模式,而不是简单地套用某个模式。
基础知识
学习设计模式前先了解一下基础知识
- js的面向对象编程,如原型、继承、构造函数、this 等
- js的函数式编程,如高阶函数、闭包、作用域等
💡 这里涵盖的知识面太多 , 我准备单独开文章讲解 ,这篇文章还是主要讲解设计模式
设计模式
设计模式有几十种, 而且并不是完全适用于前端的,所以我先讲解以下 3
种常用设计模式:
- 单例模式
- 代理模式 ( vue3 )
- 观察者模式 ( 如useEffect ,vue2 )
单例模式
是一种保证一个类只有一个实例的方法,通常用于创建全局对象或管理共享资源。例如:
// 创建一个单例对象
var Singleton = (function () {
// 保存唯一的实例
var instance;
// 创建一个新的实例
function createInstance() {
var object = new Object("I am the instance");
return object;
}
// 返回一个函数,用于获取或创建实例
return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
// 测试单例模式
var instance1 = Singleton.getInstance();
var instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
代理模式
是一种在访问一个对象之前,通过另一个对象(代理)来控制或修改这个访问的方法。我们可以用es6的Proxy
类来实现代理模式,例如:
// 创建一个目标对象
var target = {
name: "Alice",
age: 25
};
// 创建一个代理对象,用于拦截目标对象的访问
var proxy = new Proxy(target, {
// 拦截获取属性的操作
get: function (obj, prop) {
// 如果属性是name,就返回大写形式
if (prop === "name") {
return obj[prop].toUpperCase();
}
// 否则,就返回原始值
return obj[prop];
},
// 拦截设置属性的操作
set: function (obj, prop, value) {
// 如果属性是age,就检查是否是合法的数字
if (prop === "age") {
if (typeof value !== "number" || value < 0 || value > 150) {
throw new TypeError("Invalid age");
}
}
// 否则,就设置属性值
obj[prop] = value;
}
});
// 测试代理模式
console.log(proxy.name); // ALICE
console.log(proxy.age); // 25
proxy.age = 30; // ok
proxy.age = "hello"; // error: Invalid age
观察者模式
用于在对象间定义一种一对多的依赖关系,当一个对象状态发生改变时,它的所有依赖者都会收到通知并自动更新。
// 定义一个主题对象
const subject = {
observers: [],// 存放所有观察者
subscribe: function(observer) {
this.observers.push(observer);// 添加一个新的观察者
},
unsubscribe: function(observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove;// 删除指定的观察者
})
},
notify: function() {
this.observers.forEach(observer => {
observer();// 调用每个观察者的函数
})
}
}
// 使用示例const observer1 = function() {
console.log('观察者 1 收到了更新通知。');
}
const observer2 = function() {
console.log('观察者 2 收到了更新通知。');
}
const observer3 = function() {
console.log('观察者 3 收到了更新通知。');
}
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.subscribe(observer3);
subject.notify(); // 123 收到通知
subject.unsubscribe(observer2);
subject.notify(); // 13 收到通知
在这个示例中,我们定义了一个 subject
主题对象,它具有添加、删除和通知观察者的方法。我们使用三个函数作为观察者,当调用 notify
方法时,这些观察者函数将被调用以接收更新通知。在使用示例中,我们订阅了三个观察者函数,并通知了所有观察者。然后我们取消了一个观察者的订阅,并再次通知剩余的观察者。
设计原则
💡 所有的设计模式都是从设计原则中衍生出来的。
设计模式数量众多,一篇文章难以涵盖。最初并不存在设计模式。但是在写了很多代码之后,设计师们发现了一些重复出现的场景,将它们抽象成了设计模式。因此,有许多不同的设计模式,而且它们都必须遵守某些规则,这些规则被称为设计原则。
模式只有在具体的环境下,才有意义,请不要滥用它并且过度设计!
其中对我们现在有意义的,主要是 3个
:
- 单一职责原则
- 最少知识原则
- 开放封闭原则
单一职责原则
指的是一个类(组件,函数等)只应该负责单一的功能或职责。
const calculatePrice = (quantity, itemPrice) => {
const basePrice = quantity * itemPrice;
return applyDiscount(basePrice);
};
const calculateTax = (quantity, itemPrice, shipping) => {
const basePrice = quantity * itemPrice;
const taxableAmount = basePrice + shipping;
return applyDiscount(taxableAmount);
};
const applyDiscount = (basePrice) => {
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
};
我们将每个函数的功能拆分成更小的函数,实现单一职责原则。
最少知识原则
一个对象应当尽可能少地与其他对象发生相互作用。在面向对象的设计语言中,存在高内聚、低耦合的概念,这个概念指的其实就是最少知识原则。最少知识原则可以很好地通过中介者模式呈现出来。
以下是一个简单的 JavaScript 函数示例,它演示了如何使用最少知识原则思想的中介者模式:
// 中介者对象
const mediator = {
// 计算两个数字之和并返回结果
calculateSum: function (num1, num2) {
return num1 + num2;
}
};
// 使用中介者对象来计算两个数字之和
function sumCalculator(num1, num2, mediator) {
return mediator.calculateSum(num1, num2);
}
// 示例用法
const result = sumCalculator(5, 10, mediator);
console.log(result); // 输出:15
在这个例子中,sumCalculator
函数使用了最少知识原则,因为它仅仅与它需要知道的中介者对象进行交互。函数本身并不需要知道关于数字的任何信息,比如数字如何被加起来,或者数字的来源等等。
开放封闭原则
开放封闭原则: (类、组件、函数)等应该对扩展开放,对修改封闭。
这个描述其实是挺好理解的,因为原有的代码一旦发生修改,那么就必然代表着风险,所以我们尽量不要修改原有代码。这意味着我们应该在设计软件时,预留好扩展点,让软件可以在不修改原有代码的情况下,通过添加新的代码来实现新的功能。这样可以提高软件的可维护性、可复用性和可测试性 , 也就是所谓的 ” 打补丁 ”
下面是一个示例代码:
function calculateArea(shape) {
switch (shape.type) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
default:
throw new Error('Unsupported shape type');
}
}
const circle = { type: 'circle', radius: 5 };
const rectangle = { type: 'rectangle', width: 10, height: 20 };
console.log(calculateArea(circle)); // Output: 78.53981633974483
console.log(calculateArea(rectangle)); // Output: 20
上述代码中的 calculateArea
函数并没有直接针对某一种特定形状进行计算,而是通过参数的形式传入不同的形状对象。这样,当需要增加一种新的形状时,只需要新增一个对应的形状对象,而不需要修改原有的 calculateArea
函数。这就体现了开放封闭原则。
这样当需要新增行为时,就不再需要修改原有代码,也就是更加符合 开放封闭原则。
如何写出适合重构的代码 (也叫重构原则)
重构原则是指在不改变代码功能的前提下,对代码进行优化和改进,以提高代码的可读性、可维护性和可扩展性。
- 提炼函数:将一个大型函数拆分成若干个小函数,使得函数实现更加简单,同时也更加具有可读性和可维护性。
- 合并重复的条件片段:消除重复代码,提高代码的复用性。
- 把条件分支语句提炼成函数:将复杂的条件语句提取出来,封装成函数,使得代码更加简洁易懂。
- 合理使用循环:在循环中进行操作时,尽量保证每次循环操作的内容相同,减少循环体内部的复杂度,提高代码的可读性和可维护性。
- 提前让函数退出代替嵌套条件分支:尽量避免嵌套条件分支,提高代码的可读性和可维护性。
- 传递对象参数代替过长的参数列表:将多个参数封装成对象,可以让代码更加简洁易懂,提高代码的可读性和可维护性。
- 尽量减少参数数量:过多的参数会增加代码的复杂度,降低代码的可读性和可维护性。
下面我们来看 7
个提升代码可维护性的具体场景与代码:
1.提炼函数
函数提炼可以让我们更加遵循 单一职责原则,每个函数只在做一件事情就可以。下面我们来看一个例子:
在下面的代码中,我们可以看到函数 calculatePrice
中存在大量的代码,且代码量过于庞大,如果这样的代码维护起来,就会变得非常困难:
function calculatePrice (quantity, itemPrice) {
// 价格计算逻辑
let basePrice = quantity * itemPrice
if (basePrice > 1000) {
return basePrice * 0.95
} else {
return basePrice * 0.98
}
}
那么我们可以通过函数提炼的方式来优化上面的代码:
function calculatePrice (quantity, itemPrice) {
// 价格计算逻辑
const basePrice = calculateBasePrice(quantity, itemPrice)
return applyDiscount(basePrice)
}
function calculateBasePrice (quantity, itemPrice) {
return quantity * itemPrice
}
function applyDiscount (basePrice) {
if (basePrice > 1000) {
return basePrice * 0.95
} else {
return basePrice * 0.98
}
}
通过函数提炼,我们将 calculatePrice
函数中的代码分别拆分到了 calculateBasePrice
和 applyDiscount
函数中,每个函数只做一件事情,代码更加清晰,易于维护。
2.合并重复的代码片段
如果你的代码中存在重复的代码片段,那么应该把这些重复的代码片段进行合并处理。
合并重复的代码片段可以通过将重复的代码片段抽离成一个函数,然后在需要的地方调用该函数的方式来进行优化。下面是一个 JavaScript 的例子。
合并前:
function calculatePrice(quantity, itemPrice) {
// 价格计算逻辑
let basePrice = quantity * itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
}
function calculateTax(quantity, itemPrice, shipping) {
// 税费计算逻辑
let basePrice = quantity * itemPrice;
let taxableAmount = basePrice + shipping;
if (taxableAmount > 1000) {
return taxableAmount * 0.95;
} else {
return taxableAmount * 0.98;
}
}
合并后:
function calculatePrice(quantity, itemPrice) {
return applyDiscount(quantity * itemPrice);
}
function calculateTax(quantity, itemPrice, shipping) {
return applyDiscount(quantity * itemPrice + shipping);
}
function applyDiscount(basePrice) {
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
}
在上面的例子中,我们发现 calculatePrice
和 calculateTax
中都存在类似的计算逻辑,我们可以把这部分代码抽离成一个独立的函数 applyDiscount
,然后在需要的地方进行调用。这样可以避免代码冗余,提高代码的可读性和可维护性。
3.把条件分支语句提炼成函数
如果你的一段逻辑中,包含难以理解的条件分支语句,那么可以把这个条件分支语句封装成一个单独的函数进行处理:
处理前:
function calculatePrice(quantity, itemPrice) {
let basePrice = quantity * itemPrice;
if (basePrice > 1000 && quantity > 500) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
}
处理后:
function calculatePrice(quantity, itemPrice) {
let basePrice = quantity * itemPrice;
if (isQualifiedForDiscount(basePrice, quantity)) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}
}
function isQualifiedForDiscount(basePrice, quantity) {
return basePrice > 1000 && quantity > 500;
}
4.合理使用循环
在函数体内,如果有些代码实际上负责的是一些重复性的工作,那么合理利用循环不仅可以完成同样的功能,还可以使代码量更少。
在超市购物时,我们需要计算所有商品的价格总和。如果我们不使用循环,代码可能如下所示:
let item1 = 10;
let item2 = 20;
let item3 = 30;
let total = item1 + item2 + item3;
但是,如果我们有许多商品,这将变得非常麻烦。相反,我们可以使用循环来计算总价:
let items = [10, 20, 30];
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i];
}
这个循环将遍历我们的商品数组,并将每个项目的价格添加到 total
变量中。这种方法不仅更简洁,而且可以轻松处理许多项目。
5. 提前让函数退出代替嵌套条件分支
如果一个函数中存在多个条件分支,那么可以通过尽早让函数退出来减少嵌套的条件分支。这样可以简化代码逻辑,提高代码的可读性和可维护性。
下面是一个简单的例子:
function calculatePrice(quantity, itemPrice) {
if (quantity > 100) {
return itemPrice * quantity * 0.95;
} else {
if (itemPrice > 50) {
return itemPrice * quantity * 0.9;
} else {
return itemPrice * quantity;
}
}
}
在上面的代码中,我们可以看到有两个条件分支语句嵌套在一起,这会让代码变得难以理解和维护。我们可以通过提前让函数退出来改善代码:
function calculatePrice(quantity, itemPrice) {
if (quantity > 100) {
return itemPrice * quantity * 0.95;
}
if (itemPrice > 50) {
return itemPrice * quantity * 0.9;
}
return itemPrice * quantity;
}
在上面的代码中,我们通过让函数尽早退出来简化了代码逻辑。这样代码更易于理解和维护。
6. 传递对象参数代替过长的参数列表
如果一个函数需要传递多个参数,那么我们可以考虑将这些参数封装成一个对象,然后将对象作为参数传递。这样可以减少传递参数的数量,让代码更加简洁。下面是一个简单的例子:
function printOrder(name, age, address, phone, email) {
console.log(name, age, address, phone, email);
}
printOrder('张三', 18, '北京市朝阳区', '13888888888', 'zhangsan@example.com');
在上面的代码中,我们需要传递五个参数来打印订单信息。如果我们使用对象来代替这些参数,代码会更加简洁,如下所示:
function printOrder(order) {
console.log(order.name, order.age, order.address, order.phone, order.email);
}
const order = {
name: '张三',
age: 18,
address: '北京市朝阳区',
phone: '13888888888',
email: 'zhangsan@example.com',
};
printOrder(order);
在上面的代码中,我们使用一个对象来封装订单信息,然后将对象作为参数传递给 printOrder
函数。这样可以使代码更加简洁,易于维护。
7. 尽量减少参数数量
函数参数过多会增加代码的复杂性,降低代码的可读性和可维护性。如果一个参数可以在函数内部计算得到,那么就不要传递这个参数。下面是一个简单的例子:
function calculatePrice(quantity, itemPrice, discount) {
// 价格计算逻辑
let basePrice = quantity * itemPrice
if (basePrice > 1000) {
return basePrice * 0.95 * discount
} else {
return basePrice * 0.98 * discount
}
}
在上面的代码中,我们需要传递三个参数来计算价格。如果我们可以在函数内部计算出折扣,那么我们可以将 discount
参数去掉,如下所示:
function calculatePrice(quantity, itemPrice) {
// 价格计算逻辑
let basePrice = quantity * itemPrice
let discount = 1
if (basePrice > 1000) {
discount = 0.95
} else {
discount = 0.98
}
return basePrice * discount
}
在上面的代码中,我们可以看到,我们可以在函数内部计算出折扣,从而将 discount
参数去掉。这样可以使代码更加简洁,易于维护。
周末有空的话再更其他设计模式 , 下班咯 冲鸭