Skip to content

A learning material consisting of all five(5) solid design patterns with examples written in javascript and typescript.

Notifications You must be signed in to change notification settings

Caleb-Mantey/solid-design-principles-in-js

Repository files navigation

SOLID Design Principles

One fundamental software principle is the SOLID PRINCIPLE which is usually referred to as the first five principles of object oriented design. This principle was formulated by Robert C. Martin (also known as Uncle Bob). In this article we will be using javascript to explain certain concepts. Javascript doesn't support features like interfaces and abstract classes but with the addition of typescript we can write javascript like we do in other languages like c# and java. So in this article we will be using typescript too.

Solid principles helps in reducing tight coupling between classes in our code. Tight coupling is when a group of classes highly depend on one another. Loose coupling is the opposite of tight coupling and this approach makes our code more reusable, readable, flexible, stable and maintainable. It is advisable to avoid tight coupling as much as possible and always make your code as loosely coupled as possible.

SOLID stands for

Now let's break each of the principles down and get a better understanding of each of these principles.

Single Responsibility Principle

view full source code

Single responsibility principle states that

A class should have one and only one responsibility. Which means your class should have only one job.

Consider this example You have a mailer class that connects to an smtp service, takes an email processes and sends the email as either text or html. Now Lets see what this class does to get the job done.

    class Mailer{
        constructor(mail){
            this.mail = mail
            this.smtpService = this.smtp_service_connection()
        }

        smtp_service_connection(){
            // Connects to smtp service
        }

        send(){
            this.smtpService.send(this.format_text_mail())
             this.smtpService.send(this.format_html_mail())
        }

        format_text_mail(){
            // formats to text version of mail
            this.mail = "Email For You \n" + this.mail;

            return this.mail;
        }

        format_html_mail(){
            // formats to html version of mail
             this.mail = `<html>
            <head><title>Email For You</title></head>
            <body>${this.mail}</body>
            </html>`;

            return this.mail;
        }
    }

    const mailer = new Mailer("hello kwame");
    mailer.send();

This code does not follow the single responsibility principle the mailer class is responsible for doing all the following

  • Connects to an smtp service
  • Format the mail in text format
  • Format the mail in html format
  • Sending the mail

This will make the Mailer class very difficult to maintain. Let's say for example we want to change the smtp provider we are using we will have to come into this class and do some changes to the smtp_service_connection method and this can get tricky and messy if the new provider does not implement a send method but a deliver method then we will have to also come and change this line this.smtpService.send(this.format_html_mail()) in our send method to this.smtpService.deliver(this.format_html_mail()). All these is a result of the fact that our class is not performing only one functionality.

Better Approach

Mailer

    class Mailer{
        constructor(mail, mailerFormats){
            this.mail = mail
            this.mailerFormats = mailerFormats
            this.smtpService = new MailerSmtpService()
        }

        send(){
            // Loops through mail formats and calls the send method
            this.mailerFormats.forEach((formatter) => this.smtpService.send(formatter.format(this.mail)))
        }
    }

MailerSmtpService

    class MailerSmtpService{
        constructor(){
           this.smtp_con = this.smtp_service_connection()
        }

        send (mail){
            this.smtp_con.send(mail)
            // can easily change to be this if a service requires this implementation - smtp_con.deliver(mail)
        }

        smtp_service_connection(){
            // Connects to smtp service
        }
    }

HtmlFormatter

    class HtmlFormatter{
        constructor(){
        }

        format(mail){
             // formats to html version of mail
              mail = `<html>
            <head><title>Email For You</title></head>
            <body>${mail}</body>
            </html>`;

            return mail;
        }
    }

TextFormatter

    class TextFormatter{
        constructor(){
        }

        format(mail){
             // formats to text version of mail
             mail = "Email For You \n" + mail;

            return mail;
        }
    }

A more better approach is seen above where we divide all the task into separate classes. We now have the following.

  • A class that connects to the smtp service (MailerSmtpService)
  • A class that formats our mail in text (TextFormatter)
  • A class that formats our mail in html (HtmlFormatter)
  • A class responsible for sending the mail (Mailer)

