命令模式中一般包含三个角色: 命令调用者 -> 中间对象 -> 命令执行者
- 快到中午了,小明在公司点了一份蛋炒饭外卖,小明只需要通过电话向饭店下订单即可,无需知道是饭店的哪位厨师做这顿饭,此处小明是命令调用者
- 饭店前台收到蛋炒饭的订单后 告诉厨师做一份蛋炒饭,此处饭店前台是中间对象
- 前台告诉厨师有一个蛋炒饭的订单,厨师开始做蛋炒饭,无需知道是小明还是小红下的订单, 此处厨师相当于命令的执行者
为什么使用命令模式
将 命令的调用者 和 命令的执行者 借助 命令中间对象 进行解耦,命令的调用者无需知道命令的执行者内部是如何处理的,只管调用相应的命令即可,命令的执行者也无需知道是谁调用的命令
命令模式常见的使用场景:
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的具体操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
案例1:使用命令模式设计一个菜单程序
假设现在有一个编写用户界面的程序,该用户页面上有数十个 button 按钮,因为项目比较大,开发主管决定让某个程序员同学绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。对于绘制按钮的程序员来说,他完全不知道某个按钮未来用来做什么,他只知道点击这个按钮会发生某些事情。
针对这个需求来说,此处采用命令模式再适合不过了。
下面进入代码编写阶段,首先在页面中完成这些按钮的绘制:
<body>
<div>
<button id="btn1">刷新菜单</button>
<button id="btn2">添加子菜单</button>
<button id="btn3">删除子菜单</button>
</div>
<script>
function getDomById(id) {
return document.getElementById(id);
}
// 命令调用者
const btn1 = getDomById("btn1"), btn2 = getDomById('btn2'), btn3 = getDomById('btn3');
</script>
</body>
接下来定义 setCommand 函数, setCommand 函数负责往按钮上安装命令。可以肯定的是,点击按钮会执行某个 command 命令,执行命令的动作被约定为调用 command 对象的 execute() 方法。虽然还不知道这些命令代表什么操作,但负责绘制按钮的程序员不关心这些事情,他只需要预留好安装这些命令的接口, command 对象自然知道如何和正确的对象沟通。
function setComment(btn, comment) {
btn.addEventListener('click', function () {
comment.execute();
})
}
然后,负责编写点击按钮之后的具体行为的程序员总算交上了他们的成果,他们完成了刷新菜单界面,增加子菜单和删除子菜单这几个功能,这几个功能被分布在 MenuBar 和 subMenu 这两个对象中。
// 命令执行者
const MenuBar = {
refresh: function () {
console.log("刷新菜单目录");
}
}
const SubMenu = {
add: function () {
console.log("增加子菜单");
},
del: function () {
console.log("删除子菜单");
}
}
在让 button 变得有用起来之前,我们要先把这些行为都封装在命令对象中。对命令对象的封装,我们可以借用js函数的闭包特性来实现。
// 使用闭包实现命令中间对象的创建
function RefreshMenuBarComment(receiver) {
return {
execute: function () {
receiver.refresh();
}
}
}
function AddSubMenuComment(receiver) {
return {
execute: function () {
receiver.add();
}
}
}
function DelSubMenuComment(receiver) {
return {
execute: function () {
receiver.del();
}
}
}
最后就是把命令接收者传入到 command 对象创建函数中,并把 command 对象安装到 button 上面。
setComment(btn1, RefreshMenuBarComment(MenuBar));
setComment(btn2, AddSubMenuComment(SubMenu));
setComment(btn3, DelSubMenuComment(SubMenu));
以上只是一个简单的命令模式示例,从这个示例中,我们可以知道命令调用者和命令执行者是如何解耦开的。
命令模式的作用不仅仅是封装运算块,而且可以很方便的给命令对象增加撤销操作。就像订餐时可以通过电话来取消订单一样。
案例二:使用命令模式实现一个动画,这个动画让页面上的小球移动到水平方向的某个位置,也可以通过撤销按钮撤销小球上一次的移动。
现在页面上有一个input输入框和两个button按钮,文本框中可以输入一些数字,表示小球移动后的水平位置,小球在用户点击开始移动按钮后立刻开始移动,点击撤销移动按钮后可以撤销上一次的移动,回到移动前的位置。
先在页面上实现小球和按钮、输入框等元素
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>命令模式2</title>
<style>
html, body, div, button, input {
margin: 0;
padding: 0;
}
.ball {
position: absolute;
top: 50px;
background: #000;
width: 50px;
height: 50px;
border-radius: 50%;
}
</style>
</head>
<body>
<div id="ball" class="ball"></div>
输入小球移动后的位置:<input id="pos" />
<button id="moveBtn">开始移动</button>
<button id="undo">撤销移动</button>
</body>
</html>
接下来实现一个动画的构造函数
var tween = {
linear: function (t, b, c, d) {
return c * t / d + b;
},
easeIn: function (t, b, c, d) {
return c * (t /= d) * t + b;
},
strongEaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t * t * t + b;
},
strongEaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
},
sineaseIn: function (t, b, c, d) {
return c * (t /= d) * t * t + b;
},
sineaseOut: function (t, b, c, d) {
return c * ((t = t / d - 1) * t * t + 1) + b;
}
}
class Animate {
constructor(dom) {
this.dom = dom;
this.startTime = 0;
this.startPos = 0;
this.endPos = 0;
this.propertyName = null;
this.easing = null;
this.duretion = null;
}
start = (propertyName, endPos, duration, easing) => {
this.startTime = +new Date();
this.startPos = this.dom.getBoundingClientRect()[propertyName];
this.propertyName = propertyName;
this.endPos = endPos;
this.duration = duration;
this.easing = tween[easing];
let self = this;
let timeId = setInterval(function () {
if (self.step() === false) {
clearInterval(timeId);
}
}, 19)
}
step = () => {
let t = +new Date();
if (t > this.startTime + this.duration) {
this.update(this.endPos);
return false;
}
let pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
this.update(pos);
}
update = pos => {
this.dom.style[this.propertyName] = pos + 'px';
}
}
接下来开始定义命令的调用者、命令的中间对象、命令的具体执行者
<script>
function getDomById(id) {
return document.getElementById(id);
}
const ball = getDomById("ball"), pos = getDomById("pos");
// 命令的调用者
const moveBtn = getDomById("moveBtn"), undo = getDomById("undo");
let prePos; // 用来保存上一次小球移动的位置
// 命令的具体执行者 或者称为命令的接收者
const animate = new Animate(ball);
// 创建命令中间对象
const MoveCommand = function (receiver) {
return {
posStack: [],
execute: function (pos) {
receiver.start('left', pos, 1000, 'strongEaseOut');
this.posStack.push(receiver.dom.getBoundingClientRect()[receiver.propertyName]);
},
unExecute: function () {
if (this.posStack.length) {
prePos = this.posStack.pop();
receiver.start('left', prePos, 1000, 'strongEaseOut');
pos.value = prePos;
} else {
console.log('已回退到最开始步骤');
}
},
}
}
const moveCommand = MoveCommand(animate); // moveCommand 是创建的命令中间对象
// 命令的调用者通过命令中间对象与命令的具体执行者进行解耦
moveBtn.addEventListener('click', function () {
moveCommand.execute(pos.value);
})
undo.addEventListener('click', function () {
moveCommand.unExecute();
})
</script>
小球在进行开始移动和撤销移动操作时,无需知道移动和撤销移动的具体行为操作,仅需借助命令中间对象调用相应的命令即可,从而实现命令的调用者 与命令的执行者解耦的目的。
以前看书,总是看过后当时记得,过了一段时间后就忘的差不多了,总结下原因,是因为看过之后没有及时做总结,这次阅读曾探大佬的《JavaScript设计模式与开发实践》,准备边看边做总结,既可以加深记忆与理解,也方便以后复习。
由于本人技术水平有限,部分内容可能描述的不清楚或存在错误,欢迎指出,感激不尽。
参考
《JavaScript设计模式与开发实践》 --曾探