Solid Design Principles In Javascript (Part 2) — Open-Closed Principle / Liskov Substitution Principle

Caleb Mantey
5 min readFeb 5, 2022

--

Design is beautiful when it is simple

Hello readers, in my previous article i talked about solid design patterns and covered the first principle (Single Responsibility Principle). In this article we focus on the second and third principle which is the Open Closed Principle and the Liskov Substitution Principle.

If you haven’t read part 1 of this article, don’t worry you can read it here.

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

You can also access the full code example on my GitHub

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

In our previous article we had some code that looked like this:

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

Looking at the code above we are not doing anything much, we have just separated the logic for sending emails into separate classes.

The code above is doing 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)

From the code above we can simply call the Mailer class and pass some required properties to its constructor method (mail, mailerformats) which will be used to setup our mail.

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

Now lets see how we can make this code even better with the open-closed principle.

Open-closed Principle

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

Let’s take our MailerSmtpService class in the first example and let’s make it support this principle.

MailerSmtpService — ( Initial )

This is our initial implementation for the MailerSmtpService. Nothing fancy here yet. All we are doing is connecting to an smtp service in the constructor method and storing the result of the connection in this.smtp_con, then we provide a send method that takes a mail as an argument and sends an email.

But we have a problem here. Let’s say we want to change the smtp service provider. We will have to come to our MailerSmtpService class and implement the new smtp service here. However we can do better and use the open-closed principle to make our code more maintainable and even provide the option for switching smtp service providers without touching any piece of existing code.

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 )

Now in order to support the open-closed principle, we will remove the smtp_service_connection method from our MailerSmtpService class and rather we pass the method as a parameter in the MailerSmtpService constructor, then in a subclass (PostMarkSmtpService and SendGridSmtpService) that inherits from MailerSmtpService we call the constructor method of the base class with super(() => {}) then we pass a method which handles the smtp connection depending on the smtp provider in use. Also we override the send method in the parent class (MailerSmtpService ) and each of the child classes(PostMarkSmtpService and SendGridSmtpService) implement their custom versions of the send method.

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

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 by inheriting from the MailerSmtpService class.

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 implementation a developer can keep extending the MailerSmtpService to support more mailing services without modifying the existing logic in the MailerSmtpService class.

This is the open-closed principle at work.

Liskov Substitution principle

The next principle is the Liskov substitution principle it is easier to understand this principle because we have already implemented it in our code example above.

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 for the parent class with ease.

For example with typescript we can infer the type of PostMarkSmtpService and SendGridSmtpService to be their parent class MailerSmtpService and the application will still work without any errors.

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

Thanks for your time. Give me a follow or a like if you loved this article.

Watch out for the final part (part 3) of these series where we talk about the last two principles (Interface Segregation Principle and Dependency Inversion)

--

--

Caleb Mantey
Caleb Mantey

Written by Caleb Mantey

Software Engineer & Game Developer (AR/VR)

Responses (1)