You can see now the code looks better and our smtp service can be changed easily in only one class which does not affect the other parts of the mailing systems behavior. If we use a new smtp service and it implements a deliver method instead of a send method then we only have to change one method (we change this.smtp_con.send(mail) to this.smtp_con.deliver(mail)) in the MailerSmtpService class. This will not affect other parts of our application and our app will still function properly. The Mailer class takes an instance of an smtp service and only sends a mail (NOTE: It is performing one and only one job to send mail)

Also our HtmlFormatter and TextFormatter are doing just one thing formatting the mail in the right format.

Now we can send an email by simply doing this

    const mailer = new Mailer("hello kwame", [new HtmlFormatter(), new TextFormatter()])
    mailer.send();

Open-closed Principle

view full source code

This principle states that a class must be open for extension but close for modification.

This principle focus on the fact that the class must be easily extended without changing the contents of the class. If we follow this principle well we can actually change the behavior of our class without ever touching any original piece of code. This also means if a Developer named Fred works on a certain feature and another Developer named Kwame wants to add some changes, then Kwame should be able to do that easily by extending on the features Fred has already provided.

Lets take an example from our MailerSmtpService class in the first example and lets make it support this principle.

MailerSmtpService - ( Initial )

This is our initial implementation for the MailerSmtpService. Nothing fancy here yet

    class MailerSmtpService{
        constructor(){
           this.smtp_con = this.smtp_service_connection()
        }

        send (mail){
            this.smtp_con.send(mail)
            // can also be this.smtp_con.deliver(mail)
        }

        smtp_service_connection(){
            // Connects to smtp service
        }
    }

MailerSmtpService - ( Enhanced )

To support the open-closed principle we remove the smtp_service_connection method from our MailerSmtpService class and rather we pass the method through a constructor method, then in a subclass (PostMarkSmtpService and SendGridSmtpService) that inherits from MailerSmtpService we call the constructor method of the base class with super(() => {}) and we pass a method which handles the smtp connection depending on smtp provider in use.

    class MailerSmtpService{
        constructor(smtp_connection = () => {
            //connects to default smtp service
        }){
           this.smtp_con = smtp_connection()
        }

        send (mail){
            this.smtp_con.send(mail)
        }
    }

PostMarkSmtpService

    class PostMarkSmtpService extends MailerSmtpService {
        constructor(){
           super(() => {
                // Connects to postmark smtp service
            })
        }

        send (mail){
            this.smtp_con.send(mail)
            // can also be this.smtp_con.deliver(mail)
        }
    }

SendGridSmtpService

    class SendGridSmtpService extends MailerSmtpService {
        constructor(){
            super(() => {
                // Connects to sendgrid smtp service
            })
        }

        send (mail){
            this.smtp_con.send(mail)
            // can also be this.smtp_con.deliver(mail)
        }
    }

In our mailer class we can now create a new PostMarkSmtpService or SendGridSmtpService in our app and we can easily keep extending to support different smtp service.

    class Mailer{
        constructor(mail, mailerFormats){
            this.mail = mail
            this.mailerFormats = mailerFormats
            this.smtpService = new PostMarkSmtpService()
            // OR this.smtpService = new SendGridSmtpService()

        }

        send(){
            // Loops through mail formats and calls the send method
            this.mailerFormats.forEach((formatter) => this.smtpService.send(formatter.format(this.mail)))
        }
    }

With this implementaion a developer can keep extending the MailerSmtpService to support more mailing service without modifying the existing logic in the MailerSmtpService.

Liskov Substitution principle

view full source code

This principle states that

Derived or child classes must be substitutable for their base or parent classes.

This means that a parent class should be easily substituted by the child classes without blowing up the application. This principle can be seen in the example above where we created a parent class called MailerSmtpService and we had two child classes called PostMarkSmtpService and SendGridSmtpService. You can observe that the child classes where used as substitute with the parent class with ease.

For example:

    mailerSmtp = new MailerSmtpService();
    postmarkMailerSmtp = new PostMarkSmtpService();
    sendgridMailerSmtp = new SendGridSmtpService();

Interface Segregation Principle

view full source code

This principle states that

Do not force any client to implement an interface which is irrelevant to them.

This principle is similar to the single responsibility principle but applies to interfaces. It is usually referred too as the first principle of interfaces. Since javascript does not support interfaces we will implement it with typescript to get a better understanding. Lets take our first example where we had the HtmlFormatter and TextFormatter class which formats our email and do some few changes.

