It is said that every programmer thinks about the elevator scheduling algorithm while waiting for the elevator… So how do you actually implement one? While this scenario may seem complex, it is well suited to be handled using the responsive programming paradigm. Below we will in RxJS and Vue based, step by step to achieve a minimum available elevator scheduling simulation Demo.

Demo

To avoid readers [showing me this with their pants off?] Here we show what 50 lines of code can finally achieve: a 10-story elevator, you can press the ↓ on each floor to summon the elevator to take you to the first floor. When multiple floors summon the elevator at different times, the simulator should rise and fall in a manner consistent with the everyday experience. Before making fun of why it is so simple, the reasons for its implementation will be slowly introduced in the following 😅

Link to the demo

Nuggets iframe tag doesn’t work properly, check Blog Post for Demo

Get Started

Before introducing the actual coding details, we might as well consider the most basic idea, that is, how to express the elevator scheduling? Or to put it another way, it’s actually a more interesting topic: How do you abstract an elevator using code?

The elevator can be abstracted as a box hanging on a string. We can pass in its weight m, its height from the ground H, its current velocity V, and its current acceleration A, and then use a series of clever formulas to describe its trajectory… Congratulations, science thinking has led you astray 🙄 Rest assured that the last 50 lines of code do not involve any knowledge of high school physics.

There is an old joke about elevators that is more in line with our abstraction: an old diaosi sees an old woman enter the elevator, and when she comes out, she is rich and beautiful, so he thinks it would be nice to bring his wife… The abstraction of the elevator here is nothing more than a door with numbers jumping. We do not need to care about how its machinery works, for its state, as long as we know the direction and floor number on the LCD screen at the elevator mouth is enough. Well, that’s the technical thinking in Duck Typing!

What is the difference between these two kinds of thinking? Let’s consider the simplest case: press a button on the tenth floor to call the elevator up from the first floor. In this case, the two abstract methods describe very different things:

  • Method 1: the box starts to move up at speed V and stops at height H of the tenth floor.
  • Method 2: the number of floors starts from 1 and increases by 1 at a fixed time interval until it stops at 10.

Well, the latter seems easy to implement: just change the number of floors by setTimeout every second and the elevator is simulated 😎 Congratulations, you are jumping into a pit of asynchronous event flow. Consider these requirements:

  • You are on the second floor and want to go downstairs. You notice that the elevator is coming down from the third floor. The elevator will pick you up 😆
  • You are on the tenth floor and want to go downstairs. You notice that the elevator is going down the ninth floor. The elevator will not come back to pick you up 😜
  • You are on the tenth floor and want to go downstairs when you notice the elevator coming up from the second floor. You thought it would stop here, but it turned out to be the asshole on the 20th floor who called the elevator 😡

Redux Flux MobX. There is a layer to writing about this requirement. Well, that’s the end of our foreplay, and it’s time to introduce Reactive Programming to 😀

In the Reactive paradigm, the concept of a Stream of events is very powerful. We all know that computer data is inherently discrete. Even a little sister’s video is broken down into 24 frames per second. For our elevator simulator, its input is actually a series of discrete operations of the user on each floor over time, while the output is a state of the current floor and direction at the current time. Thus, we can use Stream to express the emulator’s input.

What’s the difference between a Stream and a naive event listener? Stream can be combined, filtered and transformed in the time dimension. If that sounds abstract, consider this example: Press the elevator button once on the tenth floor, and the number of floors goes from one to ten. At this point, we map one event in a stream of events to a new stream that fires ten events in turn. For example, as long as we simply connect the event flow from the first floor to the tenth floor and the event flow from the tenth floor to the first floor, we can realize the basic function of the elevator to pick up people and then return!

With that said, it’s time to Show Me the Code 🤓 And let’s take a step-by-step approach to implementing Demo Reactive.

Step 1

First, a brief introduction to the technical background of the Demo: For simplicity, we have chosen Vue to act as a simple view layer and RxJS Reactive to implement the core functions. Due to space limitations, we will not cover the details of Vue usage, but only the important features related to Reactive 🙃 on the other hand, going from 0 to 1 is always the hardest, so Step 1 will be the most extensive 😅

We’ve already mentioned the power of streams in Rx. So, let’s first consider the most basic requirement: press the ↓ on the tenth floor, and the elevator number increases successively from 1. At this point, we map a new stream from an event in the click event stream:

import { Observable } from 'rxjs'

const stream = Observable
  // Convert DOM floor click events into An Observable event stream
  .fromEvent(emitter, 'click')
  // A new stream of new events is triggered with an output interval of 1s
  .interval(1000)

// A series of asynchronous outputs of a stream can be subscribed
stream.subscribe(x= > console.log(x))Copy the code

