Alchemist image

Alchemist

A fast, lightweight, zero-dependency ts/js package designed to restore the rightful throne to the most powerful and elegant aspect of OOP: multiple inheritance.

Simple, flexible, and ready to roll!

What’s included:

What’s not included yet:

Motivation

Show

Multiple inheritance is the most powerful pattern in object-oriented programming (OOP).

Of course, like any other weapon, it can be dangerous. Sure, you could accidentally shoot yourself with it.

That’s why the creators of many modern programming languages decided, “Hey, let’s take away this dangerous toy from the user, and just to be safe, let’s strap them into a straightjacket!”

Guys, that doesn’t work! Single inheritance — the one thing users (for now) still have — often leads to ugly application architecture. It turns what could’ve been a reasonable representation of reality using classes into the nightmare of a binge-drinking stranger.

 

If I met the owner of an airline in a flophouse, and he offered me to create an app for his company, the conversation might go like this:

 

Him: Hey, you seem like a smart person. How about writing an app for my airline?

Me: Well, that’s an unexpected offer, but it sounds interesting. What kind of app do you need?

Him: I want something better than what my competitors have. It should be simple, user-friendly, and, of course, cool!

 

In the morning, hungover, I remember this conversation and start brainstorming the architecture.

Here’s how we can describe the airplane crew for our app:

  • Captain (First Pilot)
    • Duties: Scold passengers and turn on autopilot.
      (You know, the serious stuff. Someone’s gotta do it!).
  • Co-Pilot
    • Skills: Play music and use GPS.
      (Why GPS? Well, at least it’s handy for quickly finding a pub in any city!)
  • Flight Engineer
    • Talents: Tell jokes and wake up the rest of the crew after landing.
      (Because old jokes won’t laugh at themselves, right?)

Describing this with classes is easier than taking candy from a baby:

class Captain {
  scoldPassengers() { return 'This? This is me being polite. Wait till I actually start!'; };
  turnOnAutopilot() { return 'Does anyone know where the damn button is?'; };
}

class CoPilot {
  playMusic() { return 'Oh, come on, my singing’s fine.'; };
  useGPS() { return 'The GPS isn’t working: I left my phone somewhere.'; };
}

class FlightEngineer {
  tellJokes() { return 'So, an Irishman walks into a pub…'; };
  wakeUpCrew() { return 'First, someone needs to wake me up.'; };
}

"Perfect!" says the company owner after looking at the result of our work. "Just one small comment. You see, my airline isn’t exactly in the top ten global leaders (by the way, will $5 be enough for your work?). So, not all flights have three crew members. For some routes, I hire just one person who can handle all the crew’s duties."

And that’s when we realize our app is heading for a plane crash! Implementing such a simple and natural thing without multiple inheritance... damn, it’d be easier to explain to my mom why I’m still single!

But we’re not here to talk about the problems. We’re here to talk about the solution.

Fortunately, neither our earnings nor our reputation are at risk: the Alchemist package brings simplicity back to these simple things:

Simple example

import { alchemize } from '@lenka/alchemist';

class Captain {
  scoldPassengers() { return 'This? This is me being polite. Wait till I actually start!'; };
  turnOnAutopilot() { return 'Does anyone know where the damn button is?'; };
}

class CoPilot {
  playMusic() { return 'Oh, come on, my singing’s fine.'; };
  useGPS() { return 'The GPS isn’t working: I left my phone somewhere.'; };
}

class FlightEngineer {
  tellJokes() { return 'So, an Irishman walks into a pub…'; };
  wakeUpCrew() { return 'First, someone needs to wake me up.'; };
}

const SuperHero = alchemize(Captain, CoPilot, FlightEngineer);

const superman = new SuperHero();

superman.scoldPassengers();
superman.playMusic();
superman.tellJokes();
// ...

And that’s it!

So far, nothing too complicated, right?

But there’s still something unclear.

alchemize() is supposed to combine several classes into one. But how is it supposed to do that?

For example, the original classes take some parameters in their constructors:

class Volume {
  constructor(private length: number, private width: number, private height: number) {}

  calculateVolume(): number {
    return this.length * this.width * this.height;
  }
}

class Person {
  constructor(private firstName: string, private lastName: string) {}

  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}
class Volume {
  #length;
  #width;
  #height;

  constructor(length, width, height) {
    this.#length = length;
    this.#width = width;
    this.#height = height;
  }

  calculateVolume() {
    return this.#length * this.#width * this.#height;
  }
}

class Person {
  #firstName;
  #lastName;
  constructor(firstName, lastName) {
    this.#firstName = firstName;
    this.#lastName = lastName;
  }

  get fullName() {
    return `${this.#firstName} ${this.#lastName}`;
  }
}

Obviously, our combined class should also take all those parameters:

const VolumeOfPerson = alchemize(Volume, Person);

const instance = new VolumeOfPerson(25, 18, 38, 'John', 'Dow');

But how will it know what to do with them?

So, what do we usually do in plain old JavaScript when working with single inheritance? In the constructor of the derived class, we call the constructor of the base class (using super(...)) and pass it some of the arguments — the ones it needs for its initialization.

alchemize(...) works in a pretty similar way. By default, when you create an instance (using new Combined(...)), it initializes all the input classes and passes all the received parameters to each constructor.

Obviously, this isn’t always the correct behavior.

This is where recipes come to the rescue, helping us solve this and other challenges. Let’s dive right into them!

Recipes

Recipe image

To create something extraordinary (a panacea, the sorcerer’s stone, or grandma’s apple pie), every alchemist needs a recipe.

In the Alchemist package, recipe(...) is a function that takes a set of instructions and returns an object with a configured alchemize(...) function:

import { recipe, Recipe } from '@lenka/alchemist';

// For your convenience, Alchemize offers a handy helper
// type called Recipe.
const prescription: Recipe = { instanceOfSupport: true };

class SomeClassA {};
class SomeClassB {};

// You can either chain the call like this...
const CombinedClass =
  recipe(prescription).alchemize(SomeClassA, SomeClassB);

// ...or you can save the result into a variable if you plan
// to reuse the same settings in multiple places and call
// alchemize later:
const tunedAlchemize = recipe(prescription).alchemize;

const TheSameCombinedClass =
  tunedAlchemize(SomeClassA, SomeClassB);

const instance = new CombinedClass(...);
import { recipe } from '@lenka/alchemist';

const prescription = { instanceOfSupport: true };

class SomeClassA {};
class SomeClassB {};

// You can either chain the call like this...
const CombinedClass =
  recipe(prescription).alchemize(SomeClassA, SomeClassB);

// ...or you can save the result into a variable if you plan
// to reuse the same settings in multiple places and call
// alchemize later:
const tunedAlchemize = recipe(prescription).alchemize;

const TheSameCombinedClass =
  tunedAlchemize(SomeClassA, SomeClassB);

const instance = new CombinedClass(...);

recipe(...) takes a single parameter – an object with two optional keys: