About object-oriented design and the “class” & “extends” keywords in TypeScript / ES6

A few weeks ago I found an interesting article titled In Defense of JavaScript Classes. The article exposed some concerns about the class keyword in ES6 / TypeScript:

These days it feels like everyone is attacking classes in JavaScript. Developers I respect say ES6 classes are a virus. We’ve compiled long lists on the reasons that ES6 classes are not awesome. Apparently, if we’re still brave enough to try them, we need advice on how to use classes and still sleep at night.

The problems that I see with the class and extends keywords in ES6 / TypeScript are not something new. I believe that these problems are cause by bad object-oriented (OO) design and I’m sure that the source of most of the criticism is coming from programmers with a strong interest in functional programming and I understand their fears. They are afraid of some of the OOP “monsters”:

I’m going to write a really small TypeScript application to showcase some of these “monsters” and try to explain how to “fight” them.

ts.png

Let’s imagine that our business domain has three entities: Samurai, Sword and Material.

We also have some business rules:

 Inheritance

Dan Abramov wrote the following in his article How to Use Classes and Sleep at Night:

Classes encourage inheritance but you should prefer composition.

When we don’t follow this recommendation we end up with really bad code like the following:

class Material {
    public name: string;
}

class Iron extends Material {
    constructor() {
        super();
        this.name = "iron";
    }
}

class Wood extends Material {
    constructor() {
        super();
        this.name = "wood";
    }
}

class Sword {
    material: Material;
}

class Katana extends Sword {
    constructor() {
        super();
        this.material = new Iron();
    }
}

class Bokken extends Sword {
    constructor() {
        super();
        this.material = new Iron();
    }
}

class Samurai {
    public sword: Sword;
}

class SamuraiMaster extends Samurai {
    constructor() {
        super();
        this.sword = new Katana();
    }
}

class SamuraiStudent extends Samurai {
    constructor() {
        super();
        this.sword = new Bokken();
    }
}

let master = new SamuraiMaster();
let student = new SamuraiStudent();

The preceding code snippet is really wrong because it doesn’t follow the core OOP principles:

We should be very careful with the extends keyword. Because when a class extends another we are coupling the two forever as inheritance is the strongest kind of coupling that we can encounter.

However, I believe that is not a bad thing that the extends keyword is available. We just need to be very careful and use it only in very limited cases.

 Internal class state

Let’s imagine that a material has a level of endurance from 0 to 100:

class Sword {
    material: Material;
    constructor(material: Material) {
        this.material =  material;
    }
    use() {
       if (this.material.endurance > 0) {
           this.material.endurance = this.material.endurance - 10;
           return this.material.power;
       }
       return 0;
    }
}

As we use a weapon its endurance will decrease. The endurance is part of the class internal state. The problem of internal state in classes is that predicting the output of its method becomes a difficult task. Being able to predict the output of a method is important because predictability and testability are directly proportional.

For example, let imagine that you are looking to a piece of code in the application that invokes the use method:

function doSomething(weapon: IWeapon) {
   return weapon.use(); // Will it cause any damage?
}

We need to now the class internal state to be able to predict the value returned by its methods. We can re-write the method as a pure function:

function use(endurance, power) {
       if (endurance > 0) {
           endurance = endurance - 10;
           return power;
       }
       return 0;
    }

Pure functions/methods don’t use the internal class state. This makes much easier to predict their return type:

function doSomething(weapon: IWeapon, material: IMaterial) {
   return weapon.use(material.endurance, material.power); 
}

In real live the classes would have some methods. We should try to be as declarative as possible when implementing these methods. How can you be more declarative? You need to learn about functional programming and use libraries like Ramda .

flat,800x800,075,f.jpg

Let’s take a look to an example. The following code filters a list of tasks by one of its properties to find the list of incomplete tasks:

// Plain JS
var incompleteTasks = tasks.filter(function(task) {
    return !task.complete;
});

// Lo-Dash
var incompleteTasks = _.filter(tasks, {complete: false});

Both examples mention the actual data collection tasks. We have declared how to iterate the items and filter each item (imperative) as opposed to declare what we want the code to do (declarative). With Ramda it would look as the following:

