在软件开发的过程中,设计模式如同一盏明灯,照亮了解决复杂问题的道路。设计模式不仅仅是编程技巧的堆砌,更是一种简洁而优化的处理方案,使得开发者在面对特定问题时能够迅速找到行之有效的解决路径。本文将深入探讨设计模式的本质、意义、设计原则以及几种常见的设计模式,帮助读者更好地理解和应用这些宝贵的编程智慧。
文章导航
一、什么是设计模式?
在软件工程中,设计模式是针对软件设计中常见问题的可重用解决方案。设计模式也是经验丰富的开发人员针对特定问题的最佳实践。它可以被当作编程的模板。举例说明:
- 假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,
- 但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东西的位置也不容易。
- 所以在房间里做一些柜子也许是个更好的选择,虽然柜子会增加我们的成本,但它可以在维护阶段为我们带来好处。使用这些柜子存放东西的规则,或许就是一种设计模式。
二、为什么要使用设计模式?
正确使用设计模式,则可以帮助你写出更好的可读性更高的代码,并且代码更容易被维护和理解。
最重要的是,设计模式为软件开发人员提供了通用的词汇表。它们能让学习你代码的人很快了解代码的意图。例如,如果你的项目中使用了装饰器模式,那么新的开发可以很快就知道这段代码的作用,从而他们可以将更多精力放在解决业务问题上,而不是试图理解代码在做什么。
三、设计原则
单一职责原则(SRP)
一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。应该把对象或方法划分成较小的粒度
最少知识原则(LKP)
一个软件实体应当 尽可能少地与其他实体发生相互作用应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的 相互联系,可以转交给第三方进行处理
开放-封闭原则(OCP)
软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定
四、有哪些常见设计模式
单例模式
什么是单例模式呢?构造函数可以创造一个对象,我们 new 很多次构造函数就能得到很多的对象单例模式: 就是使用构造函数实例化的时候,不管实例化多少回,都是同一个对象,也就是一个构造函数一生只能 new 出一个对象也就是说,当我们使用构造函数,每一次 new 出来的对象/属性/功能/方法完全一样的时候,我们把他设计成单例模式。
核心代码
单例模式的核心代码很简单其实就是判断一下,他曾经有没有 new 出来过对象:如果有,就还继续使用之前的那个对象;如果没有,那么就给你 new 一个:
// 普通宠物 (准备一个构造函数,将来要 new 的)
function Pet(name,species){
this.name = name;
this.species = species;
}
// 单例模式函数 => 这个单例模式函数要把 Person 做成一个单例模式
// 将来再想要 new Pet 的时候只要执行这个 GamePet 函数就可以了
// 游戏宠物 => 一个账户只能养一个宠物
let instance = null;
function GamePet(name,species){
if(!instance){
instance = new Pet(name,species);
}
return instance;
}
// 闭包优化版:
let GamePet = (function(){
let instance = null;
return function(name,species){
if(!instance){
instance = new Pet(name,species);
}
return instance;
}
})();
var res = GamePet("毛毛","Cat")
console.log(res);
var res = GamePet("旺财","Dog")
console.log(res);
复用优化
上述案例只能对 Pet 类,实现单例模式,如果其他类也要转为单例默认,代码无法复用,所以可以对单例默认进行封装优化:
let getSingleton = function(fn){ // 接收一个函数为构造函数
let instance = null;
return function(){ // 接收参数 => arguments
if(!instance){
// 调用构造函数 =>
console.log(fn,instance);
instance = new fn(...arguments);
}
return instance;
}
}
let GamePet = getSingleton(Pet);
var res = GamePet("毛毛","Cat")
console.log(res);
var res = GamePet("旺财","Dog")
console.log(res);
组合模式
组合模式,就是把几个构造函数的启动方式组合再一起然后用一个 ”遥控器“ 进行统一调用:
class GetHome {
init () {
console.log('到家了')
}
}
class OpenComputer {
init () {
console.log('打开电脑')
}
}
class PlayGame {
init () {
console.log('玩游戏')
}
}
上面几个构造函数的创造的实例化对象的启动方式都一致那么就可以把这几个函数以组合模式的情况书写, 然后统一启动准备一个组合模式的构造函数:
class Compose {
constructor () {
this.compose = []
}
// 添加任务的方法
add (task) {
this.compose.push(task)
}
// 一个执行任务的方法
execute () {
this.compose.forEach(item => {
item.init()
})
}
}
我们就用我们的组合模式构造函数来把前面的几个功能组合起来:
const c = new Compose()
// 把所有要完成的任务都放在队列里面
c.add(new GetHome())
c.add(new OpenComputer)
c.add(new PlayGame)
// 直接器动任务队列
c.execute()
// 就会按照顺序执行三个对象中的 init 函数
观察者模式
观察者模式通常也被叫做发布-订阅模式或者消息模式, 英文名称叫做 Observer。官方解释: 当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合。
一个例子:
- 当你想去书店买书,但是恰巧今天你要买的书没有了
- 我们又不能总在书店等着,就把我们的手机留给店员
- 当你需要的书到了的时候,他会打电话通知你,你去买了就好了
- 你买到书了以后,就告诉他,我买到了,那么以后再来了书就不会通知你了
addEventListener
addEventListener 是一个我们都用过的东西,它其实就是一个标准的观察者模式。
简单来说,观察者模式就是我们自己实现一个 addEventListener 的功能,只不过 addEventListaner 只有固定的一些事件,而且只能给 dom 元素绑定而我们自己写的可以随便绑定一个事件名称,自己选择触发时机而已。
书写代码
首先我们分析功能
- 我们要有一个观察者(这里抽象为一个对象 {})
- 需要有一个属性,存放消息的盒子(把你绑定的所有事件放在里面-> 对应类型绑定)
- 需要一个 on 方法,用于添加事件
- 需要一个 emit 方法,用于发布事件(触发)
- 需要一个 off 方法,把已经添加的方法取消
const observer = {
message: {},
on: function () {},
emit: function () {},
off: function () {}
}
我们把它写成一个构造函数的形式
class Observer {
constructor () {
this.message = {}
}
on () {}
emit () {}
off () {}
}
现在,一个观察者的雏形就出来了接下来完善方法就可以了
ON
先来写 ON 方法
- 添加一个事件
- 我们的 on 方法需要接受 两个参数
- 事件类型
- 事件处理函数
class Observer {
constructor () {
this.message = {}
}
on(type, fn) {
// 判断消息盒子里面有没有设置事件类型
if (!this.message[type]) {
// 证明消息盒子里面没有这个事件类型
// 那么我们直接添加进去
// 并且让他的值是一个数组,再数组里面放上事件处理函数
this.message[type] = [fn]
} else {
// 证明消息盒子里面有这个事件类型
// 那么我们直接向数组里面追加事件处理函数就行了
this.message[type].push(fn)
}
}
emit () {}
off () {}
}
EMIT
- 接下来就是发布事件
- 也就是让我们已经订阅好的事件执行一下
- 同样需要接受两个参数
- 要触发的事件类型
- 给事件处理函数传递的参数
class Observer {
constructor () {
this.message = {}
}
on(type, fn) {
// 判断消息盒子里面有没有设置事件类型
if (!this.message[type]) {
// 证明消息盒子里面没有这个事件类型
// 那么我们直接添加进去
// 并且让他的值是一个数组,再数组里面放上事件处理函数
this.message[type] = [fn]
} else {
// 证明消息盒子里面有这个事件类型
// 那么我们直接向数组里面追加事件处理函数就行了
this.message[type].push(fn)
}
}
emit(type, ...arg) {
// 判断你之前有没有订阅过这个事件
if (!this.message[type]) return
// 如果有,那么我们就处理一下参数
const event = {
type: type,
arg: arg || {}
}
// 循环执行为当前事件类型订阅的所有事件处理函数
this.message[type].forEach(item => {
item.call(this, event)
})
}
off() {}
}
OFF
- 最后就是移除事件
- 就是把已经订阅的事件处理函数移除掉
- 同样需要接受两个参数
- 要移除的事件类型
- 要移除的事件处理函数
class Observer {
constructor () {
this.message = {}
}
on(type, fn) {
// 判断消息盒子里面有没有设置事件类型
if (!this.message[type]) {
// 证明消息盒子里面没有这个事件类型
// 那么我们直接添加进去
// 并且让他的值是一个数组,再数组里面放上事件处理函数
this.message[type] = [fn]
} else {
// 证明消息盒子里面有这个事件类型
// 那么我们直接向数组里面追加事件处理函数就行了
this.message[type].push(fn)
}
}
emit(type, ...arg) {
// 判断你之前有没有订阅过这个事件
if (!this.message[type]) return
// 如果有,那么我们就处理一下参数
const event = {
type: type,
arg: arg || {}
}
// 循环执行为当前事件类型订阅的所有事件处理函数
this.message[type].forEach(item => {
item.call(this, event)
})
}
off (type, fn) {
// 判断你之前有没有订阅过这个事件
if (!this.message[type]) return
// 如果有我们再进行移除
for (let i = 0; i < this.message[type].length; i++) {
const item = this.message[type][i]
if (item === fn) {
this.message[type].splice(i, 1)
i--
}
}
}
}
结语
设计模式不仅提高了代码的可读性、可维护性和可扩展性,还可以促进团队成员之间的沟通与协作。通过学习和掌握设计模式,开发者能够更加高效地解决复杂问题,编写出更加灵活和易于理解的代码。然而,值得注意的是,设计模式并非一成不变的教条,而是需要根据实际情况灵活运用的工具。在软件开发过程中,我们应该始终保持开放的心态,不断探索和创新,将设计模式与具体业务场景相结合,创造出更加实用的软件产品。
延展阅读:
toFixed数字精度丢失问题怎么解决?银行家舍入规则是什么?
咨询方案 获取更多方案详情