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.
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!
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>