S.O.L.I.D Principles
Previously, I was asked to explain the Dependency Inversion principle in an interview — I had briefly covered SOLID, though it wasn’t concrete in my memory.
The last few days I have been scraping the internet looking at examples for each of the principles. I wanted to make the examples as high level as possible so the concepts are what stand out, not the syntax.
According to wikipedia:
“SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable.”
Ideally, we want our software to be readable, able to handle change efficiently, and testable.
Mastering the SOLID principles will no-doubt make you a better developer.
Let’s start.
S — Single Responsibility Principle
“Each class and module in a program should focus on a single task”
Real World Example
Vacuum Cleaner
- Clean carpets, tiles, has heaps of nozzles and attachments.
- However, at its core — its main responsibility is to suck in dirt/dust etc and store it in its container that can then be thrown out.
- If we had a vacuum that cleans windows, it may work and that’s all fine and good. Though as time goes by, it may increase maintenance costs and inevitably break down.
Code Example — Invoice
public class Invoice { private String customer;
private String state;
private int total; public Invoice(String customer, String state, int total) {
this.customer = customer;
this.state = state;
this.total = total;
} //Getters & Setters public String getDetails() {
return this.getCustomer() + ", " + this.getTotal();
} public void emailInvoice() {
System.out.println("Sending email...");
System.out.println(this.details());
} public double determineTax() {
switch(this.state) {
case "VIC":
return 0.3;
case "NSW":
return 0.5;
default:
return;
}
}}
Here we have an Invoice
class that seems to be relatively straightforward. The class:
- Returns details about the invoice
- Calculates sales tax
- Emails the invoice with its details
When we run the code everything works fine.
Rule of Thumb: No ANDs allowed
Let’s break down what our invoice class does:
“The invoice class returns invoice details AND calculates sales tax AND emails the invoice.”
It’s doing too much. When refactoring, treat the behaviour between the ANDs as their own class.
Refactoring
We’ll be creating seperate classes:
- Mailer — handles the emailing
- Sales Tax — handles the sales tax calculation
- Invoice — handles the invoice info
Invoice Class
public class Invoice {private String customer;
private int total;
public Invoice(String customer, int total) {
this.customer = customer;
this.total = total;
}
//Getters & Setters
public String getDetails() {
return this.getCustomer() + ", " this.getTotal();
}
}
Notice the removal of state attribute. As the Invoice
class is no longer taking responsibility of sales tax, it is no longer necessary.
Sales Tax Class
public class SalesTax {private String state;
public SalesTax(String state) {
this.state = state;
}
//Getters & Setters
public double determineTax() {
switch(this.state) {
case "VIC":
return 0.3;
case "NSW":
return 0.5;
default:
return;
}
}
}
The Tax
class should be decoupled from the Invoice class, as it may need to be used in other parts of the application. Keeping the sales tax inside the invoice class means we need to create an Invoice
class to access the sales tax.
This violates the SRP principle.
Mailer Class
public class Mailer {
public Mailer(){}
public static void sendEmail(String content) {
System.out.println("Sending email...");
System.out.println(content);
}
}
The Mailer
class takes complete responsibility of emailing customers.
- Want to email an invoice? Input the invoice details.
- Want to email them a newsletter? Input the newsletter into the content parameter.
Running the Application
public static void main(String[] args) { //Create a new invoice Invoice invoice = new Invoice("John Doe", 100); //Create a tax object SalesTax tax = new SalesTax("VIC");
tax.determineTax(); //returns 0.3 //Print out the details String details = invoice.getDetails();
System.out.println(details); //Outputs; "John Doe, 100" //Let's email the invoice Mailer.sendEmail(details);
}
Wrapping Up
So why is this type of design pattern important?
- Reduces coupling (dependencies)
- Allows for classes to be used independently
- Assists with testing
O — Open-Closed Principle
“Software elements (classes, modules, functions etc.) should be open for extension, but closed for modification.”
This means that:
- You should be able to build your classes in such a way that you can extend via child classes and inheritance. And that once you’ve created the class, it no longer needs to be changed.
Code Example — Order Report
public class OrderReport { private String customer;
private int total; public OrderReport(String customer, int total) {
this.customer = customer;
this.total = total;
} //Getters & Setters public void generateInvoice() {
System.out.println("---- Invoice ----");
System.out.println("Customer: " + this.getCustomer());
System.out.println("Total: " + this.getTotal());
System.out.println("-----------------");
} public void generateShippingReport() {
System.out.println("---- Shipping Report ----");
System.out.println("Customer: " + this.getCustomer());
System.out.println("Total: " + this.getTotal());
System.out.println("-------------------------");
}}
What’s wrong with this code?
- To start, it’s breaking SRP. This code generates an invoice AND generates a shipping report. That’s not singular.
- It’s not flexible. Whenever we want to generate an order report we must include customer and total parameters. What if something changes, we need another attribute? An address, perhaps?
- The
OrderReport
class, must be modified by adding anAddress
attribute to the class. This breaks Open-Closed Principle.
Refactoring the Code
OrderReport
is the parent class. Invoice
and ShippingReport
will both be sub-classes that have their own unique purpose.
public class OrderReport { private String customer;
private int total; public OrderReport(String customer, int total) {
this.customer = customer;
this.total = total;
}
}
public class Invoice extends OrderReport {
public Invoice(String customer, int total) {
super(customer, total);
}
public void generateInvoice() {
System.out.println("---- Invoice ----");
System.out.println("Customer: " +
this.getCustomer());
System.out.println("Total: " + this.getTotal());
System.out.println("-----------------");
}
}
public class ShippingInvoice extends OrderReport {
private String address;
public ShippingReport(String customer, int total, String address) {
super(customer, total);
this.address = address;
}
//Getters and setters
public void generateShippingReport() {
System.out.println("---- Shipping Report ----");
System.out.println("Customer: " + this.getCustomer());
System.out.println("Address: " + this.getAddress());
System.out.println("Total: " + this.getTotal());
System.out.println("-------------------------");
}
}
//Implementation
public static void main(String[] args) {
//Create an Invoice object becuase we want to generate an invoice
Invoice inv = new Invoice("Tim", 1000);
inv.generateInvoice();
//Let's create a shipping report for "Tim"
String customer = inv.getCustomer();
int total = inv.getTotal();
ShippingReport sh = new ShippingReport(customer, total, "123 Fake Street");
sh.generateShippingReport();
}
What happened?
The OrderReport
class is the parent class, which both Invoice
and ShippingReport
inherit from. This keeps the core functionality of an OrderReport
with extension of either the Shipping Report
or Invoice
classes.
Principles being followed after refactor:
☑️ Single Responsibility
☑️ Open-Closed
L — Liskov Substitution
“Ability to replace any instance of a parent class with an instance of one of its child classes without negative side effects”
OR
“If a program is using a Base class, then the reference to the Base class can be replaced with Derived class without affecting the functionality of the program”
What it means:
- Subtypes should retain the behaviour of main types
- Children should be like their parents for what they inherit
“If an override method does nothing or throws and exception, you’re probably violating LSP.”
Code Example — Employees
An employee:
- Has an id, name and salary
- Different types of employees: Permanent, Temporary, Contract
- Only Permanent & Temporary employees are entitled to a bonus
Wrong way to build our program
abstract class Employee { private Long id;
private String name;
private double salary; //Constructor //Getters & Setters abstract void getBonusAmount();}
Let’s create our subclasses:
public class PermanentEmployee extends Employee { //Constructor //Getters & Setters public void getBonusAmount() {
System.out.prinln(this.getSalary() * 0.1);
}
}public class TemporaryEmployee extends Employee { //Constructor //Getters & Setters public void getBonusAmount() {
System.out.prinln(this.getSalary() * 0.1);
}
}public class ContractEmployee extends Employee { //Constructor //Getters & Setters public void getBonusAmount() throws Exception {
throw new InvalidEmployeeException();
}
}
What’s wrong with this code?
Remember:
If an override method does nothing or throws and exception, you’re probably violating LSP.
The problem here is that ContractEmployee
is inheriting getBonusAmount()
even though contract employees are not entitled to bonuses. This violates LSP. Why should we add a method that has no use? Why don't I give you keys to a car even though you don't have a license.
It doesn’t make sense.
Now if there came a scenario to loop through all the employees that are entitled to bonuses:
public static void main(String[] args) { //Adding employees
Employee a = new PermanentEmployee(1, "John Doe", 50000);
Employee b = new TemporaryEmployee(1, "Jane Citizen", 60000);
Employee c = new ContractEmployee(1, "Joe Bloggs", 75000); Employee[] employees = {a, b, c}; for(Employee e : employees) {
e.getBonusAmount(); //Throws exception for c object
}
}
The code will run, but when it reaches the iteration of ContractEmployee
, it'll throw and exception and error. Why would we want to implement such functionality?
Refactoring our code
To do:
- Convert
Employee
to a concrete class - Create an
IBonus
interface with thegetBonusAmount()
method - Have only the classes that are eligible to receive a bonus to implement
IBonus
First things first, satisfy SRP by making Employee
a concrete class.
class Employee { private Long id;
private String name;
private double salary; //Constructor //Getters & Setters}
Creating the interface
public interface IBonus {
public void getBonusAmount();
}
Add the IBonus method functionality to specified classes:
public class PermanentEmployee extends Employee implements IBonus { //Constructor //Getters & Setters @Override
public void getBonusAmount() {
System.out.prinln("Bonus for perm employee: " + this.getSalary() * 0.1);
}
}public class TemporaryEmployee extends Employee implements IBonus { //Constructor //Getters & Setters @Override
public void getBonusAmount() {
System.out.prinln("Bonus for temp employee: " + this.getSalary() * 0.15);
}
}public class ContractEmployee extends Employee {
//Constructor
//Getters & Setters
//Other functionality}
The beauty of interfaces:
TemporaryEmployee
andPermanentEmployee
both have unique bonus amounts. This gives greater flexibility to our code.
Outputting the code:
public static void main(String[] args) { //Create our objects
TemporaryEmployee temp = new TemporaryEmployee("John", 60000);
PermanentEmployee perm = new PermanentEmployee("Bob", 50000);
ContractEmployee con = new ContractEmployee("Jack", 75000); //Create a list of IBonus interface type
List<IBonus> employees = new ArrayList<IBonus>(); employees.add(temp);
employees.add(perm);
/* employees.add(con); compilation error: ContractEmployee doesn't implement IBonus interface */ for(IBonus e : employees) {
e.getBonusAmount();
} /*
For-loop returns:
Bonus for temp employee: $9000
Bonus for perm employee: 5000
*/}
So, what just happened:
- We decoupled our
bonusAmount()
method from theEmployee
class. - We only implemented
IBonus
interface to classes that are eligible for a bonus. ContractEmployee
still inherit everything a normal employee would have. Now employees are singular, satisfying SRP. Further, OCP is satisfied as any subclass of employee can have added functionality that our concreteEmployee
class may not have.
I — Interface Segregation
“Code should not be forced to depend on methods that it doesn’t use”
This is pretty straight forward:
One large job class should be segregated into multiple interfaces depending on the requirement of the class.
Code Example — Printer
Printer
class has a name and brandIPrinterOptions
is an interface with the operations printers can perform
How NOT to implement
public class Printer implements IPrinterOptions { private String name;
private String brand; public Printer(String name, String brand) {
this.name = name;
this.brand = brand;
} //Getters & setters
}public interface IPrinterOptions {
public boolean printContent(String content);
public boolean faxContent(String content);
public boolean photocopyContent(String content);
}
The problem here is that every object create with the IPrinterOptions
will have to implement all the IPrinterOptions
methods.
What happens when:
- We want to add a new method to
IPrinterOptions
. We will have to modify every object that implements the interface. If n objects implement it, then thats n files that we have to change. That’ll take some time.
Refactored
Remember:
One large job class should be segregated into multiple interfaces depending on the requirement of the class.
public interface IPrint {
public boolean printContent(String content);
}public interface IFax {
public boolean faxContent(String content);
}public interface IPhotocopy {
public boolean photocopyContent(String content);
}
Now each interface is unique, and we can add required functionality to relative classes.
Let’s create a: Printer that prints and photocopies.
public HPPrinter implements IPrint, IPhotocopy {
//Constructor
//Getters & Setters @Override
public boolean printContent(String content) {
//perform some operation
return true;
} @Override
public boolean photocopyContent(String content) {
//perform some operation
return true;
}
}
We only add the required functionality of the object. This allows for reusable, maintainable and modular code.
We no longer need to implement the faxContent()
method just to keep it empty (which also violates LSP). That's a waste of code, a waste of space and definitely a waste of time to refactor.
D — Dependency Inversion/Injection
“High-level modules should not depend on low-level modules. Both should depend on abstractions.”
OR
“Abstractions should not depend on details. Details should depend on abstractions.”
The above is achieved using Dependency Injection (DI). There are 3 ways we can use DI:
- Constructor Injection
- Property Injection
- Method Injection
Why do we use Dependency Injection/Inversion
- It makes the code easier to test independently
- It also means that components are easier to replace in later versions because you can just swap out what objects you provide to it, with objects of a different class with a matching interface.
Code Example — Photos/Gallery App on mobile
- We know that when we choose an image, we have the option to share the image through a particular application.
- These applications can change over time, so we have to cater for new apps coming and going.
Without DI
public class Gallery { private WatsApp whatsapp = new WatsApp();
private FBMessenger fbmsgr = new FBMessenger();
private Viber viber = new Viber();
public void shareImg(String type){
if(type === "FB") {
fbmsgr.send();
} else if(type === "WATSAPP") {
watsapp.send()
} else {
viber.send();
}
}
}
The problem here:
- What if we need to implement 100 different apps, will we write 100 if statements?
- What if the send method changes? We need to go and modify all the lines of code that execute the send method.
- Instantiating objects with the new keyword, even though we may not need to use them all.
- There are a lot more problems with this code, not just the above dot points — however, that’s for another time.
With dependency injection/inversion we can get rid of these problems in a clean and structured manner.
Let’s create a blueprint for the send method through an interface.
public interface IShareable {
public void send();
}
Instead of creating n objects that override the parent send()
method, (and could potentially violate LSP) we create an interface that acts a blueprint of any class that implements it.
Next, have our App classes implement IShareable
, and provide their own unique logic for the send()
method.
public Watsapp implements IShareable { @Override
public void send() {
System.out.println("Sending from Watsapp");
}
}public FBMessenger implements IShareable { @Override
public void send() {
System.out.println("Sending from FB Messenger");
}
}public Viber implements IShareable { @Override
public void send() {
System.out.println("Sending from Viber");
}
}
Let’s build the Gallery
class with Dependency Injection
public class Gallery { IShareable shareableApp; public void setShareableApp(IShareable shareable) {
this.shareableApp = shareable;
} public void send() {
shareableApp.send();
}
}
What we have done:
- We don’t instantiate Watsapp, FBMessenger or Viber at compile time.
- We don’t need to change the
Gallery
code whenever there's a new application that has the ability to share images. - Gallery is flexible to use any class that implements the
IShareable
interface (our abstraction layer). - We instantiate
IShareable
at runtime, not at compilation.
Conclusion:
If you are mindful of the SOLID principles when designing and building applications, you can create software designs that are more understandable, flexible and maintainable.
Please comment and give your feedback :)
Thank you!