【青训营】- 浅谈JavaScript中的几种设计模式(图文并茂带实例)

463 阅读6分钟

这是我参与8月更文挑战的第29天,活动详情查看:8月更文挑战



通过结合实际场景,真实案例,来一步一步分析理解设计模式是怎么?怎么用?什么时候应该用?

前言

本文适合人群:和我一样的设计模式入门小白📖

什么是设计模式?

就是在软件设计过程中,针对特定问题的简洁而优雅的解决方案;

简单的说就是前辈们给我们的开发经验总结;

image-20210829133618811

SOLID五大设计原则

image-20210829135035039

为什么需要设计模式?

image-20210829135558014

常见设计模式

单例模式

示例

假如要创建一个登陆弹窗(伪代码)

image-20210829174139949

写了一个createLoginLayer来创建登陆弹窗(默认隐藏),点击登陆按钮的时候,创建这个登录弹窗,并且让它显示出来;

这样存在什么问题?

可以看到,每次点击的时候都会重复的执行createLoginLayer,即每次点击都会重复的创建一个弹窗,但是我们只需要创建一个就够了

改写

image-20210829174448084

我们新建一个getSingle函数,来接受一个函数,并且用result把结果缓存下来

const getSingle = (fn) => {
    let result;
    return (...rest) => {
        return result || (result = fn.apply(this, rest));
    };
};

创建的时候,取getSingle里面的result,这样业务逻辑只会被执行一次,保证不会重复创建实例,每次取的都是第一次创建的值。

定义

唯一&全局访问。保证一个类仅有个实例,并提供一个访问它的全局访问点。

应用场景

能被缓存的内容,例如登录弹窗。



策略模式

示例

假如当前我们要根据等级和薪资来计算一个奖金,规则是这样的

image-20210829170155159

这要让你来实现,你会怎么做?

很简单,直接使用if-else或者switch就完事了

image-20210829170810736

可是这样写会有什么问题呢?

  • 如果不止三个等级,而是十几等级的时候,我们这个函数将会有一堆的if...else或者一堆的case语句;

这个时候就可以用到策略模式了

使用策略模式改写:

image-20210829171746127

解析

我们构造了一个策略表(strategies),在策略表里面去维护计算规则,函数(calculateBonus)只需要去策略表找规则,而自身逻辑不会被改变;

延伸

如果情况多的时候,甚至可以把策略表单独分离成一个文件,或者放在其他地方,由其他人维护

定义

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。把看似毫无联系的代码提取封装、复用,使之更容易被理解和拓展。

应用场景

要完成一件事情,有不同的策略。例如绩效计算、表单验证规则。



代理模式

示例

现在我们想要渲染一张图片,这个图片体积可能比较大,网站拉取图片的时候可能会比较费时,在图片还没有返回时,我们先使用loading来代替

image-20210829200819955

存在问题

可以看到,我们的rawImage方法,本身的作用,应该是设置我们从服务端接收到的图片,但是现在,它既设置了loading图片,又进行了返回图片的替换,这就不符合我们【单一职责】的原则了,耦合度比较高。

改写

image-20210829175801388

我们可以用通过代理模式将【创建图片】与【预加载逻辑】分离, 这样rawImage只负责设置图片,而通过proxyImage来设置loading图片,未来如果不需要预加载了, 只要改成请求本体代替请求代理对象就行。

定义

为一个对象提供个代用品或占位符, 以便控制对它的访问。替身对象可对请求预先进行处理,再决定是否转交给本体对象。

应用场景

当我们不方便直接访问某个对象时,或不满足需求时,可考虑使用一个替身对象来控制该对象的访问。



装饰器模式

示例

以吃鸡游戏为例,刚开始的时候,玩家一无所有,但是后面可以通过捡或抢装备来武装自己,最终获得游戏的胜利。

image-20210829185621722

代码实现

image-20210829192317839

我们通过Decorator实现了一个相同的equip()方法,并且在其中封装了getHelmet、getBodyArmor、getKar98等额外的方法,在同样调用equip()方法的同时,也没有改变原对象自身。

定义

能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责(方法)。

应用场景

数据上报、统计函数执行时间。



适配器模式

示例

假设现在要渲染地图A和地图B,但是他们初始化的方式不相同,首先想到的办法或许是,加一个判断,如果是地图a,则使用a的初始化方法,地图b则使用地图b的方法

image-20210829200030162

但是除此之外,我们其实还可以通过适配器模式来改写

image-20210829200755148

我们可以新增一个适配层,通过这种方法来让他们的初始化方法变成一致。

定义

解决两个软件实体间的接口不兼容问题,不需要改变已有的接口,就能够使它们协同作用。

应用场景

接口不兼容的情况。

适配器模式是一种“亡羊补牢”的模式,没有人会在程序的设计之初就使用它。适配器模式和有的模式的结构非常相似,比如装饰者模式、代理模式和外观模式,这几种模式都属于“包装模式”,都是由一个对象来包装另一个对象。区别它们的关键就是模式的意图。

扩展

设计模式分类

  • 创建型
    • 单例模式
    • 原型模式
    • 工厂模式
    • 抽象工厂模式
    • 建造者模式
  • 结构型
    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 外观模式
    • 桥接模式
    • 组合模式
    • 享元模式
  • 行为型
    • 观察者模式
    • 迭代器模式
    • 策略模式
    • 模板方法模式
    • 职责链模式
    • 命令模式
    • 备忘录模式
    • 状态模式
    • 访问者模式
    • 中介者模式
    • 解释器模式

结语

光看【设计模式】几个词,不知道有没有和我一样觉得很头疼的小伙伴,感觉就是很理论的一个东西,但是从上面几个简单的示例来看,我们平时开发中好像或多或少都用到了设计模式,只是很多时候不知道自己使用了哪种设计模式或者说该使用何种设计模式,并且如果细看可以发现很多代码重构或者优化的技巧都和设计模式有关,由此可见学习设计模式还是很有必要的,但是也不要为了用设计模式而去用设计模式哦!

参考:《JavaScript 设计模式与开发实践》

如以上有错误的地方,请在评论区中指出!


如果有收获的话,就留个鼓励一下吧!🍜