Solid Design Principles In Javascript (Part 2) — Open-Closed Principle / Liskov Substitution Principle
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)