DEV Community

Samuele Resca
Samuele Resca

Posted on

SOLID principles using Typescript

SOLIDÂ **is an acronym for the **first five object-oriented design(OOD) principles by Robert C. Martin, popularly known as @UncleBob. The five SOLID principles are:

  • Single responsibility principle: a class should have one, and only one, reason to change;
  • Open-closed principle: it should be possible to extend the behavoir of a class without  modifying it;
  • Liskov Substitution principle: subclasses should be substitutable for their superclasses;
  • Interface segregation principle: many small, client-specific interfaces are better than one general purpose interface;
  • Dependency inversion principle: depends on abstractions not concretions;

These principles, make it easy for a programmer to develop software that are easy to maintain and extend. They also make it easy for developers to avoid code smells, easily refactor code, and are also a part of the agile or adaptive software development.

The Single Responsibility Principle (SRP)

The SRP requires that a class should have only one reason to change. A class that follows this principle performs just few related tasks. You don't need to limit your thinking to classes when considering the SRP. Y*ou can apply the principle to methods or modules, ensuring that they do just one thing and therefore have just **one reason to change*. Â

Example - wrong way

The class Task defines properties related to the model, but it also defines the data access method to save the entity on a generic data source:

UML

 SOLID principles using Typescript - SRP wrong way

/*
* THE  CLASS DOESN'T FOLLOW THE SRP PRINCIPLE
*/
class Task {
    private db: Database;

    constructor(private title: string, private deadline: Date) {
        this.db = Database.connect("admin:password@fakedb", ["tasks"]);
    }

    getTitle() {
        return this.title + "(" + this.deadline + ")";
    }
    save() {
        this.db.tasks.save({ title: this.title, date: this.deadline });
    }
}
Enter fullscreen mode Exit fullscreen mode

Example - right way

The  Task class can be divided between Task class, that takes care of model description and TaskRepository that is responsabile for storing the data.

UML

 SOLID principles using Typescript - SRP right way

class Task {

    constructor(private title: string, private deadline: Date) {
    }

    getTitle() {
        return this.title + "(" + this.deadline + ")";
    }


}


class TaskRepository {
    private db: Database;

    constructor() {
        this.db = Database.connect("admin:password@fakedb", ["tasks"]);
    }

    save(task: Task) {
        this.db.tasks.save(JSON.stringify(task));
    }
}
Enter fullscreen mode Exit fullscreen mode

The Open-closed Princple (OCP)

 Software entities should be open for extension but closed for modification.

The risk of changing an existing class is that you will introduce  an inadvertent change in behaviour. The solution is create another class that overrides the behaviour of  the original class. By following the OCP, a component is more likely to contain maintainable and re-usable code.

Example - right way

The CreditCard class describes a method to calculate the monthlyDiscount(). The monthlyDiscount() depends on the type of Card, which can be : Silver or Gold. To change the monthly discount calc, you should create another class which overrides the monthlyDiscount() Method. The solution is to create two new classes: one for each type of card.

UML

SOLID principles using Typescript - OCP Right

class CreditCard {
    private Code: String;
    private Expiration: Date;
    protected MonthlyCost: number;

    constructor(code: String, Expiration: Date, MonthlyCost: number) {
        this.Code = code;
        this.Expiration = Expiration;
        this.MonthlyCost = MonthlyCost;
    }

    getCode(): String {
        return this.Code;
    }

    getExpiration(): Date {
        return this.Expiration;
    }

    monthlyDiscount(): number {
        return this.MonthlyCost * 0.02;
    }

}



class GoldCreditCard extends CreditCard {

    monthlyDiscount(): number {
        return this.MonthlyCost * 0.05;
    }
}


class SilverCreditCard extends CreditCard {

