SOLID Design Principles — The Simplest Explanation.
If you working on small or even medium scale projects you may thoughts all these principles useless burden, but for large-scale projects, you will definitely will realize the greatness of these principles.
Content:
- Single Responsibility Principle (SRP)
- Open-Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
- A class should have only one reason to change.
- this means that a class should not be loaded with multiple responsibilities and a single responsibility should not be spread across multiple classes or mixed with other responsibilities.
- The reason is that the more changes requested in the future, the more changes the class needs to apply.
- Example:
chief financial officer (CFO), chief operating officer (COO), chief technology officer (CTO) responsible for Calculate payments, reports, and save data to the database respectively.
if we put all this logic into Employee class like that:
public class Employee{public double CalculatePay(Money money){//logic for payments}
public String reportHours(Employee employee){//logic for get report for employee}public Employee save(Employee employee){//store employee to the database}}
we will face a problem, and this problem is Employee class takes care of three different Responsibilities (the logic of calculate payments, save Employee data to the database, and create reports). we solve it by creating three different classes and make every class have Single Responsibility.
So what is the problem with putting all together?
if you work on a small project individually, you will see this, not a big deal to have all logic in one place, but in large scale projects, there will a lot of cases for every method so for example calculatePay metho may depend on many methods based on the case (each case have the different calculation), so the bigger project is the bigger logic needed, and in huge companies, there is a team responsible for every part of project logic.
So in this example:
- The calculatePay() method is specified by the accounting department, which reports to the CFO.
- The reportHours() method is specified and used by the human resources department, which reports to the COO.
- The save() method is specified by the database administrators, who report to the CTO.
For this reason, it not wise to put all logic in one place.
And this will be the new structure now all logic is separated from each other, and have employee data shared across them.
So now every team can work fine with their scope of the project without affecting other parts of the project.
But this leads to another problem which is developers now have three classes that they have to instantiate. A common solution to this solve this problem is to use the Facade pattern.
Facade pattern hides the complexities of the system and provides an interface to the client using which the client can access the system.
and after apply SRP the code will be something like that:
public class Employee{// Employee Data
}public class PayCalculator{public double CalculatePay(Money money){//logic for payments}
}public class HourReport{public String reportHours(Employee employee){//logic for get report for employee}
}public class EmployeeSaver{public Employee saveEmployee(Employee employee){//store employee to database}}
Open-Closed Principle (OCP)
- Software entities should be open for extension but closed for modification.
- The OCP states that the behaviors of the system can be extended without having to modify its existing implementation.
- New features should be implemented using the new code, but not by changing the existing code.
- reduces the risk of breaking the existing implementation code.
if simple extensions to the requirements force massive changes to the software, then the architecture of that software is bad, and a good software architecture would reduce the amount of changed code to the barest minimum. Ideally, zero.
- build the classes in a way that we able to extend them by child classes and inheritance and once you created the class, it no longer needs to be changed.
- Example:
public enum paymentType = {Cash, CreditCard};public class PaymentManager{
public PaymentType paymentType {get;set;}
public void Pay(Money money){
if(paymentType == PaymentType.cash){// pay with cash
}else{// pay with credit card
}}}
and if we need to add a new payment type? we need to modify this class.
open for extension:
public class payment{public virtual void pay(Money money){// base class
}}
close for modification:
public class CashPayment:payment{public override void pay(Money money){// pay with cash
}}public class CreditCard:payment{public override void pay(Money money){// pay with credit card
}}
Now if you need to add another payment logic you only need to create a new class and implement or extends the payment interface or base class.
If component A should be protected from changes in component B, then component B should depend on component A.
Liskov Substitution Principle (LSP)
- A subclass should behave in such a way that it will not cause problems when used instead of the superclass.
- LSP is a definition of a subtyping relation, called strong behavioral subtyping.
- if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the desirable properties of the program.
- Example:
Bad Example of LSP:
public class Bird{
public void fly(){}
}
public class Duck extends Bird{}
The duck can fly because it is a bird, But what about this:
public class Ostrich extends Bird{}
Ostrich is a bird, But it can’t fly, Ostrich class is a subtype of class Bird, But it can’t use the fly method, that means that we are breaking the LSP principle.
Good Example of LSP:
public class Bird{
}
public class FlyingBirds extends Bird{
public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{}
ability to replace any instance of a parent class with an instance of one of its child classes without negative side effect.
Interface Segregation Principle (ISP)
- Developers shouldn’t be forced to depend upon interfaces that they don’t use.
- Example:
public interface someInterface{public int methodOne(){
// Do something}public int methodTwo(){
// Do somthing diffrent}}
if you want to implement this Interface
public someClass implements someInterface{public int methodOne(){
// logic to do something}public int methodTwo(){
}}
and only need to overwrite one method only why you forced to overwrite the other method and left it empty.
we solve this problem by creating two separate interfaces, every interface has only relevant methods.
public interface someInterface{public int methodOne(){
// Do something}}public interface someDiffrentInterface{public int methodTwo(){
// Do somthing diffrent}}
Now you can implement the only interface you need.
depending on something that carries baggage that you don’t need can cause you troubles that you didn’t expect.
Dependency Inversion Principle (DIP)
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
- components should depend on abstraction.
module need to manage high-level functionality with zero implementation details this allows the module to be used for any type of application and for all modules inside the application.
- Example:
we have an existing system which sends notifications to users by email
public class Email{public void sendMail(){// send mail
}
}public class Notification{private Email _email;public Notification(){_email = new Email();
}public void promotionalNotification(){_email.sendMail();
}
}
And if we need to send a notification by SMS, we need to change email to SMS in this class, which violates DIP, because the parent class doesn't depend on abstraction, it has a specific data tie to it.
we can solve it by:
1. create an interface.
public interface Messeneger{void sendMessage();
}
2. create classes implements this interface.
public class Email implements Messenger{void sendMessage(){// send email
}
}public class SMS implements Messenaer{void sendMessage(){// send SMS
}
}
3. build class Notification which has Messenger property.
public class Notification{private Messenger _messenger;public Notification(){_messenger = new Email();
}public void promotionalNotification(){_messeager.sendMessage();
}
}
You can replace crate a new object in three ways:
1. Constructor injection:
passing data to Notification Constructor
public class Notification{private Messenger _messenger;public Notification(Messenger messenger){_messenger= messenger;
}public void promotionalNotification(){_messenger.sendMessage();
}
}
2. Property injection:
use the setter to set a Property of Notification class
public class Notification{private Messenger messenger;public void setMessenger(Messenger messenger) {
this.messenger= messenger;
}public void promotionalNotification(){_messenger.sendMessage();
}
}
3. Method injection:
pass object data to the method we want to use directly in Notification class
public class Notification{public void promotionalNotification(Messenger messenger){messenger.sendMessage();
}
}