Solid Design Principles In Javascript (Part 3) — Interface Segregation Principle & Dependency Inversion

Welcome back folks, hope you have been enjoying this series so far. This is the final part of the series. In the previous article we looked at the second and third solid design principle (Open Closed Principle and the Liskov Substitution Principle). In this article we focus on the last two principles which are the Interface Segregation Principle and Dependency Inversion.

If you haven’t read part 1 or part 2 of this article, you can access it here.

Part 1: https://manteycaleb.medium.com/solid-design-principles-in-javascript-50ff0d2b75b5

Part 2: https://manteycaleb.medium.com/solid-design-principles-in-javascript-part-2-open-closed-principle-6ba72d703629

You can also access the full code example on my GitHub

https://github.com/Caleb-Mantey/solid-design-principles-in-js

Now lets recap what our code looks like from (part 1 & 2)

Mailer

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)))
}
}

MailerSmtpService

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)
}
}

SendGridSmtpService

class SendGridSmtpService extends MailerSmtpService {
constructor(){
super(() => {
// Connects to sendgrid smtp service
})
}
send (mail){
this.smtp_con.deliver(mail)
}
}

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;
}
}

The code above is doing the following.

  • A base class that sets up the behaviour of an smtp service (MailerSmtpService)
  • A child class that inherits from MailerSmtpService class and connects to an smtp service (PostMarkSmtpService)
  • A child class that inherits from MailerSmtpService class and connects to an smtp service (SendGridSmtpService)
  • 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)

This code is tied together in our Mailer class when it is instantiated and can be used to send an email like this.

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

This is the implementation we had from part 1 and 2 of this article. To follow along i recommend you read part 1 and 2 if you haven’t read it. You can do so by visiting the links provided at the beginning of this text.

Interface Segregation Principle

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 to as the first principle of interfaces. Since javascript does not support interfaces we will implement it with typescript to get a better understanding. Let’s 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 included, we have to declare the return types for our functions and the data type for our variables. We also have the ability to create an interface like we do in languages like c# and java.

With this features available to us, we have added an interface (IFormatter) that exposes two functions (format and custom_styles). Our TextFormatter and HtmlFormatter class are also implementing this interface’s (IFormatter) methods. This will 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 the IFormatter interface, our application will throw an error. But, there is a problem here because the custom_styles method 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

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 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.

Our Mailer class is now depending an abstraction(IFormatter) of the HtmlFormatter and TextFormatter class.

Thanks for your time. This brings us to the end of this series. If you find my content interesting and want to learn more please like and give me a follow.

I will be posting a lot of articles on different topics in the coming weeks, so if you don’t want to miss out then keep your eye on this space to stay updated.

You can also follow me on my github.

https://github.com/Caleb-Mantey/

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Caleb Mantey

Caleb Mantey

Software Engineer & Game Developer (AR/VR)