JavaScript状态模式

JavaScript状态模式

  • 1 什么是状态模式
  • 2 使用状态模式改造电灯程序
  • 3 缺少抽象类的变通方式
  • 4 示例:文件上传
    • 4.1 场景描述
    • 4.2 代码过程

1 什么是状态模式

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

比如说这样一个场景:有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又将被打开。同一个开关按钮,在不同的状态下,表现出来的行为是不一样的。

我们用代码来描述上面的场景:

// 定义一个Light类
var Light = function () {
  this.state = "off"; // 给电灯设置初始状态 off
  this.button = null; // 电灯开关按钮
};

// 在页面中创建一个真实的button节点
Light.prototype.init = function () {
  var button = document.createElement("button"), // 创建一个开关按钮
    self = this;
  button.innerHTML = "开关";
  this.button = document.body.appendChild(button);
  // 开关被按下的事件
  this.button.onclick = function () {
    self.buttonWasPressed();
  };
};

// 开关被按下的行为
Light.prototype.buttonWasPressed = function () {
  // 如果当前是关灯状态,按下开关表示开灯
  if (this.state === "off") {
    console.log("开灯");
    this.state = "on";
  } else if (this.state === "on") {
    // 如果当前是开灯状态,按下开关表示关灯
    console.log("关灯");
    this.state = "off";
  }
};

var light = new Light();
light.init();

但是灯的种类是多种多样的,另外一种电灯,这种电灯也只有一个开关,但它的表现是:第一次按下打开弱光,第二次按下打开强光,第三次才是关闭电灯,现在我们改造上面的代码来完成这种新型电灯的制造:

Light.prototype.buttonWasPressed = function () {
  if (this.state === "off") {
    console.log("弱光");
    this.state = "weakLight";
  } else if (this.state === "weakLight") {
    console.log("强光");
    this.state = "strongLight";
  } else if (this.state === "strongLight") {
    console.log("关灯");
    this.state = "off";
  }
};

在上面的代码中,存在一些很明显的缺点:

  • buttonWasPressed方法违反开放—封闭原则,每次新增或者修改灯光的状态,都需要改动buttonWasPressed方法中的代码,这使其成为了一个非常不稳定的方法
  • 所有跟状态有关的行为,都被封装在buttonWasPressed方法里,如果这个电灯又增加了其他光的种类,那这个方法会越来越庞大
  • 状态的切换不明显,仅仅表现为改变state,容易漏掉某些状态
  • 状态之间的切换关系,是靠ifelse语句,增加或者修改一个状态可能需要改变若干个操作,这使代码难以阅读和维护

2 使用状态模式改造电灯程序

状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所以button被按下的的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。
在这里插入图片描述
同时我们还可以把状态的切换规则事先分布在状态类中, 这样就有效地消除了原本存在的
大量条件分支语句,代码如下:

// OffLightState:
var OffLightState = function (light) {
  this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
  console.log("弱光"); // offLightState 对应的行为
  this.light.setState(this.light.weakLightState); // 切换状态到 weakLightState
};

// WeakLightState:
var WeakLightState = function (light) {
  this.light = light;
};
WeakLightState.prototype.buttonWasPressed = function () {
  console.log("强光"); // weakLightState 对应的行为
  this.light.setState(this.light.strongLightState); // 切换状态到 strongLightState
};

// StrongLightState:
var StrongLightState = function (light) {
  this.light = light;
};
StrongLightState.prototype.buttonWasPressed = function () {
  console.log("关灯"); // strongLightState 对应的行为
  this.light.setState(this.light.offLightState); // 切换状态到 offLightState
};

// 改写Light类,在Light类中为每个状态类都创建一个状态对象,可以很明显的看到灯的种类
var Light = function () {
  this.offLightState = new OffLightState(this);
  this.weakLightState = new WeakLightState(this);
  this.strongLightState = new StrongLightState(this);
  this.button = null;
};

// 按下按钮的事件中,将请求委托给当前持有的状态对象去执行
Light.prototype.init = function () {
  var button = document.createElement("button"), // 创建button
    self = this;
  this.button = document.body.appendChild(button);
  this.button.innerHTML = "开关";
  // 设置当前状态
  this.currState = this.offLightState;
  this.button.onclick = function () {
    self.currState.buttonWasPressed();
  };
};

// 切换light对象的状态
Light.prototype.setState = function (newState) {
  this.currState = newState;
};

var light = new Light();
light.init();

3 缺少抽象类的变通方式

在上面的代码中,在状态类中将定义一些共同的行为方法,Context最终会将请求委托给状态对象的这些方法,在这个例子里这个方法就是buttonWasPressed。无论增加了多少种状态类,它们都必须实现buttonWasPressed方法。

