设计模式-策略模式

news2024/11/20 16:36:49

前言

作为一名合格的前端开发工程师,全面的掌握面向对象的设计思想非常重要,而“设计模式”是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的,代表了面向对象设计思想的最佳实践。正如《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 类的 duckfly 方法,其中 fly 方法里面什么也不做。

class RubberDuck extends Duck {
  quack() {
    console.log('zhi zhi');
  }
  fly() {

  }
}

继承可能并不是最优解

设计者仔细思考了上述设计,提出了一个问题:如果后续新增了更多类型的鸭子,有的鸭子既不会飞又不会叫怎么办呢?难道还是继续覆写 fly 或者 quack 方法吗?

显然,集成不是最优解。

经过一番思索,设计者想到了通过接口来优化设计。

设计两个接口,分别是 FlableQuackable 接口。

interface Flyable {
  fly(): void;
}

interface Quackable {
  quack(): void;
}

这样,只有实现了 Flyable 的鸭子才能飞行,实现了 Quackable 的鸭子才能说话。

class MallardDuck implements Flayable, Quackable {
  fly() {

  }
  quack() {

  }
}

通过接口虽然可以限制鸭子的行为,但是每个鸭子都要检查一下是否需要实现对应的接口,鸭子类型多起来之后是非常容易出错的,同时,通过接口的方式虽然限制了鸭子的行为,但是代码量却没有减少,每个鸭子内部都要重复实现fly和quack的代码逻辑。

分开变化和不变化的部分

下面开始介绍我们的第一个设计原则:

找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

Duck 类中,quackfly 是会随着鸭子的不同而改变的,而 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 都能进行的一种操作。

现在我们接着之前的思路,将鸭子的 flyquack 两个行为变为两个接口 FlyBehaviorQuackBehavior。所有的鸭子不直接实现这两个接口,而是有专门的行为类实现这两个接口。

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();
  }
}

这样做有个好处:

  1. 鸭子的行为可以被复用,因为这些行为已经与鸭子本身无关了。
  2. 我们可以新增一些行为,不会担心影响到既有的行为类,也不会影响有使用到飞行行为的鸭子类。

整合鸭子的行为

现在鸭子的所有的行为需要被整合在一起,需要委托给别人处理。

继续改造 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 !');
  }
}

在鸭子类内部定义两个变量,类型分别为 FlyBehaviorQuackBehavior 的接口类型,声明为接口类型方便后续通过多态的方式设置鸭子的行为。移除鸭子类中的 flyquack 方法,因为这两个方法已经被分离到 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 可以动态的改变鸭子的行为,是鸭子具备坐火箭飞行的能力。

多用组合,少用集成

从上面的例子就可以看出,每一个鸭子都有一个 FlyBehaviorQuackBehavior,让鸭子(Duck 类)将飞行和呱呱叫委托它代为处理。

当你将两个类结合起来使用,这就是组合(composition)。这种做法和继承不同的地方在于,鸭子的行为不是继承而来,而是和使用的行为对象组合而来的。也就是我们要介绍的第三个设计原则:

多用组合,少用继承

策略模式的设计步骤

  1. 定义一个接口,接口中声明各个算法所共有的操作
interface Strategy {
  execute(): void;
} 
  1. 定义一系列的策略,并且在遵循 Strategy 接口的基础上实现算法
class StrategyA implements Strategy {
  execute() {
    console.log('I am StrategyA'); // 算法 A
  }
}

class StrategyB implements Strategy {
  execute() {
    console.log('I am StrategyB'); // 算法 B
  }
}
  1. 定义一个上下文 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;
  }
}
  1. 创建客户端类,客户端代码会根据条件来选择具体的策略。
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();

策略模式结构图:
在这里插入图片描述

策略模式的使用场景

当我们在设计代码的时候,想使用对象中各种不同的算法变体,并希望能在运行时切换算法时, 可以考虑使用策略模式。

参考

源代码地址

  1. 针对接口编程,而不是针对实现编程
  2. 策略模式
  3. 《HeadFirst设计模式》

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

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

相关文章

【Verilog】——模块,常量,变量

目录 1.模块 1.描述电路的逻辑功能 2. 门级描述 3.模块的模板​编辑 2.关键字 3.标识符 4.Verilog源代码的编写标准 5.数据类型 1.整数常量​ 2.参数传递的两种方法 3.变量 4.reg和wire的区别 5.沿触发和电平触发的区别​ 6.memory型变脸和reg型变量的区别​ 1.模块 1.描…

Mybatis一级缓存与二级缓存

一、MyBatis 缓存缓存就是内存中的数据,常常来自对数据库查询结果的保存。使用缓存,我们可以避免频繁与数据库进行交互,从而提高响应速度。MyBatis 也提供了对缓存的支持,分为一级缓存和二级缓存,来看下下面这张图&…

docker安装即docker连接mysql(window)

一 安装docker 1.什么是docker Docker容器与虚拟机类似,但二者在原理上不同。容器是将操作系统层虚拟化,虚拟机则是虚拟化硬件,因此容器更具有便携性、高效地利用服务器。 2.WSL2 WSL,即Windows Subsystem on Linux,中…

JavaScript高级 XHR - Fetch

1. 前端数据请求方式 早期的网页都是通过后端渲染来完成的:服务器端渲染(SSR,server side render) 客户端发出请求 -> 服务端接收请求并返回相应HTML文档 -> 页面刷新,客户端加载新的HTML文档 当用户点击页面中…

