Good design is obvious. Great design is transparent.
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.
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 when the need arises. 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/
Top comments (0)