12 min read

In this article by Remo H. Jansen, author of the book Learning TypeScript, explains that in the early days of software development, developers used to write code with procedural programing languages. In procedural programming languages, the programs follow a top to bottom approach and the logic is wrapped with functions.

New styles of computer programming like modular programming or structured programming emerged when developers realized that procedural computer programs could not provide them with the desired level of abstraction, maintainability and reusability.

The development community created a series of recommended practices and design patterns to improve the level of abstraction and reusability of procedural programming languages but some of these guidelines required certain level of expertise. In order to facilitate the adherence to these guidelines, a new style of computer programming known as object-oriented programming (OOP) was created.

(For more resources related to this topic, see here.)

Developers quickly noticed some common OOP mistakes and came up with five rules that every OOP developer should follow to create a system that is easy to maintain and extend over time. These five rules are known as the SOLID principles. SOLID is an acronym introduced by Michael Feathers, which stands for the each following principles:

  • Single responsibility principle (SRP): This principle states that software component (function, class or module) should focus on one unique tasks (have only one responsibility).
  • Open/closed principle (OCP): This principle states that software entities should be designed with the application growth (new code) in mind (be open to extension), but the application growth should require the smaller amount of changes to the existing code as possible (be closed for modification).
  • Liskov substitution principle (LSP): This principle states that we should be able to replace a class in a program with another class as long as both classes implement the same interface. After replacing the class no other changes should be required and the program should continue to work as it did originally.
  • Interface segregation principle (ISP): This principle states that we should split interfaces which are very large (general-purpose interfaces) into smaller and more specific ones (many client-specific interfaces) so that clients will only have to know about the methods that are of interest to them.
  • Dependency inversion principle (DIP): This principle states that entities should depend on abstractions (interfaces) as opposed to depend on concretion (classes).

JavaScript does not support interfaces and most developers find its class support (prototypes) not intuitive. This may lead us to think that writing JavaScript code that adheres to the SOLID principles is not possible. However, with TypeScript we can write truly SOLID JavaScript.

In this article we will learn how to write TypeScript code that adheres to the SOLID principles so our applications are easy to maintain and extend over time. Let’s start by taking a look to interface and classes in TypeScript.

Interfaces

The feature that we will miss the most when developing large-scale web applications with JavaScript is probably interfaces. Following the SOLID principles can help us to improve the quality of our code and writing good code is a must when working on a large project. The problem is that if we attempt to follow the SOLID principles with JavaScript we will soon realize that without interfaces we will never be able to write truly OOP code that adheres to the SOLID principles. Fortunately for us, TypeScript features interfaces.

The Wikipedia’s definition of interfaces in OOP is:

In object-oriented languages, the term interface is often used to define an abstract type that contains no data or code, but defines behaviors as method signatures.

Implementing an interface can be understood as signing a contract. The interface is a contract and when we sign it (implement it) we must follow its rules. The interface rules are the signatures of the methods and properties and we must implement them.

Usually in OOP languages, a class can extend another class and implement one or more interfaces. On the other hand, an interface can implement one or more interfaces and cannot extend another class or interfaces. In TypeScript, interfaces doesn’t strictly follow this behavior. The main two differences are that in TypeScript:

  • An interface can extend another interface or class.
  • An interface can define data and behavior as opposed to only behavior.

An interface in TypeScript can be declared using the interface keyword:

interface IPerson {
greet(): void;
}

Classes

The support of Classes is another essential feature to write code that adheres to the SOLID principles. We can create classes in JavaScript using prototypes but its is not as trivial as it is in other OOP languages like Java or C#. The ECMAScript 6 (ES6) specification of JavaScript introduces native support for the class keyword but unfortunately ES6 is not compatible with many old browsers that still around. However, TypeScript features classes and allow us to use them today because can indicate to the compiler which version of JavaScript we would like to use (including ES3, ES5, and ES6).

Let’s start by declaring a simple class:

class Person implements Iperson {
public name : string;
public surname : string;
public email : string;
constructor(name : string, surname : string, email : string){
   this.email = email;
   this.name = name;
   this.surname = surname;
}
greet() {
   alert("Hi!");
}
}