C++:哈希:闭散列哈希表

哈希的概念 哈希表就是通过哈希映射,让key值与存储位置建立关联。比如,一堆整型{3,5,7,8,2,4}在哈希表的存储位置如图所示: 插入数据的操作: 在插入数据的时候,计算数据相应的位置并进行插入。 查找数据的操作&…

从企业数字化发展的四个阶段,看数字化创新战略

《Edge: Value-Driven Digital Transformation》一书根据信息技术与企业业务发展的关系把企业的数字化分为了四个阶段: 技术与业务无关技术作为服务提供者开始合作科技引领差异化优势以技术为业务核心 下图展示了这四个阶段的特点: 通过了解和分析各个…

[ant-design-vue] tree 组件功能使用

[ant-design-vue] tree 组件功能使用描述环境信息相关代码参数说明描述 是希望展现一个树形的菜单,并且对应的菜单前有复选框功能,但是对比官网的例子,我们在使用的过程中涉及到对半选中情况的处理: 半选中状态: 选中…

NodeJS安装

一、简介Node.js是一个让JavaScript运行在服务端的开发平台,Node.js不是一种独立的语言,简单的说 Node.js 就是运行在服务端的 JavaScript。npm其实是Node.js的包管理工具(package manager),类似与 maven。二、安装步骤…

并发下的可见性、原子性、有序性还不懂?

CPU、内存、I/O速度大比拼CPU的读写速度是内存的100倍左右,而内存的读写速度又是I/O的10倍左右。根据"木桶理论",速度取决于最慢的I/O。为了解决速度不匹配的问题,通常在CPU和主内存间增加了缓存,内存和I/O之间增加了操…

C语言学习之路--操作符篇,从知识到实战

目录一、前言二、操作符分类三、算术操作符四、移位操作符1、左移操作符2、右移操作符五、位操作符拓展1、不能创建临时变量(第三个变量),实现两个数的交换。2、编写代码实现:求一个整数存储在内存中的二进制中1的个数。六、赋值操…

http客户端Feign

Feign替代RestTemplate RestTemplate方式调用存在的缺陷 String url"http://userservice/user/"order.getUserId();User user restTemplate.getForObject(url, User.class); 代码可读性差,变成体验不统一; 参数复杂的时候URL难以维护。 &l…

Gem5模拟器,一些运行的小tips(十一)

一些基础知识,下面提到的东西与前面的文章有一定的关系,感兴趣的小伙伴可以看一下: (21条消息) Gem5模拟器,全流程运行Chiplet-Gem5-SharedMemory-main(十)_好啊啊啊啊的博客-CSDN博客 Gem5模拟器&#xf…

深度学习|改进两阶段鲁棒优化算法i-ccg

目录 1 主要内容 2 改进算法 2.1 CC&G算法的优势 2.2 i-CCG算法简介 3 结果对比 1 主要内容 自从2013年的求解两阶段鲁棒优化模型的列和约束生成算法(CC&G)被提出之后,基本没有实质性的创新,都是围绕该算法在各个领…

静态路由复习实验

实验分析: 1 .R6为isp,接口IP地址均为公有有地址;该设备只能配置IP地址, 之后不能再对其进行任何配置; r6只能配置IP, 所以r1--r5上需要配置指向r6的缺省路由; 2 .R1—R5为局域网,私有P地址192.168.1.6/24,请合理分配; 图中骨干…

来说说winform和wpf异同,WPF对于新人上手容易吗?

这么问,可能还真不是很好回答,但WPF的特点决定了,他对于前端人员更容易上手。 首先,我们假定你已经安装了Visual studio 2017以上的版本(如果你的VS打开没有WPF那就说明你没有安装.net桌面开发这项)&#x…

【2023unity游戏制作-mango的冒险】-前六章API,细节,BUG总结小结

👨‍💻个人主页:元宇宙-秩沅 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 本文由 秩沅 原创 收录于专栏:unity游戏制作 ⭐mango的冒险前六章总结⭐ 文章目录⭐mango的冒险前六章总结⭐👨‍&a…

Eureka - 总览

文章目录前言架构注册中心 Eureka Server服务提供者 Eureka Client服务消费者 Eureka Client总结资源前言 微服务(Microservices,一种软件架构风格)核心的组件包括注册中心,随着微服务的发展,出现了很多注册中心的解决…

【项目精选】 塞北村镇旅游网站设计(视频+论文+源码)

点击下载源码 摘要 城市旅游产业的日新月异影响着村镇旅游产业的发展变化。网络、电子科技的迅猛前进同样牵动着旅游产业的快速成长。随着人们消费理念的不断发展变化,越来越多的人开始注意精神文明的追求,而不仅仅只是在意物质消费的提高。塞北村镇旅游…

Android事件分发机制

文章目录Android View事件分发机制:事件分发中的核心方法onTouchListener和onClickListener的优先级事件分发DOWN,MOVE,UP 事件分发CANCEL代码实践requestdisallowIntereptTouchEvent作用Android View事件分发机制: 事件分发中的核心方法 Android中事件…

一文让你彻底理解Linux内核多线程(互斥锁、条件变量、读写锁、自旋锁、信号量)

一、互斥锁(同步) 在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在…