SOLID is a set of five design principles intended to make software designs more understandable, flexible, and maintainable. This was theorized by Robert C. Martin famously known as Uncle Bob in his paper published in the year 2000.
Every software component should have one responsibility. Every software component should have one reason to change.
Cohesion – Cohesion refers to the degree to which the elements inside a module belong together.
Coupling – Coupling is the degree of interdependence between software modules.
Below code does not follow SRP
class UserRegistrationService { public void registerUser(String username, String emailId) { // Create a sql connection // Insert the username into database // Close the connection // Send a welcome email to the user } }
Below code follows SRP
class DatabaseService { public void createConnection(){} public void insert(){} public void closeConnection(){} } class EmailService { public void sendEmail(){} } class UserRegistrationService { public void registerUser(String username, String emailId) { databaseService.insert(username, emailId); emailService.sendEmail(emailId); } }
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. OCP encourages the use of polymorphism. Also a way to implement OCP is using strategy design pattern.
Below is an example for a bad design
interface Operation{} class Addition implements Operation { int a; int b; int result; // Constructor, getters and setters } class Subtraction implements Operation { int a; int b; int result; // Constructor, getters and setters } class Calculator { public void calculate(Operation operation) { // Null checks if(operation instanceof Addition) { Addition addition = (Addition) operation; addition.setResult(addition.getA() + addition.getB()); } else if(operation instanceof Subtraction) { Subtraction subtraction = (Subtraction) subtraction; subtraction.setResult(subtraction.getA() - subtraction.getB()); } } }
Polymorphism
1. Method overloading
void add(int a, int b) void add(int a, int b, int c)
2. Method overriding
interface Operation { void perform(); } class Addition implements Operation { @Override void perform() { // Some code } } class Subtraction implements Operation { @Override void perform() { // Some code } }
Below is an example of design following OCP
interface Operation { void perform(); } class Addition implements Operation { int a; int b; int result; // Constructor, getters and setters public void perform() { result = a + b; } } class Subtraction implements Operation { int a; int b; int result; // Constructor, getters and setters public void perform() { result = a - b; } } class Multiplication implements Operation { int a; int b; int result; // Constructor, getters and setters public void perform() { result = a * b; } } class Calculator { public void calculate(Operation operation) { // Null checks operation.perform(); } }
Benefits
Suppose clients are using existing class and if we modify that class there is a chance of introduction of bugs in the existing features. So it is better to create a new class so that existing features are untouched.
LSP states that if we substitute a superclass object reference with an object of any of its subclasses, the program should not break.
Correct substitution:
interface Fruit { void eat(); } class Apple implements Fruit { @Override void eat() { // Eat apple } } class Orange implements Fruit { @Override void eat() { // Eat orange } }
Wrong substitution: Duck test
interface Bird { void speak(); } class Duck implements Bird { @Override void speak() { // Duck quacks } void addBattery(){} }
Another Wrong substitution: IS-A trick – A square is a rectangle
class Rectangle { int length; int breadth; // Getters public void setBreadth(int breadth) { this.breadth = breadth; } public void setLength(int length) { this.length = length; } int getArea() { return length * breadth; } } class Square extends Rectangle { @Override public void setBreadth(int breadth) { super.setBreadth(breadth); super.setLength(breadth); } @Override public void setLength(int length) { super.setBreadth(length); super.setLength(length); } }
ISP states that no client should be forced to depend on methods it does not use.
Example violating ISP
interface MediaPlayer { void playAudio(); void playVideo(); }
Correct use of ISP
interface AudioPlayer { void playAudio(); } interface VideoPlayer { void playVideo(); } class VLC implements AudioPlayer,VideoPlayer { } class Winamp implements AudioPlayer { }
Benefit:
A client will use interfaces only it wants. So it will create less bugs.
Robert C. Martin’s definition of the Dependency Inversion Principle consists of two parts:
1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend on details. Details should depend on abstractions.
Example
interface Car { List<Seat> getSeats(); } interface Seat {} class AudiR8Car implements Car { List<Seat> seats; @Override List<Seat> getSeats() { // return 2 seats; } } class LeatherSeat implements Seat { }
Benefit:
Loose coupling of the code
Dependency injection is a technique in which an object receives other objects that it depends on.
Types of Injections
1. Constructor Injection
2. Property Injection
3. Method Injection
interface Car { void setTheSeats(List<Seat> seats); } class AudiR8Car implements Car { List<Seat> seats; // Constructor injection AudiR8Car(List<Seat> seats) { this.seats = seats; } // Setter injection void setSeats(List<Seat> seats) { this.seats = seats; } // Method injection @Override void setTheSeats(List<Seat> seats) { this.seats = seats; } }
IoC inverts the flow of control as compared to traditional control flow.
Traditional flow:
class TextEditor { SpellChecker spellChecker; public TextEditor() { this.spellChecker = new SpellChecker(); } }
public class TextEditor { SpellChecker spellChecker; public TextEditor(SpellChecker spellChecker) { this.spellChecker = spellChecker; } }
Dependency Injection is an implementation technique for populating instance variables of a class.
Dependency Inversion principle recommends that classes should only have direct relationships with high-level abstractions. It uses dependency injection to achieve it.
Inversion of Control inverts the flow of control as compared to traditional control flow. It uses dependency injection to do it.
1. Single responsibility principles states that each class should have a single responsibilily.
2. Open closed principle states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. It encourages use of polymorphism.
3. Liskov substitution principle states that if we substitute a superclass object reference with an object of any of its subclasses, the program should not break. LSP says use polymorphism but with caution. Think from the client’s perspective.
4. Interface segregation principle states let there be many interfaces but no client should be forced to depend on methods it does not use.
5. Dependency Inversion principle 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.