IFormatter

    export interface IFormatter {
        format(mail: string): string
        custom_styles(): string
    }

HtmlFormatter

    class HtmlFormatter implements IFormatter {

       format(mail: string) {
            // sends html version of mail
            mail = `<html>
            <head>
            <title>Email For You</title>
            ${this.custom_styles()}
            </head>
            <body>${mail}</body>
            </html>`;

            return mail;
        }

        custom_styles(): string {
            return "<style>body{background-color: blue}</style>"
        }
    }

TextFormatter

    class TextFormatter implements IFormatter {

        format(mail: string) {
            // sends text version of mail
            mail = "Text Version \n" + mail;

            return mail;
        }

        custom_styles(): string {
            return ""
        }
    }

Now with typescript we can see we have an interface to make sure we have the format and custom_styles methods been implemented in both our TextFormatter and HtmlFormatter class. If the format and custom_styles methods are not present in any class that implements this method our application will throw an error. But there is a problem here, because the custom_styles is only needed in the HtmlFormatter class to help in styling the html document. However since both the TextFormatter and HtmlFormatter class are using the same interface (IFormatter) they both have to implement the same methods(custom_styles and format) forcing as to write an empty custom_styles method for the TextFormatter class.

Now lets see a better approach:

IStyles

    export interface IStyles {
        custom_styles(): string
    }   

IFormatter

    export interface IFormatter {
        format(mail: string): string
    }

HtmlFormatter

    class HtmlFormatter implements IFormatter, IStyles {

       format(mail: string) {
            // sends html version of mail
            mail = `<html>
            <head>
            <title>Email For You</title>
            ${this.custom_styles()}
            </head>
            <body>${mail}</body>
            </html>`;

            return mail;
        }

        custom_styles(): string {
            return "<style>body{background-color: blue}</style>"
        }
    }

TextFormatter

    class TextFormatter implements IFormatter {

        format(mail: string) {
            // sends text version of mail
            mail = "Text Version \n" + mail;

            return mail;
        }
    }

Now you can see from the code refactor we have a new interface IStyles as well as our previous interface IFormatter. Also the HtmlFormatter class implements both the IStyles and IFormatter interface whiles the TextFormatter class implements only the IFormatter interface. This now makes our code cleaner and ensures the right methods are been implement in the classes that needs them. Now our TextFormatter class does not need to implement the custom_styles method since we have removed the custom_styles method from the IFormatter interface to a new interface (IStyles). This makes our code more maintainable and scalable. This is the Interface Segregation Principle at work.

Dependency Inversion Principle

view full source code

This principle is divided into two parts and it states that

  • High-level modules/classes should not depend on low-level modules/classes. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

The above lines simply state that if a high level module or class will be dependent more on a low-level module or class then your code would have tight coupling and if you will try to make a change in one class it can break another class. It is always better to abstract the code to make classes loosely coupled as much as you can. This makes maintaining the code easy.

There’s a common misunderstanding that dependency inversion is simply another way to say dependency injection. However, the two are not the same.

In our previous example we created two new interfaces IStyles and IFormatter which where implemented in the TextFormatter and HtmlFormatter class. Now lets see how these classes can be used with abstraction in the example below:

Mailer

class Mailer {
    mail: string;
    mailerFormats: Array<IFormatter>; // abstraction
    smtpService: MailerSmtpService;


    constructor(mail: string, mailerFormats: Array<IFormatter>/*abstraction*/) {
        this.mail = mail;
        this.mailerFormats = mailerFormats;
        this.smtpService = new SendGridSmtpService();
    }

    send() {
        // Loops through mail formats and calls the send method
        this.mailerFormats.forEach((formatter) =>
        this.smtpService.send(formatter.format(this._mail))
        );
    }
}

Now lets look at the refactor of the Mailer class from our first example (the first principle - Single responsibility principle). You can see we now have a _mailerFormats property which takes an array of IFormatter objects (_mailerFormats: Array<IFormatter>;). This means any class that implements the IFormatter interface can be stored in this array. Also our mailer class doesn't need to know about what formatter we are going to use, all it cares about is the formatter is implementing an IFormatter interface and it has a format method which we can call with ease. This will allow our Mailer class to be loosely coupled with our HtmlFormatter and TextFormatter class.

About

A learning material consisting of all five(5) solid design patterns with examples written in javascript and typescript.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published