所以使用状态模式的时候要格外小心,如果我们编写一个状态子类时,忘记了给这个状态子类实现buttonWasPressed方法,则会在状态切换的时候抛出异常,因为Context总是把请求委托给状态对象的buttonWasPressed方法。因此我们让抽象父类的抽象方法直接抛出一个异常:

var State = function () {};
State.prototype.buttonWasPressed = function () {
  throw new Error("父类的 buttonWasPressed 方法必须被重写");
};
var SuperStrongLightState = function (light) {
  this.light = light;
};
SuperStrongLightState.prototype = new State(); // 继承抽象父类
SuperStrongLightState.prototype.buttonWasPressed = function () {
  // 重写 buttonWasPressed 方法
  console.log("关灯");
  this.light.setState(this.light.offLightState);
};

4 示例:文件上传

4.1 场景描述

例如,控制文件上传需要两个节点按钮,第一个用于暂停和继续上传,第二个用于删除文件

  • 当文件在扫描状态中,不能进行任何操作,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的md5值判断,若确认该文件已经存在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳往上传失败状态。剩下的情况下才进入上传中状态
  • 上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传
  • 扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件

假设我们使用一个插件对象帮助我们完成上传工作:

var plugin = (function () {
  var plugin = document.createElement("embed");
  plugin.style.display = "none";
  plugin.type = "application/txftn-webkit";
  plugin.sign = function () {
    console.log("开始文件扫描");
  };
  plugin.pause = function () {
    console.log("暂停文件上传");
  };
  plugin.uploading = function () {
    console.log("开始文件上传");
  };
  plugin.del = function () {
    console.log("删除文件上传");
  };
  plugin.done = function () {
    console.log("文件上传完成");
  };
  document.body.appendChild(plugin);
  return plugin;
})();

上传是一个异步的过程,所以控件会不停地调用全局函数window.external.upload,来通知目前的上传进度,控件会把当前的文件状态作为参数state塞进window.external.upload,在此例中该函数负责打印一些log

window.external.upload = function (state) {
  console.log(state); // 可能为 sign、uploading、done、error
};

4.2 代码过程

首先定义Upload类,在构造函数中为每种状态子类都创建一个实例对象:

var Upload = function (fileName) {
  this.plugin = plugin;
  this.fileName = fileName;
  this.button1 = null;
  this.button2 = null;
  this.signState = new SignState(this); // 设置初始状态为 waiting
  this.uploadingState = new UploadingState(); // 上传中
  this.pauseState = new PauseState(this); // 暂停
  this.doneState = new DoneState(this); // 上传完成
  this.errorState = new ErrorState(this); // 上传错误
  this.currState = this.signState; // 设置当前状态
};

创建两个按钮,一个控制文件暂停和继续上传,一个用于删除文件:

Upload.prototype.init = function () {
  var that = this;
  this.dom = document.createElement("div");
  this.dom.innerHTML =
    "<span>文件名称:" +
    this.fileName +
    '</span><button data-action="button1">扫描中</button><button data-action="button2">删除</button>';
  document.body.appendChild(this.dom);
  this.button1 = this.dom.querySelector('[data-action="button1"]'); // 第一个按钮
  this.button2 = this.dom.querySelector('[data-action="button2"]'); // 第二个按钮
  this.bindEvent();
};

为两个按钮分别绑定点击事件,在点击了按钮之后,Context并不做任何具体的操作,而是把请求委托给当前的状态类来执行:

Upload.prototype.bindEvent = function () {
  var self = this;
  this.button1.onclick = function () {
    self.currState.clickHandler1();
  };
  this.button2.onclick = function () {
    self.currState.clickHandler2();
  };
};

// 扫描中
Upload.prototype.sign = function () {
  this.plugin.sign();
  this.currState = this.signState;
};
// 上传中
Upload.prototype.uploading = function () {
  this.button1.innerHTML = "正在上传,点击暂停";
  this.plugin.uploading();
  this.currState = this.uploadingState;
};
// 暂停
Upload.prototype.pause = function () {
  this.button1.innerHTML = "已暂停,点击继续上传";
  this.plugin.pause();
  this.currState = this.pauseState;
};
// 上传成功
Upload.prototype.done = function () {
  this.button1.innerHTML = "上传完成";
  this.plugin.done();
  this.currState = this.doneState;
};
// 上传失败
Upload.prototype.error = function () {
  this.button1.innerHTML = "上传失败";
  this.currState = this.errorState;
};
// 删除
Upload.prototype.del = function () {
  this.plugin.del();
  this.dom.parentNode.removeChild(this.dom);
};

再接下来是编写各个状态类的实现:

var StateFactory = (function () {
  var State = function () {};
  State.prototype.clickHandler1 = function () {
    throw new Error("子类必须重写父类的 clickHandler1 方法");
  };
  State.prototype.clickHandler2 = function () {
    throw new Error("子类必须重写父类的 clickHandler2 方法");
  };
  return function (param) {
    var F = function (uploadObj) {
      this.uploadObj = uploadObj;
    };
    F.prototype = new State();
    for (var i in param) {
      F.prototype[i] = param[i];
    }
    return F;
  };
})();

var SignState = StateFactory({
  clickHandler1: function () {
    console.log("扫描中,点击无效...");
  },
  clickHandler2: function () {
    console.log("文件正在上传中,不能删除");
  },
});

var UploadingState = StateFactory({
  clickHandler1: function () {
    this.uploadObj.pause();
  },
  clickHandler2: function () {
    console.log("文件正在上传中,不能删除");
  },
});

var PauseState = StateFactory({
  clickHandler1: function () {
    this.uploadObj.uploading();
  },
  clickHandler2: function () {
    this.uploadObj.del();
  },
});

var DoneState = StateFactory({
  clickHandler1: function () {
    console.log("文件已完成上传, 点击无效");
  },
  clickHandler2: function () {
    this.uploadObj.del();
  },
});

var ErrorState = StateFactory({
  clickHandler1: function () {
    console.log("文件上传失败, 点击无效");
  },
  clickHandler2: function () {
    this.uploadObj.del();
  },
});

测试一下:

var uploadObj = new Upload("AAAAAAAAA");
uploadObj.init();
window.external.upload = function (state) {
  // 插件调用 JavaScript 的方法
  uploadObj[state]();
};
window.external.upload("sign"); // 文件开始扫描
setTimeout(function () {
  window.external.upload("uploading"); // 1 秒后开始上传
}, 1000);
setTimeout(function () {
  window.external.upload("done"); // 5 秒后上传完成
}, 5000);

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

opencv入门到精通——图像阈值

目录 目标 简单阈值 自适应阈值 Otsu的二值化 Otsu的二值化如何实现&#xff1f; 目标 在本教程中&#xff0c;您将学习简单阈值&#xff0c;自适应阈值和Otsu阈值。 你将学习函数cv.threshold和cv.adaptiveThreshold。 简单阈值 在这里&#xff0c;问题直截了当。对于…

Blazor 混合开发_MAUI+Vue_WPF+Vue

Blazor 混合开发_MAUIVue_WPFVue 背景混合开发的核心为什么必须使用 wwwroot 文件夹放置 Web 项目文件 创建 MAUI 项目创建 wwwroot 文件夹服务注册创建 _import.razor添加 Main.razor 组件修改 MainPage.xaml 文件 创建 WPF 项目创建 wwwroot 文件夹服务注册创建 _import.razo…

基于电商场景的高并发RocketMQ实战-Broker高并发消息写入、读写队列原理分析

&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308; 【11来了】文章导读地址&#xff1a;点击查看文章导读&#xff01; &#x1f341;&#x1f341;&#x1f341;&#x1f341;&#x1f341;&#x1f341;&#x1f3…

HarmonyOS构建第一个JS应用(FA模型)

构建第一个JS应用&#xff08;FA模型&#xff09; 创建JS工程 若首次打开DevEco Studio&#xff0c;请点击Create Project创建工程。如果已经打开了一个工程&#xff0c;请在菜单栏选择File > New > Create Project来创建一个新工程。 选择Application应用开发&#xf…

谷粒商城-商品服务-新增商品功能开发(商品图片无法展示问题没有解决)

在网关配置路由 - id: member_routeuri: lb://gulimemberpredicates:- Path/api/gulimember/**filters:- RewritePath/api/(?<segment>.*),/$\{segment}并将所有逆向生成的工程调式出来 获取分类关联的品牌 例如&#xff1a;手机&#xff08;分类&#xff09;-> 品…

【零基础入门Docker】什么是Dockerfile Syntax

✍面向读者&#xff1a;所有人 ✍所属专栏&#xff1a;零基础入门Docker专栏https://blog.csdn.net/arthas777/category_12455882.html 目录 编写Dockerfile和Format的语法 2. MAINTAINER 3. RUN 4. ADD 6. ENTRYPOINT 7. CMD 8. EXPOSE 9. VOLUME 11. USER 12. ARG …

【设计模式】RBAC 模型详解

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、什么是 RBAC 呢&#xff1f; 二、RBAC 的组成 三、RBAC 的优缺点 3.1 优点&#xff1a; 3.2 缺点&#xff1a; 四、RBAC 的…

drools规则引擎介绍

1 什么是规则引擎 规则引擎&#xff0c;全称为业务规则管理系统&#xff0c;英文名为BRMS(即Business Rule Management System)。规则引擎的主要思想是将应用程序中的业务决策部分分离出来&#xff0c;并使用预定义的语义模块编写业务决策&#xff08;业务规则&#xff09;&…

C/C++ 共用体union的应用和struct不同

共用体union是一种数据格式&#xff0c;它能够存储不同的数据类型&#xff0c;但只能同时存储其中的一种类型。也就是说&#xff0c;结构体同时存储int、long和double,共用体只能春初int、long或double,共用体的语法与结构体相似&#xff0c;但含义不同。例如下面的声明&#x…

ZKP Mathematical Building Blocks (2)

MIT IAP 2023 Modern Zero Knowledge Cryptography课程笔记 Lecture 3: Mathematical Building Blocks (Yufei Zhao) Fiat Shamir heuristic Turn an interactive proof to a non-interactive proofP can simulate V whenever V picks a random valueP can simulate V’s ran…

线段树/区间树(java实现版详解附leetcode例题)

目录 什么是线段树 线段树基础表示 创建线段树&#xff08;Java版详解&#xff09; 线段树的区间查询 leetcode上的线段树相关问题 leetcode303题.区域和检索-数组不可变 使用线段树解题 不使用线段树解题 leetcode307题.区域和检索-数组可修改 不使用线段树解题 线…

利用PySpark进行商业洞察与可视化

利用PySpark进行商业洞察与可视化 引言数据集与技术栈数据集&#xff1a;YELP数据集技术栈&#xff1a;Flask、MySQL、Echarts、PySpark 分析维度与功能创新点与应用 引言 近年来&#xff0c;数据分析和可视化技术在商业决策中的应用越来越广泛。在这个信息爆炸的时代&#xf…

Leetcode算法系列| 4. 寻找两个正序数组的中位数

目录 1.题目2.题解C# 解法一&#xff1a;合并List根据长度找中位数C# 解法二&#xff1a;归并排序后根据长度找中位数C# 解法三&#xff1a;方法二的优化&#xff0c;不真实添加到listC# 解法四&#xff1a;第k小数C# 解法五&#xff1a;从中位数的概念定义入手 1.题目 给定两个…

5G边缘计算:解密边缘计算的魔力

引言 你是否曾想过&#xff0c;网络可以更贴心、更智能地为我们提供服务&#xff1f;5G边缘计算就像是网络的小助手&#xff0c;时刻待命在你身边&#xff0c;让数字生活变得更加便捷。 什么是5G边缘计算&#xff1f; 想象一下&#xff0c;边缘计算就像是在离你最近的一层“云…

案例149:基于微信小程序的家庭财务管理系统的设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

Linux bridge开启hairpin模拟测试macvlan vepa模式

看到网上介绍可以通过Linux bridge 开启hairpin方式测试macvlan vepa模式&#xff0c;但是没有找到详细资料。我尝试测试总提示错误信息&#xff0c;无法实现&#xff0c;经过几天的研究&#xff0c;我总算实现模拟测试&#xff0c;记录如下&#xff1a; 参考 1.Linux Macvla…

展望2023年CSDN博客之星评选

目录 1 前言2 博客的意义3 人工智能对博客的影响4 AI 技术下的成长与分享5 技术的探索6 博客之星评选对于技术人的激励作用7 结语 1 前言 当我们回顾过去&#xff0c;博客不仅仅是一种记录生活、分享经验的方式&#xff0c;更是一个见证自我成长与进步的平台。站在2023年度 CS…

计算机是如何工作的(下)

4. 编程语言&#xff08;Program Language&#xff09; 本块内容主要是还原下我们已经熟悉的编程语言&#xff0c;即编程语言是如何和 CPU 指令对应起来的。 4.1 程序&#xff08;Program&#xff09; 所谓程序&#xff0c;就是一组指令以及这组指令要处理的数据。狭义上来说&…

【数据结构入门精讲 | 第十四篇】散列表知识点及考研408、企业面试练习(1)

在上一篇中我们进行了树的专项练习&#xff0c;在这一篇中我们将进行散列表知识点的学习。 目录 概念伪代码线性探测法平方探测法查找成功的平均查找长度查找失败的平均查找长度判断题选择题 概念 散列表&#xff08;Hash Table&#xff09;&#xff0c;也被称为哈希表或散列映…

向量投影:如何将一个向量投影到矩阵的行向量生成子空间?

向量投影&#xff1a;如何将一个向量投影到矩阵的行向量生成子空间&#xff1f; 前言 本问题是在学习Rosen梯度投影优化方法的时候遇到的问题&#xff0c;主要是对于正交投影矩阵(NT(NNT)-1N)的不理解&#xff0c;因此经过查阅资料&#xff0c;学习了关于向量投影的知识&…