Behaviours in Javascript?

I really like to use behaviors in Elixir but now I have to work with Javascript so I don’t have them.
Is there a library that would let me have this functionality? Or maybe there is a native solution for this?

You can use an interface, but that might only be available if you use typescript.

1 Like

javascript: “If it walks like a duck and it quacks like a duck, then it must be a duck”.
ECMAScript 6 has classes, but not abstract classes or interfaces.

Typescript has interfaces

In javascript you can emulate abstract methods but these are only checked at runtime:

class Foo {
 
    constructor(text){
       this._text = text;
    }
 
    /**
     * Implementation optional
     */
    genericMethod() {
        console.log('running from super class. Text: '+this._text);
    }
    
    /**
     * Implementation required
     */
    doSomething() {
       throw new Error('You have to implement the method doSomething!');
    }
 
}
 
class Bar extends Foo {
 
    constructor(text){
       super(text);
    }
    
}
 
let b = new Bar('Howdy!');
b.genericMethod(); //gonna print: running from super class. Text: Howdy
b.doSomething(); //gonna throw an error: 
           // Uncaught Error: You have to implement the method doSomething!
2 Likes

Behaviours use a conceptual contract between the (generic) behaviour module and the (specialized) callback module - but that doesn’t mean a behaviour is-like an interface - because that doesn’t look at the whole picture.

Is the strategy pattern like an interface? No, the Strategy implements the interface that the Context accepts.

For some more details see here.

So in JavaScript you would use the Strategy Pattern

Context -> Behaviour Module
Strategy -> Callback Module

The Context is the generic part, while the Strategy is the specialized part. Inject (i.e. compose) the Strategy into the Context instance upon creation so that the Strategy can affect the Context’s behaviour at runtime.


(function() {

  // Context/Behaviour module
  class Server {
    start(callbackModule, initArg) {
      let [_, state] = callbackModule.init(initArg)
      this.state = state
      this.cbModule = callbackModule
    }

    call(request) {
      let [_, reply, new_state] = this.cbModule.handle_call(request, this.state)
      this.state = new_state
      return reply
    }

    cast(request) {
      let [_, new_state] = this.cbModule.handle_cast(request, this.state)
      this.state = new_state
    }
  }

  // Strategy/Callback module
  class Stack {
    static init(stack) {
      return [true, stack]
    }

    static handle_call(request, state) {
      let status = false
      let reply = null
      let new_state = state

      switch(request) {
        case 'POP':
          reply = state.shift()
          // state mutated new_state
          status = true
          break;
      }
      return [status, reply, new_state]
    }

    static handle_cast(request, state) {
      let status = false
      let new_state = state

      if (!Array.isArray(request)) {
        return [status, new_state]
      }

      switch(request[0]) {
        case 'PUSH':
          state.unshift(request[1])
          // state mutated new_state
          status = true
          break;
      }
      return [status, new_state]
    }
  }

  let server = new Server()
  server.start(Stack, ['hello'])
  let result = server.call('POP')
  console.log(result)
  server.cast(['PUSH','world'])
  result = server.call('POP')
  console.log(result)
}())

Module-based version:

// file: server.mjs
// Context/Behaviour module
export async function start(callbackName, initArg) {
  let cbModule = await import(callbackName);
  let [_, state] = await cbModule.init(initArg);
  return {cbModule, state};
}

export async function call(server, request) {
  let {cbModule, state} = server;
  let [_, reply, new_state] = await cbModule.handleCall(request, state);
  return [{...server, state: new_state}, reply];
}

export async function cast(server, request) {
  let {cbModule, state} = server;
  let [_, new_state] = await cbModule.handleCast(request, state);
  return {...server, state: new_state};
}
// file: stack.mjs
// Strategy/Callback module

export async function init(stack) {
  return [true, stack];
}

export async function handleCall(request, state) {
  let status = false;
  let reply = null;
  let new_state = state;

  switch(request) {
    case 'POP':
      reply = state.shift();
      // state mutated new_state
      status = true;
      break;

  }
  return [status, reply, new_state];
}

export async function handleCast(request, state) {
  let status = false;
  let new_state = state;

  if (!Array.isArray(request)) {
    return [status, new_state];
  }

  switch(request[0]) {
    case 'PUSH':
      state.unshift(request[1]);
      // state mutated new_state
      status = true;
      break;
  }
  return [status, new_state];
}
<!DOCTYPE html>
<html lang="en">
  <!-- file: index.html -->
  <head>
    <meta charset="utf-8">
    <title>Behaviours in JavaScript</title>
  </head>
  <body>
    <h1>Behaviours in JavaScript</h1>

    <p>Check the developer tools console</p>
    <p style="font-size:0.7rem">Works in Chrome 78.0.3904.108 with http-server@0.11.1</p>
  </body>

  <script type="module">
   import {start, call, cast} from './server.mjs';

   async function run() {
     let server = await start('./stack.mjs', ['hello']);
     let result;
     [server, result] = await call(server, 'POP');
     console.log(result);

     server = await cast(server, ['PUSH','world']);
     [server, result] = await call(server, 'POP');
     console.log(result)
   }

   run();
  </script>
</html>
3 Likes