白雪公主和七种行为模式——设计模式简介
欢迎阅读我的“设计模式童话”系列的第三篇!本系列旨在以一种更通俗易懂的方式——也就是童话般的隐喻——来解释设计模式。
上一篇文章介绍了行为模式的前半部分,这些模式允许多个对象协同完成复杂的任务。这篇文章将介绍这组模式的后半部分,并使用简单的例子,以另一个经典故事《白雪公主和七个小矮人》为背景进行解释。
这个系列我已经介绍过两次了,这里就不多说了。我们直接开始吧。
用侦察兵警告矮人
让我们把白雪公主的故事聚焦在小矮人和他们的采矿工作上。所以,让我们以七个小矮人开始一天工作的方式开始:其中一个小矮人向其他小矮人喊一声“嗨嗬”,让他们知道该开始工作了。在电影里,博士是第一个喊出这句话的人,所以我们在这里也照做。
Doc 需要一种能够大声呼喊,并且确保其他矮人能够听到并回应的方式,所以他决定使用观察者模式。首先,他为自己编写了一个类,作为发出呼喊的对象。他从一个简单的方法开始,创建一个矮人列表,用于向其发出呼喊。
class Caller {
constructor() {
this.dwarves = [];
this.yell = null;
}
register(dwarf) {
this.dwarves.push(dwarf);
}
}
最重要的属性是this.dwarves
,它是他正在呼叫的实际矮人的列表。它最初是空的,所以他添加了一个register
方法,可以快速添加额外的矮人。现在,列表上的每个矮人基本上都在“监听”呼叫者的操作。
一旦他有了添加矮人的方法,他需要一种方法来更新所有矮人。所以当他发出呼叫时,他需要找到每个矮人,并向他们提供所需的信息。
class Caller {
constructor() {
this.dwarves = [];
this.yell = null;
}
register(dwarf) {
this.dwarves.push(dwarf);
}
updateAll() {
return this.dwarves.forEach(el => el.update(this));
}
callOut(yell) {
this.yell = yell;
this.updateAll();
}
}
这个updateAll
函数会接收每个正在听的小矮人的信息,并说“呼叫者在喊叫,所以你也需要喊叫!”每个小矮人都会被传递一个Caller
,它会告诉他们需要喊什么。
我们可以看到矮人在DwarfObserver
课堂上会如何反应。
class DwarfObserver {
constructor() {
this.yell = null;
}
update(caller) {
this.yell = caller.yell;
}
}
每当一个矮人“更新”了,我们就会让他们跟着博士喊。换句话说,当他们听到博士喊的时候,他们也应该喊同样的话。这就是观察者模式的核心,一个对象观察另一个对象的变化,并在变化发生时立即采取特定的行动。
让我们通过将 Doc 定义为呼叫者,并定义另外四个矮人听他说话来建立这个具体的例子。
const Doc = new Caller();
const Happy = new DwarfObserver(),
Bashful = new DwarfObserver(),
Sneezy = new DwarfObserver(),
Grumpy = new DwarfObserver();
我们会向 Doc 登记每个矮人的情况,这样他们就会开始听他的喊叫。
const dwarves = [Happy, Bashful, Sneezy, Grumpy]
dwarves.forEach(dwarf => Doc.register(dwarf));
现在,当我们告诉 Doc 大声喊叫时,他会自动更新其他矮人,以便他们也大声喊叫!
Doc.callOut('Heigh Ho!');
Doc.yell;
// 'Heigh Ho!'
Happy.yell;
// 'Heigh Ho!'
Bashful.yell;
// 'Heigh Ho!'
Sneezy.yell;
// 'Heigh-Ho!'
Grumpy.yell;
// 'Heigh Ho!'
使用迭代器跨越桥梁
现在矮人们都去干活了,但他们还没到矿井。假设现在是清晨,他们需要穿过一座窄桥才能到达矿井。但天色太暗,他们几乎看不见其他矮人,而且他们又不敢冒险让多个矮人同时过桥。
Doc 发现他们需要一个谨慎的方法,让每个矮人都能过河,并且能够追踪有多少矮人需要过河。于是他编写了一个 Iterator 类来实现这个功能。
class DwarfIterator {
constructor(dwarves) {
this.index = 0;
this.dwarves = dwarves; // is an array
}
hasNext() {
return this.index < this.dwarves.length;
}
}
目前这个类的功能不多。它有一个矮人列表,索引为零,所以它位于列表的开头,还有一个函数来判断它们是否位于列表的末尾。这很有用,但不足以让它们过桥。Doc 需要它来追踪下一个过桥的人,并向他们发出通知。
所以他又添加了两个方法。nextDwarf
一个会告诉当前的矮人穿过列表,并进一步增加索引。他还补充说callNextDwarf
,在调用下一个矮人之前,会检查下一个矮人是否还在。
class DwarfIterator {
constructor(dwarves) {
this.index = 0;
this.dwarves = dwarves;
}
hasNext() {
return this.index < this.dwarves.length;
}
nextDwarf() {
return `${this.dwarves[this.index++]}, you may cross!`;
}
callNextDwarf() {
return this.hasNext() ? this.nextDwarf() : "Everyone is here!";
}
}
通过此设置,Doc 只需传入矮人列表即可反复使用callNextDwarf
。迭代器 (Iterator) 让它们能够轻松地浏览矮人列表,从而抽象出追踪矮人位置和执行操作的工作。
让我们设置矮人列表(我们可以使用字符串数组而不是对象来节省时间),并将它们传递到迭代器中。
const crossingDwarfs = [
"Sneezy",
"Sleepy",
"Happy",
"Doc",
"Grumpy",
"Dopey",
"Bashful"
];
const dwarfCounter = new DwarfIterator(crossingDwarfs);
现在,Doc 可以使用迭代器来追踪下一个应该被叫到谁,以及他们什么时候全部完成。他只需不断喊出迭代器指示的内容,直到他们完成为止。
dwarfCounter.callNextDwarf();
// Sneezy, you may cross!
dwarfCounter.callNextDwarf();
// Sleepy, you may cross!
dwarfCounter.callNextDwarf();
// Happy, you may cross!
dwarfCounter.callNextDwarf();
// Doc, you may cross!
dwarfCounter.callNextDwarf();
// Grumpy, you may cross!
dwarfCounter.callNextDwarf();
// Dopey, you may cross!
dwarfCounter.callNextDwarf();
// Bashful, you may cross!
dwarfCounter.callNextDwarf();
// Everyone is here!
通过调解员管理每个人的工作
矮人们已经进矿了,准备开始工作。矿区有很多区域需要开采,每个矮人都会去不同的区域。在各个区域之间移动需要花费大量时间,而且他们不希望一个区域同时有超过一个矮人,因为他们想要各种各样的石头。
这很难管理,因为如果一个矮人想换到其他区域,他们不知道那里是否已经有人了。Doc 发现了这个问题,作为一个擅长解决问题的矮人,他编写了一个类,让自己成为一名调解员。
Caller
Mediator 类与Observer 类类似。它有一个矮人列表,但它不会向所有矮人发出变化警报,而是利用这些矮人的信息来更好地协调和指导他们的行动。
class DwarfMediator {
constructor() {
this.dwarves = [];
}
checkSection(section) {
return this.dwarves.every(dwarf => dwarf.section !== section);
}
}
这里,协调不同的矮人采用 的形式checkSection
。它会检查该区域内是否已有其他矮人,并分别返回true
或false
。这可以防止他所协调的所有矮人的区域重叠。
Doc 还为他调解的矮人编写了一个类。每个矮人都需要一个调解员,所以他确保每当一个工人被创建时,它都会将自己添加到调解员的工人列表中。
class DwarfWorker {
constructor(section, mediator) {
this.section = section;
this.mediator = mediator;
this.mediator.dwarves.push(this);
}
}
现在,Worker 可以参考 Mediator 及其关于其他矮人的信息。它可以请求 Mediator 检查某个特定区域是否可用askToMove
,如果可用,Worker 就会移动到该区域。
class DwarfWorker {
constructor(section, mediator) {
this.section = section;
this.mediator = mediator;
this.mediator.dwarves.push(this);
}
askToMove(section) {
const available = this.mediator.checkSection(section);
if (available) {
this.section = section;
}
}
}
这很好,因为该类与 Mediator 的耦合度较低。Mediator 专注于组织每个 Worker(其所属部分)提供的信息,并尽可能地将操作留给 Worker(例如,迁移到新的部分)。
我们可以看到这在实际操作中有多有用。Doc 创建了一个实例,将自己设置为 Mediator,将其他矮人设置为 worker。
const MediatorDoc = new DwarfMediator();
const Sneezy = new DwarfWorker("Diamonds", MediatorDoc),
Sleepy = new DwarfWorker("Rubies", MediatorDoc),
Happy = new DwarfWorker("Sapphires", MediatorDoc),
Grumpy = new DwarfWorker("Emeralds", MediatorDoc),
Dopey = new DwarfWorker("Gems", MediatorDoc),
Bashful = new DwarfWorker("Crystals", MediatorDoc);
假设 Sneezy 想去“Rubies”区。Doc 看到 Sleepy 已经在那里了,就会阻止他。
Sneezy.askToMove("Rubies");
// Sneezy.position is still "Rubies!"
喷嚏精明白了,待在原地,转而询问“珍珠”区的情况。医生看到那里是空的,就让他过去了。
Sneezy.askToMove("Pearls");
// Sneezy.position has changed to "Pearls!"
这样,我们就可以让多个对象以有组织的方式一起工作,从而避免紧密耦合。
使用纪念品追踪挖矿进度
矮人们继续工作,意识到最近珠宝和钻石失踪了,他们需要更好地记录找到了多少。Sneezy 想要记录他们每小时挖出的数量,这样如果他们发现有些宝石不见了,就可以每小时查看有多少,看看它们是什么时候消失的。
Sneezy 决定为此设置一个 Memento 模式。这样他就能追踪变化,并检查之前的状态计数。
Sneezy 写的 Memento 图案分为三个部分。首先是 Memento 本身,它是收集到的石头的“快照”。他每小时都会制作一张新的。
class Memento {
constructor(jewels, diamonds) {
this.jewels = jewels;
this.diamonds = diamonds;
}
}
他还需要一个originator
,这是一个额外的抽象层,可以直接处理 Memento。在这种情况下,Sleepy 只需要发起者创建新的 Memento 并从旧 Memento 中提取数据。
const originator = {
store: function(jewels, diamonds) {
return new Memento(jewels, diamonds);
},
restore: function(memento) {
return {
jewels: memento.jewels,
diamonds: memento.diamonds,
};
}
};
此模式的最后一部分是caretaker
,用于跟踪大型组中的备忘录。它提供了添加和检索备忘录的方法,但它与备忘录类本身没有直接联系——这留给了发起者。管理者使用一些基本函数来存储和管理它们。
class CaretakerDwarf {
constructor() {
this.mementos = [];
}
addMemento(memento) {
this.mementos.push(memento);
}
getMemento(index) {
return this.mementos[index];
}
getMementoFromHour(hour) {
const hourIndex = hour - 1;
return this.getMemento(hourIndex)
}
}
现在,Sneezy 已经准备好开始追踪 Mementos 了。首先,他指定自己为管理员。
const Sneezy = new CaretakerDwarf();
现在他可以使用发起者添加新的纪念品来每小时追踪珠宝和钻石。
Sneezy.addMemento(originator.store(2, 4));
Sneezy.addMemento(originator.store(3, 6));
Sneezy.addMemento(originator.store(0, 2));
如果 Sneezy 需要检查第二个小时收集了多少数据,他可以快速恢复该小时的“状态”。该状态存储在一个变量中,他可以将其与其他变量进行比较,或者执行其他操作。
const secondHourResults = originator.restore(Sneezy.getMementoFromHour(2));
secondHourResults.jewels; // 3
secondHourResults.diamonds; // 6
通过责任链加载珠宝
一天的工作快结束了,矮人们得开始把石头装上车了。哈皮很高兴能完成这件事,但他意识到还有很多事情需要考虑。
- 每辆推车只能装载有限数量的石头。矮人不能有剩余,推车也不能装满。
- 根据每个矮人的位置,手推车可以走多条不同的路径。
- 一些矮人可能仍在采矿,需要继续传递矿车。
Happy 唯一能看到的常数是手推车在矮人之间移动,但每个矮人的具体动作可能有所不同。这个谜题的核心是如何沿着矮人链传递手推车对象,他意识到责任链模式可以解决这个问题。
首先,他为采矿车创建一个类。每个采矿车必须分配一个宝石限额和一个简单的指定矮人列表。
class MiningCart {
constructor(limit) {
this.limit = limit;
this.jewels = 0;
this.dwarves = [];
}
setNextDwarf(dwarf) {
this.dwarves.push(dwarf);
}
addJewels(jewels) {
this.jewels += jewels;
}
}
Happy 还添加了一些额外的方法,因此更容易确定购物车是否有足够的空间来添加更多宝石。
class MiningCart {
constructor(limit) {
this.limit = limit;
this.jewels = 0;
this.dwarves = [];
}
setNextDwarf(dwarf) {
this.dwarves.push(dwarf);
}
addJewels(jewels) {
this.jewels += jewels;
}
getAvailableSpace() {
return this.limit - this.jewels;
}
hasEnoughSpace(jewels) {
return this.getAvailableSpace() - jewels >= 0;
}
}
Happy 还需要为每个获得手推车的矮人设置一个等级,以追踪他们是否正在采矿以及他们拥有的宝石。
class Dwarf {
constructor(jewels, isMining) {
this.jewels = jewels;
this.isMining = isMining;
}
}
最后,也是最重要的一点,Happy 编写了一个类,用于将矿车沿着责任链送下去。它会将矿车送去列表中的每个矮人,在尝试将他们的宝石添加到矿车之前,会检查他们的采矿状态。更新后的矿车会被传递给下一个矮人,依此类推,直到完成整个链条。这使得矿车和矮人能够协同工作,以正确的顺序收集宝石并执行其他操作,而不会将它们连接得太紧密。最终,装满了尽可能多的宝石的矿车会被交还给 Happy。
class CartChainOfResp {
calc(cart) {
cart.dwarves.forEach(dwarf => {
const cartHasSpace = cart.hasEnoughSpace(dwarf.jewels),
dwarfHasFinishedMining = !dwarf.isMining;
if (dwarfHasFinishedMining && cartHasSpace) {
cart.addJewels(dwarf.jewels);
}
});
return cart;
}
}
让我们来看看这个模式的实际应用。哈皮看到矿井里有三个矮人,他也可以把矿车交给他们。矿车的宝石限额是 100 颗,所以哈皮记下了其他矮人的信息,并把他们分配到矿车上。
const miningCart = new MiningCart(100),
Sneezy = new Dwarf(50, false),
Doc = new Dwarf(25, true),
Dopey = new Dwarf(50, false);
miningCart.setNextDwarf(Sneezy);
miningCart.setNextDwarf(Happy);
miningCart.setNextDwarf(Dopey);
设置好矿车及其路径后,它就可以沿着链条向上移动了。他创建了一个责任链实例,并将矿车传递给它,让它沿着链条向上移动。
const cartChainOfResp = new CartChainOfResp();
let finishedCart = cartChainOfResp.calc(miningCart);
finishedCart.jewels;
// 100
手推车从 Sneezy 那里收集了 50 颗宝石,由于 Doc 仍在采矿所以没有收集,从 Dopey 那里收集了 50 颗宝石,然后满载着 100 颗宝石返回。
让我们稍微调整一下场景,增加手推车的限制和矮人的数量。
const miningCart = new MiningCart(150),
Sneezy = new Dwarf(50, false),
Doc = new Dwarf(25, false),
Dopey = new Dwarf(50, false),
Sleepy = new Dwarf(30, false);
// Same code still here //
finishedCart.jewels;
// 125
手推车会从所有矮人那里收集宝石,然后跳过 Sleepy(因为他会让手推车超出限制),最后带着 125 颗宝石返回。
现在让我们调整一下这个场景,让 Dopey 继续挖矿。这样可以为 Sleepy 的宝石腾出空间,但会降低矿车的总宝石数量。
const miningCart = new MiningCart(150),
Sneezy = new Dwarf(50, false),
Doc = new Dwarf(25, false),
Dopey = new Dwarf(50, true),
Sleepy = new Dwarf(30, false);
// Same code still here //
finishedCart.jewels;
// 105
在所有这些场景中,我们都看到责任链无论由哪些矮人组成,都能执行其逻辑。
设计模式城堡方法
这个解释性童话故事只剩一篇探讨结构模式的文章了。虽然写这个系列很有趣,但我也期待着拯救编程公主,并结束这个故事的回购。所以,请继续关注最后一篇!
待续...
文章来源:https://dev.to/maxwell_dev/snow-white-and-the-seven-behavioral-patterns-a-design-patterns-intro-3ljp