设计模式-策略模式
前言
作为一名合格的前端开发工程师,全面的掌握面向对象的设计思想非常重要,而“设计模式”是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的,代表了面向对象设计思想的最佳实践。正如《HeadFirst设计模式》中说的一句话,非常好:
知道抽象、继承、多态这些概念,并不会马上让你变成好的面向对象设计者。设计大师关心的是建立弹性的设计,可以维护,可以应付改变。
是的,很多时候我们会觉得自己已经清楚的掌握了面向对象的基本概念,封装、继承、多态都能熟练使用,但是系统一旦复杂了,就无法设计一个可维护、弹性的系统。本文将结合 《HeadFirst设计模式》书中的示例加上自己一丢丢的个人理解,带大家认识下设计模式中的第一个模式——策略模式。
策略模式
策略模式:Strategy,是指,定义一组算法,并把其封装到一个对象中。然后在运行时,可以灵活的使用其中的一个算法
模拟鸭子游戏
设计模拟鸭子游戏
一个游戏公司开发了一款模拟鸭子的游戏,所有的鸭子都会呱呱叫(quack)、游泳(swim) 和 显示(dislay) 方法。
基于面向对象的设计思想,想到的是设计一个 Duck
基类,然后让所有的鸭子都集成此基类。
class Duck {quack() {}swim() {}display() {}
}
绿头鸭(MallardDuck)和红头鸭(RedheadDuck)分别继承 Duck
类:
class MallardDuck extends Duck {quack() {console.log('gua gua');}display() {console.log('I am MallardDuck');}
}class RedheadDuck extends Duck {display() {console.log('I am ReadheadDuck');}quack() {console.log('gua gua');}
}
让所有的鸭子会飞
现在对所有鸭子提出了新的需求,要求所有鸭子都会飞。
设计者立马想到的是给 Duck
类添加 fly
方法,这样所有的鸭子都具备了飞行的能力。
class Duck {quack() {}fly() {}swim() {}display() {}
}
但是这个时候代码经过测试发现了一个问题,系统中新加的橡皮鸭(RubberDuck)也具备了飞行的能力了。这显然是不科学的,橡皮鸭不会飞,而且也不会叫,只会发出“吱吱”声。
于是,设计者立马想到了覆写 RubberDuck
类的 duck
和 fly
方法,其中 fly
方法里面什么也不做。
class RubberDuck extends Duck {quack() {console.log('zhi zhi');}fly() {}
}
继承可能并不是最优解
设计者仔细思考了上述设计,提出了一个问题:如果后续新增了更多类型的鸭子,有的鸭子既不会飞又不会叫怎么办呢?难道还是继续覆写 fly
或者 quack
方法吗?
显然,集成不是最优解。
经过一番思索,设计者想到了通过接口来优化设计。
设计两个接口,分别是 Flable
和 Quackable
接口。
interface Flyable {fly(): void;
}interface Quackable {quack(): void;
}
这样,只有实现了 Flyable
的鸭子才能飞行,实现了 Quackable
的鸭子才能说话。
class MallardDuck implements Flayable, Quackable {fly() {}quack() {}
}
通过接口虽然可以限制鸭子的行为,但是每个鸭子都要检查一下是否需要实现对应的接口,鸭子类型多起来之后是非常容易出错的,同时,通过接口的方式虽然限制了鸭子的行为,但是代码量却没有减少,每个鸭子内部都要重复实现fly和quack的代码逻辑。
分开变化和不变化的部分
下面开始介绍我们的第一个设计原则:
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
在 Duck
类中,quack
和 fly
是会随着鸭子的不同而改变的,而 swim 和 display 是每个鸭子都不变的。因此,这里可以运用第一个设计原则,就是分开变化和不变化的部分。
面向接口编程,而不是针对实现编程
为了更好的设计我们的代码,现在介绍第二个设计原则:
针对接口编程,而不是针对实现编程。
针对接口编程的真正含义是针对超类型编程(抽象类或者接口),它利用了多态的特性。
在针对接口的编程中,一个变量声明的类型应该是一个超类型,超类型强调的是它与它的所有派生类共有的“特性”。
针对实现编程。
比如:
interface Animal {void makeSound();
}class Dog implements Animal {public void makeSound() {bark();}public void bark() {// 汪汪叫}
}Dog d = new Dog();
d.bark();
因为 d
的类型是 Dog
,是一个具体的类,而不是抽象类,并且 bark
方法是 Dog
上特有的,不是共性。
针对接口编程。
Animal a = new Dog();
a.makeSound();
变量 a
的类型是 Animal
,是一个抽象类型,而不是一个具体类型。此时 a
调用 makeSound
方法,代表的是所有的 Animal
都能进行的一种操作。
现在我们接着之前的思路,将鸭子的 fly
和 quack
两个行为变为两个接口 FlyBehavior
和 QuackBehavior
。所有的鸭子不直接实现这两个接口,而是有专门的行为类实现这两个接口。
interface FlyBehavior {fly(): void;
}interface QuackBehavior {quack(): void;
}
行为类来实现接口:
// 实现了所有可以飞行的鸭子的动作
class FlyWithWings implements FlyBehavior {fly(): void {console.log('I can fly with my wings !');}
}
// 实现了所有不会飞行的鸭子的动作
class FlyNoWay implements FlyBehavior {fly(): void {console.log('I can not fly !');}
}
// 实现了所有坐火箭飞行的鸭子的动作
class FlyRocketPowered implements FlyBehavior {fly(): void {console.log('I can fly with a rocket !');}
}
// 实现了橡皮鸭的吱吱叫声
class Squeak implements QuackBehavior {quack(): void {console.log('zhi zhi !');}
}
// 实现了哑巴鸭的叫声
class MuteQuack implements QuackBehavior {quack(): void {console.log();}
}
这样做有两个好处:
- 鸭子的行为可以被复用,因为这些行为已经与鸭子本身无关了。
- 我们可以新增一些行为,不会担心影响到既有的行为类,也不会影响有使用到飞行行为的鸭子类。
整合鸭子的行为
现在鸭子的所有的行为需要被整合在一起,需要委托给别人处理。
继续改造 Duck
类。
abstract class Duck {flyBehavior: FlyBehavior;quackBehavior: QuackBehavior;constructor(flyBehavior: FlyBehavior, quackBehavior: QuackBehavior) {this.flyBehavior = flyBehavior;this.quackBehavior = quackBehavior;}public performFly(): void {this.flyBehavior.fly();}public performQuack():void {this.quackBehavior.quack();}public setFlyBehavior(flyBehavior: FlyBehavior) {this.flyBehavior = flyBehavior;}public abstract display(): void;public swim() {console.log('all ducks can swim !');}
}
在鸭子类内部定义两个变量,类型分别为 FlyBehavior
和 QuackBehavior
的接口类型,声明为接口类型方便后续通过多态的方式设置鸭子的行为。移除鸭子类中的 fly
和 quack
方法,因为这两个方法已经被分离到 fly
行为类和 quack
行为类中了。
通过 performQuack
方法来调用鸭子的行为,setFlyBehavior
方法来动态修改鸭子的行为。
所有的鸭子集成 Duck
类:
// 绿头鸭
class MallardDuck extends Duck {constructor() {super(new FlyWithWings(), new Quack());}display() {console.log('I am mallard duck !');}
}// 模型鸭
class ModelDuck extends Duck {constructor() {super(new FlyNoWay(), new MuteQuack());}public display(): void {console.log('I am model duck !');}
}
在鸭子的构造函数中调用父类的构造函数,初始化鸭子的行为。
测试鸭子游戏
class Test {duck: Duck;constructor() {this.duck = new MallardDuck();}setPerformFly() {this.duck.setFlyBehavior(new FlyRocketPowered());}quack() {this.duck.performQuack();}fly() {this.duck.performFly();}
}const test = new Test();test.fly();
test.quack();test.setPerformFly();test.fly();
通过 setFlyBehavior
可以动态的改变鸭子的行为,是鸭子具备坐火箭飞行的能力。
多用组合,少用集成
从上面的例子就可以看出,每一个鸭子都有一个 FlyBehavior
和 QuackBehavior
,让鸭子(Duck
类)将飞行和呱呱叫委托它代为处理。
当你将两个类结合起来使用,这就是组合(composition
)。这种做法和继承不同的地方在于,鸭子的行为不是继承而来,而是和使用的行为对象组合而来的。也就是我们要介绍的第三个设计原则:
多用组合,少用继承
策略模式的设计步骤
- 定义一个接口,接口中声明各个算法所共有的操作
interface Strategy {execute(): void;
}
- 定义一系列的策略,并且在遵循
Strategy
接口的基础上实现算法
class StrategyA implements Strategy {execute() {console.log('I am StrategyA'); // 算法 A}
}class StrategyB implements Strategy {execute() {console.log('I am StrategyB'); // 算法 B}
}
- 定义一个上下文
Context
类,在Context
类中维护指向某个策略对象的引用,在构造函数中来接收策略类,同时还可以通过setStrategy
在运行时动态的切换策略类,Context
类通过setStrategy
来将具体的工作委派给策略对象。这里的Context
类也可以作为一个基类被具体的实现类所继承,就相当于上文鸭子游戏中介绍的 Duck 类,可以被 MallardDuck、ReadheadDuck 等继承。
class Context {private strategy: Strategy;constructor(stategy: Stategy) {this.stategy = Stategy;}executeStrategy() {this.stragegy.execute();}setStrategy(stategy: Stategy) {this.stategy = stategy;}
}
- 创建客户端类,客户端代码会根据条件来选择具体的策略。
class Application {stragegy: Stragegy;constructor(stragegy: Stragegy) {this.stragegy = stragegy;}setCondition(condition) {if (condition === 'conditionA') {stragegy.setStrategy(new StrategyA());}if (condition === 'conditionB') {stragegy.setStrategy(new StrategyB());}}execute() {this.stragegy.execute();}
}const app = new Application();app.setCondition('conditionB');
app.execute();
策略模式结构图:
策略模式的使用场景
当我们在设计代码的时候,想使用对象中各种不同的算法变体,并希望能在运行时切换算法时, 可以考虑使用策略模式。
参考
源代码地址
- 针对接口编程,而不是针对实现编程
- 策略模式
- 《HeadFirst设计模式》
相关文章:

设计模式-策略模式
前言 作为一名合格的前端开发工程师,全面的掌握面向对象的设计思想非常重要,而“设计模式”是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的,代表了面向对象设计思想的最佳实践。正如《HeadFirst设计模式》中说的一句话&…...

面试+算法:罗马数字及Excel列名与数字互相转换
概述 算法是一个程序员的核心竞争力,也是面试最重要的考查环节。 试题 判断一个罗马数字是否有效 罗马数字包含七种字符:I,V,X,L,C,D和M,如下 字符数值I1V5X10L50C100D500M1000…...
Connext DDS路由服务Routing Service(1)
1 简介 RTI路由服务是一种开箱即用的解决方案,允许开发人员快速扩展和集成不同或地理位置分散的实时系统。它跨域、LAN和WAN扩展RTI ConnextDDS应用程序,包括防火墙和NAT穿越。 它还支持DDS到DDS的桥接,允许您对数据进行转换。这允许未修改的DDS应用程序进行通信,即使它们是…...

如何使用SaleSmartly进行Facebook Messenger 营销、销售和支持
如何使用SaleSmartly(ss客服)进行Facebook Messenger 营销、销售和支持上篇文章我们讲了什么是Facebook Messenger CRM以及获得Facebook Messenger CRM的注意事项,现在你有更多时间与客户聊天,让我们看看你如何使用SaleSmartly&am…...

教资教育知识与能力中学教学
目录 3.1 教学概述 3.2 教学过程 3.3 教学原则*【简答/辨析重点】 3.4 教学方法 3.5 教学组织形式 3.6 教学工作基本环节 3.7 教学评价 3.1 教学概述 1、教学的意义【14/18辨析】 教学是传授系统知识、促进学生发展的最有效形式; 教学是学校进行全面发展教…...

IDEA中使用Tomcat的两种方式:集成本地Tomcat使用Tomcat Maven插件
一、前言 在IDEA中创建完一个Maven Web项目,并补齐了目录以后,准备使用Tomcat时,就需要在自己创建的项目中去部署Tomcat,前文已经介绍了如何创建Maven Web,所以这里就不多加赘述,直接讲述部署Tomcat的方法…...

IP 地址的简介
IP 地址 Internet 依靠 TCP/IP 协议,在全球范围内实现不同硬件结构、不同操作系统、不同网络系统的主机之间的互联。在 Internet 上,每一个节点都依靠唯一的 IP 地址相互区分和相互联系,IP 地址用于标识互联网中的每台主机的身份,…...
3D动作/动画特效
硕士/博士符合一本高校人才引进条件的硕士、博士,教研能力突出者可签合作高校正式编制本科/硕士成绩优异专业扎实、有创新思维者可在签约工作后在校继续读研读博【产业模式】数字经济→数字孪生→升级转型【细份领域】数字产业、数字工程、数字教研、数字政企【合作…...
python 多线程编程之_thread模块
_thread模块除了可以派生线程外,还提供了基本的同步数据结构,又称为锁对象(lock object,也叫原语锁、简单锁、互斥锁、互斥和二进制信号量)。 下面是常用的线程函数: 函数描述start_new_thread(function,…...

vue:vue2与vue3的区别
一、背景 vue2是指的2.X vue3是指的3.0以及更新的版本(3.2版本在script标签里可以写setup,极大的简化了开发) 本文对比两者区别。 二、官网 生命周期选项 | Vue.js API 参考 | Vue.js Vue.js - 渐进式 JavaScript 框架 | Vue.js Vue.…...
SQL数据库语法
目录 1. 常用数据类型 2. 约束 4. 数据库操作 5. 数据表操作 1. 常用数据类型 int 整型double 浮点数varchar 字符型data 年月日datetime 年月日 时分秒2. 约束 主键 primary key : 物理上存储的顺序(存在真实排序), 主键…...

人机界面艺术设计
人机界面艺术设计 2.1人机界面艺术设计思路 人们经常有意通过某种工具或创造来解决难题,然而这并不意味着人们乐于接受别人或其他事情,他们很难提出问题。在用户使用网页或软件的时候,他们有明确的目标,他们利用电脑来帮助自己达…...

【办公类-19-01-02】办公中的思考——Python,统计教职工的姓名中那些字最多?
背景需求:上一篇计算了教职工的姓氏谁最多,col[0]]这一篇统计教职工的(姓氏名字)里面哪些字出现最多。材料准备:1、下载所有员工名单写代码。py 包含”姓氏名字“的重字率统计from pandas import DataFrame, Series im…...

HCIP实验1
实验要求 1 R6为isp, 接口IP地址均为公有地址;该设备只能配置IP地址,之后不能冉对其进行其他任何配置; 2 R1-R5为局域网,私有IP地址192.168.1.0/24, 请合理分配; 3 R1, R2, R4,各有两个环回地址; R5; R6各有一个环回地址;所有路由器上环回均…...

一个Bug让人类科技倒退几十年?
大家好,我是良许。 前几天在直播的时候,问了直播间的小伙伴有没人知道「千年虫」这种神奇的「生物」的,居然没有一人能够答得上来的。 所以,今天就跟大家科普一下这个人类历史上最大的 Bug 。 1. 全世界的恐慌 一个Bug会让人类…...

2023王道考研数据结构笔记第四章串
第四章 串 4.1 串的定义 4.1.1 串的相关概念 串:即字符串(String)是由零个或多个字符组成的有限序列。一般记为S‘a1a2…an’ (n>0) 其中S是串名,单引号(注:有的地方用双引号,如Java、C&am…...

【AI绘图学习笔记】深度学习相关数学原理总结(持续更新)
如题,这是一篇深度学习相关数学原理总结文,由于深度学习中涉及到较多的概率论知识(包括随机过程,信息论,概率与统计啥啥啥的),而笔者概率知识储备属实不行,因此特意开一章来总结(大部…...

CSGO服务器配置全贴纸插件方法教程
CSGO服务器配置全贴纸插件方法教程 关于插件的警告 一定要了解V社对于CSGO社区服务器的规定,全皮肤插件/全手套插件等,在设置了GSLT的情况下,是有可能被封禁GSLT账号的(所以慎用) 配置好服务器之后呢,想安…...

Python爬虫——使用socket模块进行图片下载
Python爬虫——使用socket模块进行图片下载什么是socket爬虫的工作流程socket爬取图片为什么能用socket能下载图片socket下载图片和request下载图片的区别使用socket下载一张图片使用socket下载多张图片方法1方法2什么是socket Socket 是一种通信机制,用于实现网络…...

通用游戏地图解决方案设计解析
前言: 在软件开发过程中,我们都希望能设计出一个稳健的,可维护的系统,为了实现这个目的,人们总结出了很多相关的设计原则,比如SOLID原则, KISS原则等等。SOLID每个字母代表了一种设计原则&…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...

铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
Java 8 Stream API 入门到实践详解
一、告别 for 循环! 传统痛点: Java 8 之前,集合操作离不开冗长的 for 循环和匿名类。例如,过滤列表中的偶数: List<Integer> list Arrays.asList(1, 2, 3, 4, 5); List<Integer> evens new ArrayList…...

使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...

自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...