任务

我们的机器人将在村庄周围移动。 在各个地方都有包裹,每个都寄往其他地方。 机器人在收到包裹时拾取包裹,并在抵达目的地时将其送达。

自动机必须在每个点决定下一步要去哪里。 所有包裹递送完成后,它就完成了任务。

为了能够模拟这个过程,我们必须定义一个可以描述它的虚拟世界。 这个模型告诉我们机器人在哪里以及包裹在哪里。 当机器人决定移到某处时,我们需要更新模型以反映新情况。

如果你正在考虑面向对象编程,你的第一个冲动可能是开始为世界中的各种元素定义对象。 一个机器人,一个包裹,也许还有一个地点。 然后,它们可以持有描述其当前状态的属性,例如某个位置的一堆包裹,我们可以在更新世界时改变这些属性。

这是错的。

至少,通常是这样。 一个东西听起来像一个对象,并不意味着它应该是你的程序中的一个对象。 为应用程序中的每个概念反射式编写类,往往会留下一系列互连对象,每个对象都有自己的内部的变化的状态。 这样的程序通常很难理解,因此很容易崩溃。

相反,让我们将村庄的状态压缩成定义它的值的最小集合。 机器人的当前位置和未送达的包裹集合,其中每个都拥有当前位置和目标地址。这样就够了。

当我们到达新地点时,让我们这样做,在机器人移动时不会改变这种状态,而是在移动之后为当前情况计算一个新状态。

  1. class VillageState {
  2. constructor(place, parcels) {
  3. this.place = place;
  4. this.parcels = parcels;
  5. }
  6. move(destination) {
  7. if (!roadGraph[this.place].includes(destination)) {
  8. return this;
  9. } else {
  10. let parcels = this.parcels.map(p => {
  11. if (p.place != this.place) return p;
  12. return {place: destination, address: p.address};
  13. }).filter(p => p.place != p.address);
  14. return new VillageState(destination, parcels);
  15. }
  16. }
  17. }

move方法是动作发生的地方。 它首先检查是否有当前位置到目的地的道路,如果没有,则返回旧状态,因为这不是有效的移动。

然后它创建一个新的状态,将目的地作为机器人的新地点。 但它也需要创建一套新的包裹 - 机器人携带的包裹(位于机器人当前位置)需要移动到新位置。 而要寄往新地点的包裹需要送达 - 也就是说,需要将它们从未送达的包裹中移除。 'map'的调用处理移动,并且'filter'的调用处理递送。

包裹对象在移动时不会更改,但会被重新创建。 move方法为我们提供新的村庄状态,但完全保留了原有的村庄状态。

  1. let first = new VillageState(
  2. "Post Office",
  3. [{place: "Post Office", address: "Alice's House"}]
  4. );
  5. let next = first.move("Alice's House");
  6. console.log(next.place);
  7. // → Alice's House
  8. console.log(next.parcels);
  9. // → []
  10. console.log(first.place);
  11. // → Post Office

move会使包裹被送达,并在下一个状态中反映出来。 但最初的状态仍然描述机器人在邮局并且包裹未送达的情况。