There are many concepts in the OOP paradigm. Inheritance is the most known, and it allows us to model software as relations between objects, just like in real life.
But it has some drawbacks, which alternative paradigms can fix. Let’s dive into OOP, inheritance, and everything in–between.
Object-oriented programming
Object-oriented programming, or OOP in short, is probably one of the most popular programming paradigms. It’s an easy paradigm to understand, because it follows the way we, humans, are seeing the world.
For us—the world is a collection of objects that can be modeled in software. Let’s say a car. A car consists of the engine, a set of tires, a chassis, a steering wheel, seats, etc. In code, this would look something like this:
class Engine {
function goVroom() {}
}
class Tire {}
class SteeringWheel {}
class Car {
private engine = new Engine();
private tires = [new Tire(), new Tire(), new Tire(), new Tire()];
private steeringWheel = new SteeringWheel();
}
Inheritance
We can’t talk about OOP without mentioning inheritance. Let’s look back at our car example. Say we want to output the parameters of our car. For the sake of simplicity, let’s omit the Engine
for now.
class Tire {
private radius: number;
public function getInfo() {
return `Tire Radius: ${this.radius}`;
}
get circumference() {
return 2 * Math.PI * this.radius;
}
}
class SteeringWheel {
private radius: number;
public function getInfo() {
return `Steering Wheel Radius: ${this.radius}`;
}
get circumference() {
return 2 * Math.PI * this.radius;
}
}
We can notice some repetition in there. If only there was a way to specify that both the Tire
and SteeringWheel
are forms of a Circular
object. Welcome—Inheritance.
abstract class CircularObject {
constructor(protected radius: number) {}
get circumference() {
return 2 * Math.PI * this.radius;
}
}
class Tire extends CircularObject {}
class SteeringWheel extends CircularObject {}
Inheritance allows us to define common behavior and functionality, as well as common interface. We can enforce, through inheritance, that all object that inherit from CircularObject
will have a radius
property, and a circumference
property, which is calculated. I’m not going to go too deep into inheritance in this post, though.
Where inheritance fails
Inheritance, however, has one big pitfall—multiple inheritance.
Let’s say we have the following code:
abstract class Fruit {}
class Plum extends Fruit {}
class Apricot extends Fruit {}
class Pluot extends Plum, Apricot {}
If you try to “compile” that code, you will get an error:
Classes can only extend a single class.(1174)
And it’s worth mentioning that this is not some weird constraint of a Typescript compiler. Apart from C++ (and apparently python), no other major language supports multiple inheritance. Why you ask? There is one problem with it.
The diamond problem
One of the biggest drawbacks of multiple inheritance, is that it can lead to one particular problem knows as—the diamond problem. Let’s try to visualize the above inheritance as a relations graph:
Fruit
/\
/ \
/ \
Plum Apricot
\ /
\ /
\/
Pluot
As you can see, this inheritance creates a diamond-like shape. The shape itself is not the problem, but the problem lies in an underlying implementation ambiguity one can introduce. Imagine that everything we inherit from Fruit
, needs to implement an abstract method called getColor
:
abstract class Fruit {
public abstract getColor(): string;
}
class Plum extends Fruit {
public getColor() {
return 'purple';
}
}
class Apricot extends Fruit {
public getColor() {
return 'yellow-orangish';
}
}
class Pluot extends Plum, Apricot {}
Since Pluot
extends non-abstract classes, it is not obliged to implement the getColor
method. Assuming multiple inheritance was possible in Typescript, what would be the output of:
const pluot = new Pluot();
console.log(pluot.getColor());
It’s impossible to know. This ambiguity is hard to mitigate. In complied language—it’s usually forbidden at the compilation level, either via explicit error during class inheritance definition, or during invocation. The latter enforces the invocation to be explicitly cast to a specific type, for example:
Pluot pluot = new Pluot();
System.out.println((Apricot)pluot.getColor()); // yellow-orangish
Another approach some languages take—is a compile time requirement to re-implement the method in the grand-child class, thus eliminating the ambiguity.
However, in case of dynamic languages—nothing guards your types. In–fact, the JavaScript runtime doesn’t know about your Apricot
and Pluot
objects. All it sees are Object
types with a function on their prototypes. In this case, Typescripts compiler is the one guarding you. And Typescript, as well all know, is just a syntactic sugar on top of JavaScript.
But what about interfaces?
Apart from abstract classes that can contain both interface and functionality, there are also what is known as Interfaces or Protocols. An interface is just a definition of an agreed upon API. It defines the API, but does not allow implementation in the body of the interface. It’s up to the object that implements the interface—to provide the actual implementation.
Therefor, it’s possible to create a diamond like relation graph with interfaces. Consider the following example:
type Target = ...;
interface Clickable {
getTarget(): Target;
}
interface Moveable {
getTarget(): Target;
}
class Ball implement Clickable, Moveable {
getTarget() {
const target = ...;
return target;
}
}
Since neither Clickable
, nor Moveable
provide the actual definition, it is perfectly fine that the ambiguity exists, and it does not create any problems, since the compiler has no confusion.
The same is true for this scenario:
interface Moveable {
move();
}
class Ball {
move() {
...
}
}
class Basketball extends Ball implements Moveable {
}
console.log((new Basketball()).move());
This code complies and runs perfectly, because from the view point of the compiler, Basketball
conforms to the interface Moveable
since it inherits the move
method from Ball
.
There are some more interesting cases, such as: what happens if the signature of move
in the Moveable
interface—is different from the move
in Ball
? Would you be able to both extend from Ball
and implement Moveable
? Which version of move
then will be called?
I don’t want to spend more time on OOP and inheritance. Drop me a message/email (contact details down below this post) if you are interested to read more about those weird cases in Typescript. For now, let’s move on.
Mixins
As we’ve seen earlier, multiple inheritance is a big no–no. However, there are legit cases where we want to inherit functionality from multiple classes. A great example is the game development industry. It’s common to think of game objects as, well, objects. Let’s say we are trying to build a modular system of NPCs (Non Playable Character—represents a game character that is controller by the game/AI and not by the player).
We have some NPCs that can move. Some NPCs can take damage. Not all NPCs are going to take damage. The local town potion seller—should not be attackable by the player. An evil orc or mimic chest—should be able to take damage. The chest however can’t move, because it’s, well, a chest.
I bet in your head you already have an inheritance graph. Let’s try to model it.
class Moveable {
constructor(private positionX: number, private positionY: number) {}
public moveTo(x: number, y: number) {
this.positionX = x;
this.positionY = y;
}
}
class TakesDamage {
constructor(private health: number) {}
public takeDamage(damage: number) {
this.health -= damage;
}
public isDead() {
return this.health <= 0;
}
}
class MimicChest extends TakesDamage {}
class PotionsSeller extends Moveable {}
class EvilOrc extends TakesDamage, Moveable {}
Oh-no! The evil orc is trying to do multiple inheritance. Even though it does not introduce any ambiguity, like we’ve seen before, multiple inheritance is still forbidden. What can we do?
Meet—Mixins. Mixin is a way to have reusable code, like our Moveable
and TakesDamage
classes, without having the end class to inherit both, hence avoiding the diamond problem.
A common way to implement a mixin, is by creating functions that return an anonymous class. In our case, it will look something like this:
type Constructor = new (...args: any[]) => {};
function Moveable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
_positionX = 0;
_positionY = 0;
moveTo(x: number, y: number) {
this._positionX = x;
this._positionY = y;
}
get x() {
return this._positionX;
}
get y() {
return this._positionY;
}
};
}
function TakesDamage<TBase extends Constructor>(Base: TBase) {
return class extends Base {
_health = 0;
takeDamage(damage: number) {
this._health -= damage;
}
get health() {
return this._health;
}
get isDead() {
return this._health <= 0;
}
};
}
class NonPlayableCharacter {
constructor(private name: string) {}
getName() {
return this.name;
}
}
const EvilOrc = Moveable(TakesDamage(NonPlayableCharacter));
const orc = new EvilOrc("sluggish");
orc._health = 10;
console.log(`Orc ${orc.getName()} is located at ${orc.x},${orc.y}`);
orc.takeDamage(20);
console.log(
`Orc ${orc.getName()} took 20 points of damage and is now ${
orc.isDead ? "dead" : "still alive"
}`,
);
The output of this program will be:
Orc sluggish is located at 0,0
Orc sluggish took 20 points of damage and is now dead
And as you can see, our EvilOrc
class was able to inherit from two classes! There are some drawbacks, as you probably noticed. For one—any mixin must have a generic constructor with ...args
. Attempt to add a non-generic constructor to a mixin will result in error TS2545
that says:
A mixin class must have a constructor with a single rest parameter of type ‘any[]’.
So all the benefits of hidden fields, or initialization—are thrown away. One way to at least remove some code duplication, is to have factory methods:
type Vector2D = { x: number; y: number };
function createOrc(name: string, initialHealth: number, initialPos: Vector2D) {
const orc = new EvilOrc(name);
orc._positionX = initialPos.x;
orc._positionY = initialPos.y;
orc._health = initialHealth;
return orc;
}
const orc = createOrc("sluggish", 10, { x: 10, y: 10 });
But data access is still not protected. Fields can’t be marked as private, because then you lose access to initialize them, and need to provide a setter method—which in this case beats the entire purpose of making the field private in the first place.
However, if you have common functionality that you want to share to between multiple objects, and don’t care much about encapsulating the properties—then mixins are a great way to do that.
As we’ve seen, with mixins you lose the encapsulation, and more over, you can encounter weird bugs like prototype overrides! Consider the following example:
type Constructor = new (...args: any[]) => {};
function Plum<TBase extends Constructor>(Base: TBase) {
return class extends Base {
getColor() {
return "purple";
}
};
}
function Apricot<TBase extends Constructor>(Base: TBase) {
return class extends Base {
getColor() {
return "yellow-orangish";
}
};
}
class BasePluot {}
const Pluot = Plum(Apricot(BasePluot));
const p = new Pluot();
console.log(p.getColor());
What would be the output? The correct answer is purple
. However, if we switch the order if the mixin composition from Plum(Apricot(BasePluot));
to Apricot(Plum(BasePluot));
, the output now changes to yellow-orangish
!
And what if we want to define a color for our Pluot
?
class BasePluot {
getColor() {
return "red";
}
}
You won’t be able to access it, since mixins are based on prototype overriding.
Last, but not least—have you noticed that we need to have 2 classes for every object we want to construct? In order to create a Pluot
, which is composed of Apricot
and Plum
, we need to have both the BasePluot
class, and the result of the mixin composition call. And how the heck do we create an instance of Plum or Apricot? That’s how:
class ApricotBase {}
const ApricotClass = Apricot(ApricotBase);
const a = new ApricotClass();
Such a mess. Imagine testing a mixin, you will need to define a dummy class just to be able to instantiate the mixin itself. Mixins are better reserved for small pieces of reusable code that needs to be composed. In a real scenario, like a game—they are hard to maintain.
Composition
Lastly, we come to Composition. Composition, as the name implies, is used to compose objects from reusable code pieces, rather than leveraging inheritance. Mixin is a form of composition, but it still relies on inheritance to combine shared functionality into one class.
You’ve probably used composition many times in your development, without even knowing it. The Car
example at the top of the article uses composition, to compose one specific object—the Car
, using other reusable objects like Tire
, Engine
, etc. But this composition approach does not help us with our EvilOrc
example. We could do something like this:
class Moveable {
...
moveTo(pos: Vector2D) {
...
}
}
class EvilOrc {
private moveable = new Moveable();
moveTo(pos: Vector2D) {
this.moveable.moveTo(pos);
}
}
But it’s ugly, and repetitive.
When building an architecture around composition, we no longer think of objects and their properties—as inheritance tree. We don’t say that an Orc is a Moveable object, and an object that can take damage. Instead, we say that an Orc has the ability to move, and to take damage. It’s a small change in wording, but a totally different concept. Let’s try to redefine our Moveable
and TakesDamage
classes, to better suit composition.
First, we need to define some utility types:
abstract class Component {}
type ComponentFunction = Function;
type ComponentClass<T extends Component> = new (...args: any[]) => T;
With composition, we need to distinguish between the composable objects, and the composed objects. In our case, Moveable
and TakesDamage
are reusable classes that have no meaning outside a context of a game object. We can create an instance of Moveable
class, but it has no meaning by itself. Therefor, we will separate those containers of common logic, into what I’ll call a Component
. Hence, the abstract Component
class. The two types are used for some type inference, and their role will be clear in a bit.
Next, let’s rewrite our Moveable
and TakesDamage
as components that will be composed, and at the same time, rename TakesDamage
to Health
:
class MoveableComponent extends Component {
constructor(private position: Vector2D) {
super();
}
public moveTo(pos: Vector2D) {
this.position = pos;
}
}
class HealthComponent extends Component {
constructor(private health: number) {
super();
}
public takeDamage(damage: number) {
this.health -= damage;
}
public isDead() {
return this.health <= 0;
}
}
Now, we marked some classes as components. Before we can finally define our EvilOrc
class, we need one more utility class:
abstract class GameObject {
private components = new Map<ComponentFunction, Component>();
protected addComponent(component: Component) {
this.components.set(component.constructor, component);
}
hasComponents(components: Iterable<ComponentFunction>): boolean {
for (const c of components) {
if (!this.components.has(c)) {
return false;
}
}
return true;
}
getComponent<T extends Component>(componentClass: ComponentClass<T>): T {
return this.components.get(componentClass) as T;
}
}
This class will serve as the base class for all our game objects, and it has a list of components. Hence, composing them into a game object. Here, the types ComponentFunction
and ComponentClass
are used for some Typescript magic.
Lastly, we need to define our EvilOrc
and MimicChest
:
class EvilOrc extends GameObject {
constructor(initialPosition: Vector2D, initialHealth: number) {
super();
this.addComponent(new MoveableComponent(initialPosition));
this.addComponent(new HealthComponent(initialHealth));
}
}
class MimicChest extends GameObject {
constructor(initialHealth: number) {
super();
this.addComponent(new HealthComponent(initialHealth));
}
}
And voilà! As I’ve said earlier: EvilOrc
is not a Moveable
and Health
object. Instead, EvilOrc
has the ability to move, and has health (and hence—ability to take damage, in our case).
Okay—you say. But what do I do with this? How do I operate on this? Let’s say we are trying to build a sequence that will perform an attack by the player, on a NPC. This might look something like this:
function performAttack(go: GameObject, damage: number) {
if (!go.hasComponents([HealthComponent])) {
throw new Error("game object does not have health component");
}
const h = g.getComponent(HealthComponent);
if (h.isDead()) {
throw new Error("game object already dead");
}
h.takeDamage(damage);
}
It’s a very modular system that allows you to compose all kinds of behavior. It’s widely used in game development, and is knows as ECS—Entity Component System. I was never able to find use-cases for it outside of game development, but recently I had the chance to use a similar implementation at my work. I thought it’s worth sharing this with you, and maybe you can find a good use cases for it as well.
Imagine for example that you want to build a highly reliable weather service. It has to depend on multiple external weather APIs, because you want to fallback to a different API if one is down, or takes too much time to answer. But not all weather APIs might support the features you need. One might support temperature and wind conditions; another one will support temperature and precipitation; while the last might support all of them: temperature, wind condition, and precipitation. This example might be a prefect use case for the aforementioned composition. The implementation of such system, using composition—will be clean and easy to maintain.
Remember—good developers have a lot of tools, and try to use the best one for each scenario. Happy coding!