let getIncomplete = R.filter(R.where({complete: false});

As we can see the data collection is not mentioned and the API is much more declarative. This makes this function highly re-usable.

We should also try to declare “pure methods”. In other words, we should declare methods that are independent of the internal class state (properties) if possible. If we can’t do this we should try to make our classes immutable when possible. This means that the properties of a class will not change after an instance has been created.

 Object composition

The following example adheres to the composite reuse and SOLID principles:

class Material {
    public name: string;
    constructor(string) {
        this.name = string;
    }
}

class Sword {
    material: Material;
    constructor(material: Material) {
        this.material =  material;
    }
}

class Samurai {
    public sword: Sword;
    constructor(sword: Sword) {
        this.sword = sword;
    }
}

function getSamurai(rank) {
    if(rank === "master") {
        return new Samurai(new Sword(new Material("iron")));
    } else {
         return new Samurai(new Sword(new Material("wood")));
    }
}

The problem with the preceding code snippet is that the getSamurai function is in charge of the object composition and it uses an imperative style.

The getSamurai function can be easily broken by small changes. Let’s try to imagine that we have a new requirement:

It is just a small change but the function will become even more imperative and harder to maintain:

function getSamurai(rank) {
    if (rank === "master") {
        return new Samurai(new Sword(new Material("iron")));
    } else if (rank === "general"){
        return new Samurai(new Sword(new Material("gold")));
    } else {
         return new Samurai(new Sword(new Material("wood")));
    }
}

This is one of the main reasons to use an IoC container. An IoC container is a tool that helps us to avoid coupling and control the way object composition works in our application. In this section I’m going to use InversifyJS.

logo.png

We are going to start by declaring the types available in our application as string literals. We do this because the typescript types are not available at run-time but we need them to identify the dependencies of a class:

let TYPES = {
    materialName: "materialName",
    IMaterial: "IMaterial",
    ISword: "ISword",
    ISamurai: "ISamurai"
};

We can then declare some interfaces:

interface IMaterial {
    name: string;
}

interface ISword {
    material: IMaterial;
}

interface ISamurai {
    sword: ISword;
}

At this point we can declare our classes:

@injectable()
class Material implements IMaterial {
    public name: string;
    constructor(@inject(TYPES.materialName) string) {
        this.name = string;
    }
}

@injectable()
class Sword implements ISword {
    material: IMaterial;
    constructor(@inject(TYPES.IMaterial) material: IMaterial) {
        this.material =  material;
    }
}

@injectable()
class Samurai implements ISamurai {
    public sword: ISword;
    constructor(@inject(TYPES.ISword) sword: ISword) {
        this.sword = sword;
    }
}

Now that our entities are defined, it is time to use our IoC container to declare some rules that will define the way the object composition works in our application:

let kernel = new Kernel();

kernel.bind<ISword>(TYPES.ISword).to(Sword);
kernel.bind<ISamurai>(TYPES.ISamurai).to(Samurai);
kernel.bind<IMaterial>(TYPES.IMaterial).to(Samurai);

kernel.bind<string>(TYPES.materialName)
      .toConstantValue("iron")
      .whenAnyAncestorTagged("rank", "master");

kernel.bind<string>(TYPES.materialName)
      .toConstantValue("wood")
      .whenAnyAncestorTagged("rank", "student");

I believe that the preceding code snippet is much more maintainable than:

function getSamurai(rank) {
    if (rank === "master") {
        return new Samurai(new Sword(new Material("iron")));
    } else {
         return new Samurai(new Sword(new Material("wood")));
    }
}

It is also a bit more verbose, but I prefer maintainable/verbose over unmaintainable/concise. Of course, this is just my opinion…

Let’s imagine once more that we have a new requirement:

We can just add a new contextual binding:

kernel.bind<string>(TYPES.materialName)
      .toConstantValue("iron")
      .whenAnyAncestorTagged("rank", "general");

The nice thing about this is that we don’t need to modify the existing code! At this point you can create a new samurai master, student or general and it will own a sword with the right material.

let general = kernel.getTagged<ISamurai>(
    TYPES.ISamurai, "rank", "general");

I would also like to mention that we are working an alternative binding API for InversifyJS based on decorators. This API is even more declarative. Instead of declaring the bindings using the fluent syntax:

kernel.bind<IMaterial>(TYPES.IMaterial)
      .to(Wood)
      .whenAnyAncestorTagged("rank", "student");

We can declare bindings using decorators:

@provideWhenAnyAncestorTagged(TYPES.IMaterial, "rank", "student")
class Wood {
    // ...

We could implement the application as follows:

@provideWhenAnyAncestorTagged(TYPES.IMaterial, "rank", "master")
class Iron implements IMaterial {
    // ....
}

@provideWhenAnyAncestorTagged(TYPES.IMaterial, "rank", "student")
class Wood implements IMaterial {
    // ....
}

@provide(TYPES.ISword)
class Sword implements ISword {
    material: IMaterial;
    constructor(@inject(TYPES.IMaterial) material: IMaterial) {
        this.material =  material;
    }
}

@provide(TYPES.ISamurai)
class Samurai implements ISamurai {
    public sword: ISword;
    constructor(@inject(TYPES.ISword) sword: ISword) {
        this.sword = sword;
    }
}

Let’s imagine that the above is our code and we are asked to introduce the “general rule” once more. We could solve this problem by adding a new material and no other changes would be required in the entire application:

@provideWhenAnyAncestorTagged(TYPES.IMaterial, "rank", "general")
class Gold implements IMaterial {
    // ....
}

 Summary

I like the new JavaScript/TypeScript OOP features introduced by ES6 but we need to be careful with them (just like in any other OOP language…).

If you want to write good OO code I encourage you to:

I believe that an IoC container can help us to write better OO code and that is why I’ve been working on InversifyJS.

Please feel free to share thoughts about this article with us via @OweR_ReLoaDeD, @WolkSoftwareLtd or @InversifyJS.

Don’t forget to subscribe if you don’t want to miss out future articles!

 
87
Kudos
 
87
Kudos

Now read this

Working with React and TypeScript

An introduction to the development of React applications with Atom and TypeScript We are about to develop the famous TODO App from the TodoMVC project using React and TypeScript: In this post you will learn about the following: 1.... Continue →