浅谈前端设计模式:策略模式和状态模式的异同点

news2024/12/23 3:32:14

一、策略模式

策略模式是定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。 而且策略模式是重构小能力,特别适合拆分“胖逻辑”

这个定义乍一看会有点懵,不过通过下面的例子就能慢慢理解它的意思。

先来看一个真实场景

某次活动要做差异化询价。啥是差异化询价?就是说同一个商品,我通过在后台给它设置不同的价格类型,可以让它展示不同的价格。具体的逻辑如下:

  • 当价格类型为“预售价”时,满 100 - 20,不满 100 打 9 折
  • 当价格类型为“大促价”时,满 100 - 30,不满 100 打 8 折
  • 当价格类型为“返场价”时,满 200 - 50,不叠加
  • 当价格类型为“尝鲜价”时,直接打 5 折

首先将四种价格做了标签化:

预售价 - pre
大促价 - onSale
返场价 - back
尝鲜价 - fresh 

大部分人可能会这么写

// 询价方法,接受价格标签和原价为入参
function askPrice(tag, originPrice) {// 处理预热价if(tag === 'pre') {if(originPrice >= 100) {return originPrice - 20} return originPrice * 0.9}// 处理大促价if(tag === 'onSale') {if(originPrice >= 100) {return originPrice - 30} return originPrice * 0.8}// 处理返场价if(tag === 'back') {if(originPrice >= 200) {return originPrice - 50}return originPrice}// 处理尝鲜价if(tag === 'fresh') { return originPrice * 0.5}
} 

上述代码运行起来确实没啥毛病。但也只是“运行起来”没毛病而已。

问题点:

  • 首先,它违背了“单一功能”原则。一个函数里面竟然处理了四个逻辑,这个函数的逻辑太胖了!而且单个能力很难被抽离复用。
  • 另外,它还违背了“开放封闭”原则。假如再加一个满 100 - 50 的“新人价”就只能在这个函数中修改代码,继续加if逻辑。

重构询价逻辑

现在我们基于设计原则思想(“单一功能”和开放封闭”原则),一点一点改造掉这个臃肿的 askPrice。

单一功能改造

首先,我们赶紧把四种询价逻辑提出来,让它们各自为政:

// 处理预热价
function prePrice(originPrice) {if(originPrice >= 100) {return originPrice - 20} return originPrice * 0.9
}

// 处理大促价
function onSalePrice(originPrice) {if(originPrice >= 100) {return originPrice - 30} return originPrice * 0.8
}

// 处理返场价
function backPrice(originPrice) {if(originPrice >= 200) {return originPrice - 50}return originPrice
}

// 处理尝鲜价
function freshPrice(originPrice) {return originPrice * 0.5
}

function askPrice(tag, originPrice) {// 处理预热价if(tag === 'pre') {return prePrice(originPrice)}// 处理大促价if(tag === 'onSale') {return onSalePrice(originPrice)}// 处理返场价if(tag === 'back') {return backPrice(originPrice)}// 处理尝鲜价if(tag === 'fresh') { return freshPrice(originPrice)}
} 

OK,我们现在至少做到了一个函数只做一件事。现在每个函数都有了自己明确的、单一的分工。

到这里,在单一功能原则的指引下,我们已经解决了一半的问题。我们已经把“询价逻辑的执行”给摘了出去,并且实现了不同询价逻辑之间的解耦。

开放封闭改造

如果现在要想给 askPrice 增加新人询价逻辑,应该怎么办? 如果在 askPrice 里面,新增了一个 if-else 判断。这样其实还是在修改 askPrice 的函数体,没有实现“对扩展开放,对修改封闭”的效果。

那么我们应该怎么做?我们仔细想想,这么多 if-else,我们的目的到底是什么?是不是就是为了把 询价标签-询价函数 这个映射关系给明确下来?那么在 JS 中,有没有什么既能够既帮我们明确映射关系,同时不破坏代码的灵活性的方法呢?答案就是对象映射

咱们完全可以把询价算法全都收敛到一个对象里去:

// 定义一个询价处理器对象
const priceProcessor = {pre(originPrice) {if (originPrice >= 100) {return originPrice - 20;}return originPrice * 0.9;},onSale(originPrice) {if (originPrice >= 100) {return originPrice - 30;}return originPrice * 0.8;},back(originPrice) {if (originPrice >= 200) {return originPrice - 50;}return originPrice;},fresh(originPrice) {return originPrice * 0.5;},
}; 

当我们想使用其中某个询价算法的时候:通过标签名去定位就好了:

// 询价函数
function askPrice(tag, originPrice) {return priceProcessor[tag](originPrice)
} 

如此一来,askPrice 函数里的 if-else 大军彻底被咱们消灭了。这时候如果你需要一个新人价,只需要给 priceProcessor 新增一个映射关系即可:

priceProcessor.newUser = function (originPrice) {if (originPrice >= 100) {return originPrice - 50;}return originPrice;
} 

这样一来,询价逻辑的分发也变成了一个清清爽爽的过程。二、状态模式

状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。

状态模式和策略模式宛如一对孪生兄弟——它们长得很像、解决的问题也可以说没啥本质上的差别。

以咖啡机为例讲状态模式

在这个能做四种咖啡的咖啡机体内,蕴含着四种状态:

- 美式咖啡态(american):只吐黑咖啡
- 普通拿铁态(latte):黑咖啡加点奶
- 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
- 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力 

大部分人第一反应还是会先用if-else完成如下

class CoffeeMaker {constructor() {/**这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑**/// 初始化状态,没有切换任何咖啡模式this.state = 'init';}// 关注咖啡机状态切换函数changeState(state) {// 记录当前状态this.state = state;if(state === 'american') {// 这里用 console 代指咖啡制作流程的业务逻辑console.log('我只吐黑咖啡');} else if(state === 'latte') {console.log(`给黑咖啡加点奶`);} else if(state === 'vanillaLatte') {console.log('黑咖啡加点奶再加香草糖浆');} else if(state === 'mocha') {console.log('黑咖啡加点奶再加点巧克力');}}
} 

测试一下,完美无缺:

const mk = new CoffeeMaker();
mk.changeState('latte'); // 输出 '给黑咖啡加点奶' 

但是考虑到设计原则中的“单一职责、开放封闭”原则,我们还需要如同上面策略模式中的改造方法进行对上面代码的改造。

根据“单一职责、开放封闭”原则改造后

const stateToProcessor = {american() {console.log('我只吐黑咖啡');},latte() {this.american();console.log('加点奶');},vanillaLatte() {this.latte();console.log('再加香草糖浆');},mocha() {this.latte();console.log('再加巧克力');}
}

class CoffeeMaker {constructor() {/**这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑**/// 初始化状态,没有切换任何咖啡模式this.state = 'init';}// 关注咖啡机状态切换函数changeState(state) {// 记录当前状态this.state = state;// 若状态不存在,则返回if(!stateToProcessor[state]) {return ;}stateToProcessor[state]();}
}

const mk = new CoffeeMaker();
mk.changeState('latte'); 

输出结果符合预期:

我只吐黑咖啡
加点奶 

现在看起来好像很完善了,但是stateToProcessor 里的工序函数,感知不到咖啡机的内部状况。

为了让stateToProcessor里的工序函数能感知到咖啡机的内部状况,把状态-行为映射对象作为主体类对应实例的一个属性添加进去就行了:

class CoffeeMaker {constructor() {/**这里略去咖啡机中与咖啡状态切换无关的一些初始化逻辑**/// 初始化状态,没有切换任何咖啡模式this.state = 'init';// 初始化牛奶的存储量this.leftMilk = '500ml';}stateToProcessor = {that: this,american() {// 尝试在行为函数里拿到咖啡机实例的信息并输出console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)console.log('我只吐黑咖啡');},latte() {this.american()console.log('加点奶');},vanillaLatte() {this.latte();console.log('再加香草糖浆');},mocha() {this.latte();console.log('再加巧克力');}}// 关注咖啡机状态切换函数changeState(state) {this.state = state;if (!this.stateToProcessor[state]) {return;}this.stateToProcessor[state]();}
}

const mk = new CoffeeMaker();
mk.changeState('latte'); 

输出结果为:

咖啡机现在的牛奶存储量是: 500ml
我只吐黑咖啡
加点奶 

如此一来,我们就可以在 stateToProcessor 轻松拿到咖啡机的实例对象,进而感知咖啡机这个主体了。

策略模式和状态模式的异同点

策略模式和状态模式确实是相似的,它们都封装行为、都通过委托来实现行为分发。但策略模式中的行为函数是“潇洒”的行为函数(策略模式是对算法的封装,封装的算法可以单独使用),它们不依赖调用主体、互相平行、各自为政,井水不犯河水。而状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。

前端中的状态模式应用——javascript-state-machine

最后

整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/342131.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

测试2年还拿实习生的薪资打发我,你后悔去吧····

20年7月大学毕业,学的计算机科学专业。因为考研之后,秋招结束了。没什么更多的岗位选择,就想找个工作先干着,然后亲戚在一家大厂公司上班说要招测试,所以就来做测试了。 虽然都是属于计算机大类,但自己专业…

记一次 .NET 某游戏网站 CPU爆高分析

一:背景 1. 讲故事 这段时间经常有朋友微信上问我这个真实案例分析连载怎么不往下续了,关注我的朋友应该知道,我近二个月在研究 SQLSERVER,也写了十多篇文章,为什么要研究这东西呢? 是因为在 dump 中发现…

零入门kubernetes网络实战-13->同一宿主机上的两个网络命名空间通信方案

《零入门kubernetes网络实战》视频专栏地址 https://www.ixigua.com/7193641905282875942 本篇文章视频地址(稍后上传) 本篇文章主要是想模拟一下,在同一个宿主机上,多个网络命名空间之间如何通信? 有哪些可以采取的方案。 可能存在的方案…

【GD32F427开发板试用】6. 定时器运用之精确定时1s

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动,更多开发板试用活动请关注极术社区网站。作者:hehung 之前发帖 【GD32F427开发板试用】1. 串口实现scanf输入控制LED 【GD32F427开发板试用】2. RT-Thread标准版移植 【GD32F427开发板试用…

MySQL:连explain的type类型都没搞清楚,怎敢说精通SQL优化?

我们在使用SQL语句查询表数据时,提前用explain进行语句分析是一个非常好的习惯。通过explain输出sql的详细执行信息,就可以针对性的进行sql优化。 今天我们来分析一下,在explain中11种不同type代表的含义以及其应用场景。 1,sys…

如何在dom节点上使用fixed定位?实现基于父节点而不是浏览器的滚动定位?一眼看懂,简单明了。

文章目录一、想要实现的功能场景二、父相子绝方式实现dom节点内元素滚动定位2.1、父相子绝代码2.2、实现效果三、iframe中使用fixed方式实现3.1、fixed代码3.2、实现效果四、总结一、想要实现的功能场景 想在一个node节点中实现滚动,但是我的文本要基于这个元素在滚…

云计算关键技术

在云计算数据中心的构建过程中,离不开一些关键的技术,比如,虚拟化技术,分布式数据存储技术,云计算平台管理软件以及其它诸如HBase,Hadoop等相关技术。目录 虚拟化技术 计算虚拟化技术-KVM 分布式数据存储技…

基于 YAML 接口自动化测试框架设计

在设计自动化测试框架的时候,我们会经常将测试数据保存在外部的文件(如Excel、YAML、CSV),或者数据库中,实现脚本与数据解耦,方便后期维护。目前非常多的自动化测试框架采用通过Excel或者YAML文件直接编写测…

Allegro如何设置自动保存和自动保存的时间操作指导

Allegro如何设置自动保存和自动保存的时间操作指导 做PCB设计的时候,自动保存软件是一个必要的功能,Allegro同样支持设置自动保存,而且可以设置自动保存的时间。 如下图 具体操作如下 点击Setup点击User Preferences

都说高速信号过孔尽量少,高速先生却说有时候多点反而好?

作者:一博科技高速先生成员 黄刚过孔在高速领域可谓让硬件工程师,PCB设计工程师甚至仿真工程师都闻风丧胆,首先是因为它的阻抗没法像传输线一样,通过一些阻抗计算软件来得到,一般来说只能通过3D仿真来确定,…

二叉树的性质与推导及常见习题整理

目录 一、性质推导 二、常见的二叉树性质习题 1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为()。 2.在具有 2n 个结点的完全二叉树中,叶子结点个数为(&#xff…

字母板上的路径[提取公共代码,提高复用率]

提取公共代码前言一、字母版上的路径二、贪心1、idea2、go3、代码不断拆分复用的过程总结参考文献前言 写代码,在提高效率的同时,要方便人看,这个人包括自己。大函数要拆分成一些小函数,让每个函数的宏观目的和步骤都显得清晰&am…

分享微信点餐小程序搭建步骤_微信点餐功能怎么做

线下餐饮实体店都开始摸索发展网上订餐服务。最多人选择的是入驻外卖平台,但抽成高,推广还要另买流量等问题,也让不少商家入不敷出。在这种情况下,建立自己的微信订餐小程序,做自己的私域流量是另一种捷径。那么&#…

分类模型评估:混淆矩阵、准确率、召回率、ROC

1. 混淆矩阵 在二分类问题中,混淆矩阵被用来度量模型的准确率。因为在二分类问题中单一样本的预测结果只有Yes or No,即:真或者假两种结果,所以全体样本的经二分类模型处理后,处理结果不外乎四种情况,每种…

反应-扩散方程(Reaction-diffusion system)

文章目录单组分反应-扩散方程双组分反应-扩散方程三组分和更多组分的反应-扩散方程Fishers equationKPP方程Belousov–Zhabotinsky reaction历史化学机理变体Noise-induced order数学背景Briggs–Rauscher reactionZFK equation行波解渐近解外部区域内部区域KPP-ZFK 转变Clavin…

13-PHP使用过的函数 121-130

121、bindColumn 将字段绑定到变量上 // while,foreach,list()进行结果数组的解构,解构到变量中; //!要在预处理对象上调用bindColumn函数 $stmt->bindColumn(id,$id); $stmt->bindColumn(name,$name); $stmt->bindColumn(sex,$sex); $stmt->bindColumn…

flink solt概念详解

ask是flink中的一个逻辑概念,一个任务由一个或者多个算子组合而成(多个算子构成一个任务是需要满足一定的条件才可以,有兴趣的老铁可以来了解一下 Operator Chain),为了提升任务执行的效率,可以对任务配置并行度,使任务在实际运行…

【服务器数据恢复】FreeNAS层UFS2文件系统数据恢复案例

服务器数据恢复环境: Dell存储服务器,采用esxi虚拟化系统,esxi虚拟化系统里有3台虚拟机;上层iSCSI使用FreeNAS构建,通过iSCSI方式实现FCSAN功能;FreeNAS层采用UFS2文件系统。 esxi虚拟化系统里有3台虚拟机中…

python中使用numpy包的向量矩阵相乘

一直对np的线性运算不太清晰,正好上课讲到了,做一个笔记整个理解一下 1.向量和矩阵 在numpy中,一重方括号表示的是向量vector,vector没有行列的概念。二重方括号表示矩阵matrix,有行列。 代码显示如下: …

VMware 复制已有的虚拟机并修改IP地址

第一步:关闭当前机器第二步:在VMware中右键要克隆的机器 选择管理-->克隆第三步:启动新克隆的虚拟机 修改主机名 如 hostname slave2第四步:修改克隆的虚拟机的ip地址和mac地址(注意:由于slave2是克隆出…