    monthlyDiscount(): number {
        return this.MonthlyCost * 0.03;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Liskov Substitution Principle (LSP)

Child classes should never break the parent class' type definitions.

The concept of this principle was introduced by Barbara Liskov in a 1987 conference keynote and later published in a paper together with Jannette Wing in 1994. As simple as that, a subclass should override the parent class methods in a way that does not break functionality from a client's point of view.

Example

In the following example ItalyPostalAddress, UKPostalAddress and USAPostalAddress extend one common class: PostalAddress. The AddressWriter class refers PostalAddress: the writer parameter can be of three different sub-types.

UML

SOLID principles using Typescript - LSP Right

abstract class PostalAddress {
    Addressee: string;
    Country: string
    PostalCode: string;
    City: string;
    Street: string
    House: number;

    /*
    * @returns Formatted full address
    */
    abstract WriteAddress(): string;
}

class ItalyPostalAddress extends PostalAddress {
    WriteAddress(): string {
        return "Formatted Address Italy" + this.City;
    }
}
class UKPostalAddress extends PostalAddress {
    WriteAddress(): string {
        return "Formatted Address UK" + this.City;
    }
}
class USAPostalAddress extends PostalAddress {
    WriteAddress(): string {
        return "Formatted Address USA" + this.City;
    }
}


class AddressWriter {
    PrintPostalAddress(writer: PostalAddress): string {
        return writer.WriteAddress();
    }
}
Enter fullscreen mode Exit fullscreen mode

The Interface Segregation Principle (ISP)

It is quite common to find that an interface is in essence just a description of an entire class. The ISP states that we should write a series of smaller and more specific interfaces that are implemented by the class. Each interface provides an single behavior. Â

Example - wrong way

The following Printer interface makes it impossible to implement a printer that can print and copy, but not staple:

interface Printer {
    copyDocument();
    printDocument(document: Document);
    stapleDocument(document: Document, tray: Number);
}


class SimplePrinter implements Printer {

    public copyDocument() {
        //...
    }

    public printDocument(document: Document) {
        //...
    }

    public stapleDocument(document: Document, tray: Number) {
        //...
    }

}
Enter fullscreen mode Exit fullscreen mode

Example - right way

The following example shows an alternative approach that groups methods into more specific interfaces. It describe a number of contracts that could be implemented individually by a simple printer or simple copier or by a super printer:

interface Printer {
    printDocument(document: Document);
}


interface Stapler {
    stapleDocument(document: Document, tray: number);
}


interface Copier {
    copyDocument();
}

class SimplePrinter implements Printer {
    public printDocument(document: Document) {
        //...
    }
}


class SuperPrinter implements Printer, Stapler, Copier {
    public copyDocument() {
        //...
    }

    public printDocument(document: Document) {
        //...
    }

    public stapleDocument(document: Document, tray: number) {
        //...
    }
}
Enter fullscreen mode Exit fullscreen mode

The Dependency inversion principle (DIP)

The DIP simply states that high-level classes shouldn't depend on  low-level components, but instead depend on an abstraction.

Example - wrong way

The high-level WindowSwitch depends on the lower-level CarWindow class:

UML

SOLID principles using Typescript - DIP wrong way

class CarWindow {
    open() {
        //... 
    }

    close() {
        //...
    }
}


class WindowSwitch {
    private isOn = false;

    constructor(private window: CarWindow) {

    }

    onPress() {
        if (this.isOn) {
            this.window.close();
            this.isOn = false;
        } else {
            this.window.open();
            this.isOn = true;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Example - right way

To follow the DIP, the class WindowSwitch should references an interface (IWindow) that is implemented by the object CarWindow:

UML SOLID principles using Typescript - DIP right way

interface IWindow {
    open();
    close();
}

class CarWindow implements IWindow {
    open() {
        //...
    }

    close() {
        //...
    }
}


class WindowSwitch {
    private isOn = false;

    constructor(private window: IWindow) {

    }

    onPress() {
        if (this.isOn) {
            this.window.close();
            this.isOn = false;
        } else {
            this.window.open();
            this.isOn = true;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Final thoughts

Typescript make it possible to bring all of the principles and practices of OOP into your software, using SOLID principles to guide your design patterns. Here's the GitHub repository containing the full examples.

Top comments (11)

Collapse
 
jillesvangurp profile image
Jilles van Gurp

Please everybody, stop doing inheritance hierarchies. It adds unnecessary levels of indirection and complexity.

I mostly use Java in my day to day work; mainly because I've been around for a few decades and this was the hip thing to do when I was younger, no shame in admitting that ;-). I disagree with the vast majority of cases where people use class extension in Java since I find it adds complexity, restricts extensibility and just generally makes life unnecessarily hard. I usually end up refactoring my code to get rid of class inheritance in the few cases I make the mistake of using it and I've learned to avoid it. Composition over inheritance is just almost always the better solution.

But it's not uncommon for me to have to deal with really face palm worthy inheritance hierarchies in other people's code where you have a concrete class extending an AbstractWhatever extending and EvenMoreAbstractWhatever implementing any number of interfaces in each of those. I've seen cases in e.g. Spring where the hierarchy goes like five, six or even more levels deep. Absolutely awful code where you have to wade through multiple levels of overrides, super calls, etc. Such code is hard to figure out, refactoring is a pain (and therefore does not happen when it needs to) and the inevitable design mistakes in the parent classes tend to get worked around over time in sub classes to add more complexity. I'd love to have to deal with less code like that professionally.

For the same reason, I actually really like what Google did with Go: medium.com/@simplyianm/why-gos-str..., Also checkout Dave Cheney's article about Go and the SOLID principles dave.cheney.net/2016/08/20/solid-g...

Basically, Google found an elegant way to get rid of classes, have type safe interfaces and duck typing. I'd love to see that in more languages.

IMHO Java style class inheritance is exactly the wrong thing to imitate in the javascript world with transpilers for TypeScript and similar languages. Maybe Go would be a better and more javascript like way of achieving the same goals of writing SOLID code but with less verbosity?

Collapse
 
samueleresca profile image
Samuele Resca • Edited

Thank you for you time Jilles. I appreciate it so much.
Actually, I do not know Go, maybe I should take a look into it.
By any way, I am agree with you, but not totally agree:).

Composition and inheritance are very different things, and should not be confused with each other.
While it is true that composition can be used to simulate inheritance with a lot of extra work, this does not make inheritance a second-class citizen, nor does it make composition the favorite son.

Probably, this demo article is not the case: it is very hard to explain the power of inheritance with Priters and Postal code.

BUT

I see a lot of smell javascript. I think typescript and design patterns can improve code clarity and simplicity.
I don't think that OOP and inheritance are the SOLUTION, they are the base philosphy behind a lot of technologies, languages and techniques.

In conclusion, Inheritance is a solution. Your own task is to find the best solution that solve your problem.
Once again, thank you for your time.

Do you also use Go in your daily work? What is your opinion about it ( comparing with Java)?

Sem

Collapse
 
jillesvangurp profile image
Jilles van Gurp

I don't use go currently but I would like to. Regarding composition and inheritance, there are a lot of good composition based design patterns that remove the need for using inheritance. Go takes the learnings from the past and removes the need for having inheritance.

A pitfall with inheritance is confusing the need for expressing is-a relations (which you can do with interfaces) and reusing bits of code. There are other and arguably better ways of achieving the last goal including such things as default methods on interfaces, mixins (not in Java sadly), or indeed composition. Also, lambda's in Java 8 are a nice way to get rid of a lot of inheritance cruft.

Looking at typescript, I see a lot of the same verbosity for which Java has been vilified popping up there just to be able to express that a button that has a show method is a drop in replacement for a dropdown or a popup that also has a show method in a type safe way. In go you'd be able to define an interface ShowableThingies with a show method and instantly make anything with a show method a ShowableThingy without the need to declare on everything that has a show method that it is indeed a ShowableThingy. Then through type composition, you can easily combine and reuse, and adapt implementations as well. All without having to do inheritance and type safe. That's nice. You can have your cake and eat it.

The nice thing with javascript was that you can call show on anything and it will simply fail at run time if show is not actually there. The problem with that of course is is that it would be nicer to find out mistakes like that before run time (e.g. through static typing) but it makes for some really concise code if you do it properly. Sadly, disciplined application of 'doing things properly' is not a scalable way to engineer software. It always breaks down at some point. Hence the need for some level of sane typing in typescript and similar transpilation languages for javascript.

OOP was fashionable in the eighties and nineties; particularly in domains related to graphics and UIs where people were obsessed for a while with coming up with reusable widget libraries. Philosophically, it's a naive attempt to classify world + dog into a strictly hierarchical onthology. IBM spent quite a bit of R&D budget on coming up with ridiculously complex frameworks for capturing all sorts of types of objects. A lot of that R&D (initially around smalltalk) fed into Java (e.g. Swing) and enterprise Java.

The problem with this approach is that no matter how you break things down, there's always a situation where that break down stops being optimal. Abstractions are leaky and inheritance hierarchies are needlessly rigid. This tends to show in older code bases based on inheritance hierarchy. People start bolting things on that really don't belong there, overriding default behavior to work around limitations, and you get these kafkaesk situations where a printer also has to be a copier because that seemed like a good idea five years ago but is obviously not appropriate to your current product.

Collapse
 
antoine profile image
Antoine Gagnon

Really good article, examples are simple and clear. Thanks !

Collapse
 
la_urre profile image
Dam Ur

Clear explanation !

About your example for the OCP, and I see this often in other posts related to this subject, I think the specialization is made at an inadequate level. Actually you specialize the whole CreditCard type by introducing GoldCreditCard and SilverCreditCard. To me, it seems that you specialize CreditCard based on a property of it. What if I apply the same reasoning with other properties ? What if one day a credit cart never expires ? If I apply the same logic, I would create a NeverExpiringCreditCard specialized type, but then how i integrate with the already specialized types ?

I think what is important in OCP is to well define the extensions' entry points. In this case I would rather have created a CreditCardType type, then specializing it with both gold and silver types, and calculate the discount in them, keeping a single CreditCard type. This way the type hierarchy seems more coherent to me. What do you think ?

Collapse
 
nitishdayal profile image
Nitish Dayal

Great article, easy to follow examples, clean and thorough explanations. Thank you for posting.

Collapse
 
milianoo profile image
Milad Rezazadeh

Really simple and good article, specially with the wrong examples, I shared with my team. Thanks.

Collapse
 
samueleresca profile image
Samuele Resca

Thanks Milad :)

Collapse
 
hariharan_dev profile image
hariharan

I'm wondering! where is the content?

Collapse
 
tingenek profile image
Mark Lawson

Thanks for a good clear article. Sadly OOP fell out of favour, which is a shame, as it is a good design methodology: encapsulation and inheritance are great tools if used judiciously.
.

Collapse
 
0xbit profile image
Nordine

Nice article with simple examples.
thanks