paint-brush
Fluent Interface With Callbacksby@msarica
462 reads
462 reads

Fluent Interface With Callbacks

by MehmetJanuary 3rd, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The API is a callback based API that is used to run functions in order. Each function will run in order and 1 second apart. However, wouldn't it be nicer if we could chain methods? How I accomplished it: simply return the object reference to chain other methods so that we can run all of them in a nested way. The result is a fluent API that makes programming so much easier and easy to use with no need for a call-based API. For example, the class has 3 methods and each expects a callback function.

Coin Mentioned

Mention Thumbnail
featured image - Fluent Interface With Callbacks
Mehmet HackerNoon profile picture
const obj = new ItemObject()
.addItem('a')
.addItem('b')
.removeItem('c')

Is there anyone who doesn't like fluent interfaces? It makes programming so much easier. I recently experimented on converting a callback based API into a fluent api.

Let's take the following class as an example. It has 3 methods and each expects a callback. If we wanted to run all of them in order, we would have to call them in a nested way as shown at the bottom of the snippet.

export type Callback = (err?: any, data?: any)=> void;

export class Task {
  func1(callback?: Callback){
    setTimeout(()=>{
      console.log('function 1');
      return callback && callback();
    },1000);
  }

  func2(callback?: Callback){
    setTimeout(()=>{
      console.log('function 2');
      return callback && callback();
    },1000);
  }

  func3(callback?: Callback){
    setTimeout(()=>{
      console.log('function 3');
      return callback && callback();
    },1000);
  }
}

const obj = new Task();
obj.func1(()=>{
  obj.func2(()=>{
    obj.func3();
  })
});

This will run as you would expect. Each function will run in order and 1 second apart.

However, wouldn't it be nicer if we could chain methods?

Ok long story short, here is how I accomplished it:

export type Callback = (err?: any, data?: any)=> void;

interface Stage {
  func: Callback;
  callback: Callback;
}

export class FluidTask {
  private stack: Stage[] = [];
  private isRunning = false;

  private stager(func: Callback, callback?: Callback){
    this.stack.push({ func, callback });

    if(!this.isRunning){
      this.stageRunner();
    }
  }

  private stageRunner(){
    const stage = this.stack.shift();
    if(!stage){
      this.isRunning = false;
      return;
    }

    this.isRunning = true;
    stage.func((err, data)=>{
      stage.callback && stage.callback(err, data);

      this.stageRunner();
    });
  }

  private _func1(callback?: Callback){
    setTimeout(()=>{
      console.log('function 1');
      return callback && callback();
    },1000);
  }

  private _func2(callback?: Callback){
    setTimeout(()=>{
      console.log('function 2');
      return callback && callback();
    },1000);
  }

  private _func3(callback?: Callback){
    setTimeout(()=>{
      console.log('function 3');
      return callback && callback();
    },1000);
  }

  func1(callback?: Callback){
    this.stager((cb)=> this._func1(cb), callback);

    return this;
  }

  func2(callback?: Callback){
    this.stager((cb)=> this._func2(cb), callback);

    return this;
  }

  func3(callback?: Callback){
    this.stager((cb)=> this._func3(cb), callback);

    return this;
  }
}

new FluidTask()
.func1()
.func2(()=> console.log('function 2 has finished'))
.func3()
;

and voila!

Let me explain what's going on!

There are two main methods

stager
and
stageRunner
.

  private stack: Stage[] = [];
  private isRunning = false;

  private stager(func: Callback, callback?: Callback){
    this.stack.push({ func, callback });

    if(!this.isRunning){
      this.stageRunner();
    }
  }

stager
method expects two arguments. The first one is the function to be executed and the second one is the callback to be called when the function is done. It pushes these two values the stack and if it's not running we will call the method
stageRunner
.

  private stageRunner(){
    const stage = this.stack.shift();
    if(!stage){
      this.isRunning = false;
      return;
    }

    this.isRunning = true;
    stage.func((err, data)=>{
      stage.callback && stage.callback(err, data);

      this.stageRunner();
    });
  }

This method will be called recursively. It will take the first item out and easy enough, if it's undefined, it will mark as not running and return.

If it has an item, that means we have the function. We call the

func
function with a callback that wraps the original callback that we stored.

This is the reason why we needed to save the original callback so that we can know when the execution finishes.

And then the obvious, call

stageRunner
.

Let's look at one of the methods now!


  private _func1(callback?: Callback){
    setTimeout(()=>{
      console.log('function 1');
      return callback && callback();
    },1000);
  }

  func1(callback?: Callback){
    this.stager((cb)=> this._func1(cb), callback);

    return this;
  }

In

func1
, the function calls the
stager
method and passes in a callback function:
(cb)=> this._func2(cb)
This is the function to be executed.

When its turn, essentially the following will happen:

this._func2((err, data) => callback(err, data));

and finally

return this;
so that we can return the object reference to chain other methods.


new FluidTask()
.func1()
.func2(()=> console.log('function 2 has finished'))
.func3()
.func1(()=> console.log('this is fun'))
;