Executing the code above, clicking the button will trigger a stream of events incrementing from zero every second, and you will see steady output on the console every second. But that doesn’t fit the bill: how do you add floors only ten times? We introduce the take method:

const up = Observable
  .fromEvent(emitter, 'click')
  .interval(1000)
  // Will only trigger 10 times!
  .take(10)Copy the code

Well, then there’s something a little less elegant: the floor numbers increase as required, but they go from zero to nine, not from one to 10. (Does your house have zero floors?) To map new streams according to specific rules, we can simply use the familiar map method:

const up = Observable
  .fromEvent(emitter, 'click')
  .interval(1000)
  .take(10)
  / / + 1 🐸
  .map(x= > x + 1)Copy the code

Now we can go from the first floor to the tenth floor, but how do we get down? Let’s first build a Stream from the tenth floor to the first floor 😏

const down = Observable
  .interval(1000)
  .map(x= > 10 - x)
  .take(10)Copy the code

The elevator needs to go UP and UP and DOWN. To do this, we simply concat two streams:

function getStream () {
  // Declare Up and Down...
  return up.concat(down) 
}Copy the code

So far we have used the interval/take/map/concat apis, but there is one key Step to complete Step 1: how to control the flow of events when the elevator button is pressed multiple times on different floors?

From the use of these apis, some of you may find that our coding algorithm is somewhat Laplacian: when the elevator button is pressed, a series of state changes in the future are already determined at that moment. In other words, give me an accurate enough current state that I can calculate the entire future (being dragged away)… The first problem is: if a new input event occurs in the output sequence of events, how do we define the subsequent state?

Here, we introduce the switchMap method to express the logic: assume that pressing a button on the tenth floor triggers ten events in the next ten seconds. Then, after the encapsulation of switchMap, once a new button is pressed at a certain point in ten seconds, the original remaining events will be discarded and the new event derived from the event of the new button will be triggered from now on. In another way, it is from the first floor to the tenth floor of the elevator, if you go halfway to the fifth floor, immediately start again from the first floor, go to the fifth floor back. Since we only care about the state, not how such a quantized elevator is implemented, the Step 1 simulator is stable. Encapsulate a few parameters, and the first Demo is done:

The link step 1

Click any button in the Demo above and the elevator will pick you up from the ground floor and back. If you click on a new floor again halfway through, the elevator will immediately start from the first floor again. Pick someone up on the new floor. Well, it’s a long way from being practical, but it’s getting there. So far our Rx logic looks something like this, very short:

import { Observable } from 'rxjs'

export function getStream (emitter, type) {
  return Observable
    .fromEvent(emitter, type)
    // target is the floor number that triggers the button event in Vue
    .switchMap(({ target }) = > {
      const up = Observable
        .interval(1000)
        .map(x= > x + 1)
        .take(target)
      const down = Observable
        .interval(1000)
        .map(x= > target - x)
        .take(target)
      return up.concat(down)
    })
}Copy the code

Step 2

In this step, we need to solve the problem that the elevator magically quantized to the ground floor when the new button was pressed. We don’t need to introduce a new API, just tweak the logic a bit:

In the first step, the state in the input stream is the only target floor, which means the elevator doesn’t even know what floor it is on when the button is triggered. To do this, we added a curr parameter to Vue to mark this state, so that the elevator will go from the current floor to the new target floor whenever a new event is triggered, instead of appearing directly on the first floor:

// Add a curr parameter
.switchMap(({ target, curr }) = > {
  const up = Observable
    .interval(1000)
    // Start from the current floor to the new floor
    .map(x= > x + curr)
    .take(target + 1 - curr)
  const down = Observable
    .interval(1000)
    .map(x= > target - x)
    .take(target)
  return up.concat(down)Copy the code

After adding this state, the effect of Step 2 is as follows:

Links to step 2

In this Demo, you can click on the fifth floor and then click on the seventh floor when the elevator reaches the third floor. The elevator will not appear directly on the first floor, but from the third floor up to the seventh floor and down.

But this brings up a new status problem: click on the fifth floor first, and then click on the second floor when the elevator reaches the third floor. Boom! The elevator has a bug and can’t move…

Step 3

The reason for the bug in the previous step is that you took a negative number (5 to 6 floors would have taken once, but 5 to 4 floors would have taken -1). Normal array subscripts cross the boundary, but an Observable for time series crosses the boundary and is actually -1s… Let’s fix it with a little logic!

.switchMap(({ target, curr }) = > {
  // The target floor is higher than the current floor
  if (target >= curr) {
    const up = Observable
      .interval(1000)
      .map(x= > x + curr)
      .take(target + 1 - curr)
    const down = Observable
      .interval(1000)
      .map(x= > target - x)
      .take(target)
    return up.concat(down)
  } else {
    // The target floor is lower than the current floor, we go straight downstairs
    return Observable
      .interval(1000)
      .map(x= > curr - x)
      .take(curr)
  }Copy the code

Ok, bug fix:

step 3

In the above example, no matter how much you press the button, the elevator will not be quantized and will not be broken. But a new storm arose: go back and forth between the tenth floor and the fifth floor, and discover why the elevator comes and goes but never reaches the first floor…

Step 4

In the above example, the state we passed into the Stream was never sufficient to support the elevator scheduling algorithm. For example, we didn’t indicate whether a floor was lit up by a button. In this step, we add a state to the Vue view layer:

  // ...
  data () {
    return {
      floors: [{up: false.down: false },
        { up: false.down: false },
        { up: false.down: false },
        { up: false.down: false },
        { up: false.down: false },
        { up: false.down: false },
        { up: false.down: false },
        { up: false.down: false },
        { up: false.down: false },
        { up: false.down: false}].currFloor: 1}},Copy the code

Well, never mind the details of why we don’t have the ↑ button and the up state. In Rx we added some simple processing to make the state of the event spread not only to the current floor, but also to the current direction:

if (targetFloor >= baseFloor) {
  const up = Observable
    .interval(1000)
    .map(count= > {
      const newFloor = count + baseFloor
      return {
        floor: newFloor,
        // Outgoing current direction
        direction: newFloor === targetFloor ? 'stop' : 'up'
      }
    })
    .take(targetFloor + 1 - baseFloor)
    // ...
}Copy the code

Anyway, the simulator now looks like this:

The link step 4

When clicked, an alert pops up in the Rx to tell you: I am aware of these states in the event stream! But it still can’t solve the problem of the first floor…

Final Step

In the last step, we need to use Rx to deal with the problem that we can’t get to the first floor before. We know that, according to determinism, Rx actually plans the elevator’s future movement at each button event. So, can we do some subtraction to filter out the events that affect the state? Here we can use filter to manipulate the event stream:

In a simplified model, we can assume that the elevator will only perform an up and down operation. At this time, the new events triggered in the process of elevator movement can be classified as follows:

  • If the elevator is falling, then no matter which floor the new event is triggered on, it can’t make the elevator up and down again, ensuring that the elevator will always descend to the first floor
  • If the elevator is going up, but the new descent event is on a floor lower than the current one, then the elevator can pass through the new floor during the descent, so that it does not need to go up and down again
  • If the elevator is going up and the floor of the new descent time is higher than the current floor, then we can conduct the up and down of the new floor again.

In three cases, we will determine whether we need up and down. Since each up and Down is an event input to the switchMap, we can directly place a filter in front of the switchMap to filter out irrelevant button events:

  return Observable
    .fromEvent(emitter, type)
    .filter(({ floors, targetFloor, currFloor, currDirection }) = > {
      //
      if (currDirection === 'down') return false
      else if (currDirection === 'up' && targetFloor <= currFloor) {
        return false
      } else return true
    })Copy the code

With this logic in place, we changed the target floor of up and down from the floor where the event occurred to the maxTargetFloor from floors, which ensured that the elevator would reach the target floor and return normally. But there’s one final snag: If you press the tenth floor on your way down, the elevator won’t pick you up when it reaches the first floor… The solution is simple, try to make the elevator up and down again as it descends to the first floor.

After we have implemented the last bit of asynchronous logic, here is the Demo at the beginning of this article:

Link to the final

At this point, there are still only 40 lines of code in Rx. The code in Vue doesn’t involve any asynchronous logic, just subscribing to an Observable and rendering data.

Wrap Up

So far, our simulator is only a subset of the real elevator, and it lacks the following features:

  • A panel that allows the user to select the status in the elevator
  • Each layer of thewritebutton

But based on the basic idea of Rx, simulating these features doesn’t add significantly to the complexity: events triggered by state selection in an elevator are exactly the same in priority as floor selection outside the elevator door (as can be demonstrated by pressing one floor in an upward-moving elevator and the elevator won’t talk to you); The introduction of the ↑ button also introduces a new deterministic state… It would be irresponsible to say this, but Rx event flows do have the ability to elegantly solve these problems based on our existing implementation.

If you’re still trying to decide whether or not to incorporate Rx into your existing projects, this exercise may give you some Pointers: Rx is very powerful when dealing with asynchronous event flows, and state managers like Redux/MobX are not really on the same level as Rx and can handle high business complexity when combined with Rx. However, if your only requirement is to display Loading state, then introducing Rx is a bit of an overkill.

Finally, this is actually the author’s first attempt at an Rx project. Not much code is actually written, but getting used to it and using it to actually solve a problem requires a lot more thinking time than typing a few lines of code… This is a bit of fun 🙃 each Step in this article is extracted from a real commit during the development process, hopefully this article will help you 🙃

The Github Portal Observable document