var me : Person = new Person("Remo", "Jansen",
"[email protected]");

We use classes to represent the type of an object or entity. A class is composed of a name, attributes, and methods. The class above is named Person and contains three attributes or properties (name, surname, and email) and two methods (constructor and greet). The class attributes are used to describe the objects characteristics while the class methods are used to describe its behavior.

The class above uses the implements keyword to implement the IPerson interface. All the methods (greet) declared by the IPerson interface must be implemented by the Person class.

A constructor is an especial method used by the new keyword to create instances (also known as objects) of our class. We have declared a variable named me, which holds an instance of the class Person. The new keyword uses the Person class’s constructor to return an object which type is Person.

Single Responsibility Principle

This principle states that a software component (usually a class) should adhere to the Single Responsibility Principle (SRP). The Person class above represents a person including all its characteristics (attributes) and behaviors (methods).

Now, let’s add some email is validation logic to showcase the advantages of the SRP:

class Person {
public name : string;
public surname : string;
public email : string;
constructor(name : string, surname : string, email : string) {
   this.surname = surname;
   this.name = name;
   if(this.validateEmail(email)) {
     this.email = email;
   }
   else {
     throw new Error("Invalid email!");
   }
}
validateEmail() {
   var re = /S+@S+.S+/;
   return re.test(this.email);
}
greet() {
   alert("Hi! I'm " + this.name + ". You can reach me at " + this.email);
}
}

When an object doesn’t follow the SRP and it knows too much (has too many properties) or does too much (has too many methods) we say that the object is a God object. The preceding class Person is a God object because we have added a method named validateEmail that is not really related to the Person class behavior.

Deciding which attributes and methods should or should not be part of a class is a relatively subjective decision. If we spend some time analyzing our options we should be able to find a way to improve the design of our classes.

We can refactor the Person class by declaring an Email class, which is responsible for the e-mail validation and use it as an attribute in the Person class:

class Email {
public email : string;
constructor(email : string){
   if(this.validateEmail(email)) {
     this.email = email;
   }
   else {
     throw new Error("Invalid email!");
   }      
}
validateEmail(email : string) {
   var re = /S+@S+.S+/;
   return re.test(email);
}
}

Now that we have an Email class we can remove the responsibility of validating the e-mails from the Person class and update its email attribute to use the type Email instead of string.

class Person {
public name : string;
public surname : string;
public email : Email;
constructor(name : string, surname : string, email : Email){
   this.email = email;
   this.name = name;
   this.surname = surname;
}
greet() {
   alert("Hi!");
}
}

Making sure that a class has a single responsibility makes it easier to see what it does and how we can extend/improve it.

We can further improve our Person an Email classes by increasing the level of abstraction of our classes. For example, when we use the Email class we don’t really need to be aware of the existence of validateEmail method so this method could be private or internal (invisible from the outside of the Email class). As a result, the Email class would be much simpler to understand.

When we increase the level of abstraction of an object, we can say that we are encapsulating that object. Encapsulation is also known as information hiding.

For example, in the Email class allow us to use e-mails without having to worry about the e-mail validation because the class will deal with it for us. We can make this more clearly by using access modifiers (public or private) to flag as private all the class attributes and methods that we want to abstract from the usage of the Email class:

class Email {
private email : string;
constructor(email : string){
   if(this.validateEmail(email)) {
     this.email = email;
   }
   else {
     throw new Error("Invalid email!");
   }      
}
private validateEmail(email : string) {
   var re = /S+@S+.S+/;
   return re.test(email);
}
get():string {
   return this.email;
}
}

We can then simply use the Email class without explicitly perform any kind of validation:

var email = new Email("[email protected]");

Liskov Substitution Principle

Liskov Substitution Principle (LSP) states:

Subtypes must be substitutable for their base types.

Let’s take a look at an example to understand what this means.

We are going to declare a class which responsibility is to persist some objects into some kind of storage. We will start by declaring the following interface:

