Snow White and the Seven Behavioral Patterns - A Design Patterns Intro

2025-05-25

白雪公主和七种行为模式——设计模式简介

欢迎阅读我的“设计模式童话”系列的第三篇!本系列旨在以一种更通俗易懂的方式——也就是童话般的隐喻——来解释设计模式。

上一篇文章介绍了行为模式的前半部分,这些模式允许多个对象协同完成复杂的任务。这篇文章将介绍这组模式的后半部分,并使用简单的例子,以另一个经典故事《白雪公主和七个小矮人》为背景进行解释。

这个系列我已经介绍过两次了,这里就不多说了。我们直接开始吧。

用侦察兵警告矮人

让我们把白雪公主的故事聚焦在小矮人和他们的采矿工作上。所以,让我们以七个小矮人开始一天工作的方式开始:其中一个小矮人向其他小矮人喊一声“嗨嗬”,让他们知道该开始工作了。在电影里,博士是第一个喊出这句话的人,所以我们在这里也照做。

Doc 需要一种能够大声呼喊,并且确保其他矮人能够听到并回应的方式,所以他决定使用观察者模式。首先,他为自己编写了一个类,作为发出呼喊的对象。他从一个简单的方法开始,创建一个矮人列表,用于向其发出呼喊。

class Caller {
  constructor() {
    this.dwarves = [];
    this.yell = null;
  }

  register(dwarf) {
    this.dwarves.push(dwarf);
  }
}
Enter fullscreen mode Exit fullscreen mode

最重要的属性是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();
  }
}
Enter fullscreen mode Exit fullscreen mode

这个updateAll函数会接收每个正在听的小矮人的信息,并说“呼叫者在喊叫,所以你也需要喊叫!”每个小矮人都会被传递一个Caller,它会告诉他们需要喊什么。

我们可以看到矮人在DwarfObserver课堂上会如何反应。

class DwarfObserver {
  constructor() {
    this.yell = null;
  }

  update(caller) {
    this.yell = caller.yell;
  }
}
Enter fullscreen mode Exit fullscreen mode

每当一个矮人“更新”了,我们就会让他们跟着博士喊。换句话说,当他们听到博士喊的时候,他们也应该喊同样的话。这就是观察者模式的核心,一个对象观察另一个对象的变化,并在变化发生时立即采取特定的行动。

让我们通过将 Doc 定义为呼叫者,并定义另外四个矮人听他说话来建立这个具体的例子。

const Doc = new Caller();

const Happy = new DwarfObserver(),
      Bashful = new DwarfObserver(),
      Sneezy = new DwarfObserver(),
      Grumpy = new DwarfObserver();
Enter fullscreen mode Exit fullscreen mode

我们会向 Doc 登记每个矮人的情况,这样他们就会开始听他的喊叫。

const dwarves = [Happy, Bashful, Sneezy, Grumpy]
dwarves.forEach(dwarf => Doc.register(dwarf));
Enter fullscreen mode Exit fullscreen mode

现在,当我们告诉 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!'
Enter fullscreen mode Exit fullscreen mode

使用迭代器跨越桥梁

现在矮人们都去干活了,但他们还没到矿井。假设现在是清晨,他们需要穿过一座窄桥才能到达矿井。但天色太暗,他们几乎看不见其他矮人,而且他们又不敢冒险让多个矮人同时过桥。

Doc 发现他们需要一个谨慎的方法,让每个矮人都能过河,并且能够追踪有多少矮人需要过河。于是他编写了一个 Iterator 类来实现这个功能。

class DwarfIterator {
  constructor(dwarves) {
    this.index = 0;
    this.dwarves = dwarves; // is an array
  }

  hasNext() {
    return this.index < this.dwarves.length;
  }
}
Enter fullscreen mode Exit fullscreen mode

目前这个类的功能不多。它有一个矮人列表,索引为零,所以它位于列表的开头,还有一个函数来判断它们是否位于列表的末尾。这很有用,但不足以让它们过桥。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!";
  }
}
Enter fullscreen mode Exit fullscreen mode

通过此设置,Doc 只需传入矮人列表即可反复使用callNextDwarf迭代器 (Iterator) 让它们能够轻松地浏览矮人列表,从而抽象出追踪矮人位置和执行操作的工作。

让我们设置矮人列表(我们可以使用字符串数组而不是对象来节省时间),并将它们传递到迭代器中。

const crossingDwarfs = [
  "Sneezy",
  "Sleepy",
  "Happy",
  "Doc",
  "Grumpy",
  "Dopey",
  "Bashful"
];

const dwarfCounter = new DwarfIterator(crossingDwarfs);
Enter fullscreen mode Exit fullscreen mode

现在,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!
Enter fullscreen mode Exit fullscreen mode

通过调解员管理每个人的工作

矮人们已经进矿了,准备开始工作。矿区有很多区域需要开采,每个矮人都会去不同的区域。在各个区域之间移动需要花费大量时间,而且他们不希望一个区域同时有超过一个矮人,因为他们想要各种各样的石头。

这很难管理,因为如果一个矮人想换到其他区域,他们不知道那里是否已经有人了。Doc 发现了这个问题,作为一个擅长解​​决问题的矮人,他编写了一个类,让自己成为一名调解员。

CallerMediator 类与Observer 类类似。它有一个矮人列表,但它不会向所有矮人发出变化警报,而是利用这些矮人的信息来更好地协调和指导他们的行动。

class DwarfMediator {
  constructor() {
    this.dwarves = [];
  }

  checkSection(section) {
    return this.dwarves.every(dwarf => dwarf.section !== section);
  }
}
Enter fullscreen mode Exit fullscreen mode

