Fluent Interface With Callbacks

Written by msarica | Published 2020/01/03
Tech Story Tags: typescript | fluid-api | callback | javascript | software-development | programming | beginners | design-patterns

TLDR 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.via the TL;DR App

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'))
;


Written by msarica | msarica.com
Published by HackerNoon on 2020/01/03