interface IPersistanceService {
save(entity : any) : number;
}

After declaring the IPersistanceService interface we can implement it. We will use cookies the storage for the application’s data:

class CookiePersitanceService implements IPersistanceService{
save(entity : any) : number {
   var id = Math.floor((Math.random() * 100) + 1);
   // Cookie persistance logic...
   return id;
}
}

We will continue by declaring a class named FavouritesController, which has a dependency on the IPersistanceService interface:

class FavouritesController {
private _persistanceService : IPersistanceService;
constructor(persistanceService : IPersistanceService) {
   this._persistanceService = persistanceService;
}
public saveAsFavourite(articleId : number) {
   return this._persistanceService.save(articleId);
}
}

We can finally create and instance of FavouritesController and pass an instance of CookiePersitanceService via its constructor.

var favController = new FavouritesController(new
CookiePersitanceService());

The LSP allows us to replace a dependency with another implementation as long as both implementations are based in the same base type. For example, we decide to stop using cookies as storage and use the HTML5 local storage API instead without having to worry about the FavouritesController code being affected by this change:

class LocalStoragePersitanceService implements IpersistanceService {
save(entity : any) : number {
   var id = Math.floor((Math.random() * 100) + 1);
   // Local storage persistance logic...
   return id;
}
}

We can then replace it without having to add any changes to the FavouritesController controller class:

var favController = new FavouritesController(new LocalStoragePersitanceService());

Interface Segregation Principle

In the previous example, our interface was IPersistanceService and it was implemented by the cases LocalStoragePersitanceService and CookiePersitanceService.

The interface was consumed by the class FavouritesController so we say that this class is a client of the IPersistanceService API.

Interface Segregation Principle (ISP) states that no client should be forced to depend on methods it does not use. To adhere to the ISP we need to keep in mind that when we declare the API (how two or more software components cooperate and exchange information with each other) of our application’s components the declaration of many client-specific interfaces is better than the declaration of one general-purpose interface.

Let’s take a look at an example. If we are designing an API to control all the elements in a vehicle (engine, radio, heating, navigation, lights, and so on) we could have one general-purpose interface, which allows controlling every single element of the vehicle:

interface IVehicle {
getSpeed() : number;
getVehicleType: string;
isTaxPayed() : boolean;
isLightsOn() : boolean;
isLightsOff() : boolean;
startEngine() : void;
acelerate() : number;
stopEngine() : void;
startRadio() : void;
playCd : void;
stopRadio() : void;
}

If a class has a dependency (client) in the IVehicle interface but it only wants to use the radio methods we would be facing a violation of the ISP because, as we have already learned, no client should be forced to depend on methods it does not use.

The solution is to split the IVehicle interface into many client-specific interfaces so our class can adhere to the ISP by depending only on Iradio:

interface IVehicle {
getSpeed() : number;
getVehicleType: string;
isTaxPayed() : boolean;
isLightsOn() : boolean;
}

interface ILights {
isLightsOn() : boolean;
isLightsOff() : boolean;
}

interface IRadio {
startRadio() : void;
playCd : void;
stopRadio() : void;
}

interface IEngine {
startEngine() : void;
acelerate() : number;
stopEngine() : void;
}

Dependency Inversion Principle

Dependency Inversion (DI) principle states that we should:

Depend upon Abstractions. Do not depend upon concretions

In the previous section, we implemented FavouritesController and we were able to replace an implementation of IPersistanceService with another without having to perform any additional change to FavouritesController. This was possible because we followed the DI principle as FavouritesController has a dependency on the IPersistanceService interface (abstractions) rather than LocalStoragePersitanceService class or CookiePersitanceService class (concretions).

The DI principle also allow us to use an inversion of control (IoC) container. An IoC container is a tool used to reduce the coupling between the components of an application.

Refer to Inversion of Control Containers and the Dependency Injection pattern by Martin Fowler at http://martinfowler.com/articles/injection.html. If you want to learn more about IoC.

Summary

In this article, we looked upon classes, interfaces, and the SOLID principles.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here