这里,协调不同的矮人采用 的形式checkSection。它会检查该区域内是否已有其他矮人,并分别返回truefalse。这可以防止他所协调的所有矮人的区域重叠。

Doc 还为他调解的矮人编写了一个类。每个矮人都需要一个调解员,所以他确保每当一个工人被创建时,它都会将自己添加到调解员的工人列表中。

class DwarfWorker {
  constructor(section, mediator) {
    this.section = section;
    this.mediator = mediator;
    this.mediator.dwarves.push(this);
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这很好,因为该类与 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);
Enter fullscreen mode Exit fullscreen mode

假设 Sneezy 想去“Rubies”区。Doc 看到 Sleepy 已经在那里了,就会阻止他。

Sneezy.askToMove("Rubies");
// Sneezy.position is still "Rubies!"
Enter fullscreen mode Exit fullscreen mode

喷嚏精明白了,待在原地,转而询问“珍珠”区的情况。医生看到那里是空的,就让他过去了。

Sneezy.askToMove("Pearls");
// Sneezy.position has changed to "Pearls!"
Enter fullscreen mode Exit fullscreen mode

这样,我们就可以让多个对象以有组织的方式一起工作,从而避免紧密耦合。

使用纪念品追踪挖矿进度

矮人们继续工作,意识到最近珠宝和钻石失踪了,他们需要更好地记录找到了多少。Sneezy 想要记录他们每小时挖出的数量,这样如果他们发现有些宝石不见了,就可以每小时查看有多少,看看它们是什么时候消失的。

Sneezy 决定为此设置一个 Memento 模式。这样他就能追踪变化,并检查之前的状态计数。

Sneezy 写的 Memento 图案分为三个部分。首先是 Memento 本身,它是收集到的石头的“快照”。他每小时都会制作一张新的。

class Memento {
  constructor(jewels, diamonds) {
    this.jewels = jewels;
    this.diamonds = diamonds;
  }
}
Enter fullscreen mode Exit fullscreen mode

他还需要一个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,
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

此模式的最后一部分是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)
  }
}
Enter fullscreen mode Exit fullscreen mode

现在,Sneezy 已经准备好开始追踪 Mementos 了。首先,他指定自己为管理员。

const Sneezy = new CaretakerDwarf();
Enter fullscreen mode Exit fullscreen mode

现在他可以使用发起者添加新的纪念品来每小时追踪珠宝和钻石。

Sneezy.addMemento(originator.store(2, 4));
Sneezy.addMemento(originator.store(3, 6));
Sneezy.addMemento(originator.store(0, 2));
Enter fullscreen mode Exit fullscreen mode

如果 Sneezy 需要检查第二个小时收集了多少数据,他可以快速恢复该小时的“状态”。该状态存储在一个变量中,他可以将其与其他变量进行比较,或者执行其他操作。

const secondHourResults = originator.restore(Sneezy.getMementoFromHour(2));
secondHourResults.jewels;   // 3
secondHourResults.diamonds; // 6
Enter fullscreen mode Exit fullscreen mode

通过责任链加载珠宝

一天的工作快结束了,矮人们得开始把石头装上车了。哈皮很高兴能完成这件事,但他意识到还有很多事情需要考虑。

  • 每辆推车只能装载有限数量的石头。矮人不能有剩余,推车也不能装满。
  • 根据每个矮人的位置,手推车可以走多条不同的路径。
  • 一些矮人可能仍在采矿,需要继续传递矿车。

Happy 唯一能看到的常数是手推车在矮人之间移动,但每个矮人的具体动作可能有所不同。这个谜题的核心是如何沿着矮人链传递手推车对象,他意识到责任链模式可以解决这个问题。

首先,他为采矿车创建一个类。每个采矿车必须分配一个宝石限额和一个简单的指定矮人列表。

class MiningCart {
  constructor(limit) {
    this.limit = limit;
    this.jewels = 0;
    this.dwarves = [];
  }

  setNextDwarf(dwarf) {
    this.dwarves.push(dwarf);
  }

  addJewels(jewels) {
    this.jewels += jewels;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Happy 还需要为每个获得手推车的矮人设置一个等级,以追踪他们是否正在采矿以及他们拥有的宝石。

class Dwarf {
  constructor(jewels, isMining) {
    this.jewels = jewels;
    this.isMining = isMining;
  }
}
Enter fullscreen mode Exit fullscreen mode

最后,也是最重要的一点,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;
  }
}
Enter fullscreen mode Exit fullscreen mode

让我们来看看这个模式的实际应用。哈皮看到矿井里有三个矮人,他也可以把矿车交给他们。矿车的宝石限额是 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);
Enter fullscreen mode Exit fullscreen mode

设置好矿车及其路径后,它就可以沿着链条向上移动了。他创建了一个责任链实例,并将矿车传递给它,让它沿着链条向上移动。

const cartChainOfResp = new CartChainOfResp();
let finishedCart = cartChainOfResp.calc(miningCart);

finishedCart.jewels;
// 100
Enter fullscreen mode Exit fullscreen mode

手推车从 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
Enter fullscreen mode Exit fullscreen mode

手推车会从所有矮人那里收集宝石,然后跳过 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
Enter fullscreen mode Exit fullscreen mode

在所有这些场景中,我们都看到责任链无论由哪些矮人组成,都能执行其逻辑。

设计模式城堡方法

这个解释性童话故事只剩一篇探讨结构模式的文章了。虽然写这个系列很有趣,但我也期待着拯救编程公主,并结束这个故事的回购。所以,请继续关注最后一篇!

待续...

封面图片由 SafeBooru.org 提供

文章来源:https://dev.to/maxwell_dev/snow-white-and-the-seven-behavioral-patterns-a-design-patterns-intro-3ljp
PREV
记录一切
NEXT
成为一名极度缺乏安全感的程序员