SOLID Principles: A Comprehensive Guide With Examples in Java
- Get link
- X
- Other Apps
SOLID Principles: A Comprehensive Guide
Table of Contents:
- Single responsibility principle
- Open-Closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
SOLID principles are object-oriented design concepts relevant to software development. the acronym S.O.L.I.D consists of five design principles for writing maintainable and scalable software.
This article provides practical guidance on how to incorporate SOLID principles into your projects.
So grab a cup of coffee or tea and let's jump right in! 🍵🚀
The Principles Covered In This Article Include:
1. Single Responsibility Principle(SRP):
The SRP states that a class should have only one reason to change. Meaning it should have only one responsibility. This promotes modularity and maintainability. To be precise, Let each class, Module, or Method handle a specific task.
2. Open-Closed Principle(OCP):
The Open-Closed Principle (OCP) is a fundamental concept in software engineering, which emphasizes designing software entities in a way that allows for easy extension without modifying the existing code. This means that software entities should be open for extension but closed for modification, using abstract classes and interfaces. Following this principle makes the code more maintainable, scalable, and flexible, even when there are changes in requirements and user needs.
Example in Java
Good Example: Using Interface and Implementation
// Person.java (Interface)
public interface Person {
void displayDetails();
}
// Student.java (Implementation)
public class Student implements Person {
private String name;
private int grade;
public Student(String name, int grade) {
this.name = name;
this.grade = grade;
}
@Override
public void displayDetails() {
System.out.println("Student: " + name + ", Grade: " + grade);
}
}
// Teacher.java (Implementation)
public class Teacher implements Person {
private String name;
private String subject;
public Teacher(String name, String subject) {
this.name = name;
this.subject = subject;
}
@Override
public void displayDetails() {
System.out.println("Teacher: " + name + ", Subject: " + subject);
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Person student = new Student("Alice", 8);
Person teacher = new Teacher("Mr. Smith", "Math");
student.displayDetails();
teacher.displayDetails();
}
}
//Bad Example: Without Interface and Implementation
// Person.java
public class Person {
protected String name;
public Person(String name) {
this.name = name;
}
public void displayStudentDetails(int grade) {
System.out.println("Student: " + name + ", Grade: " + grade);
}
public void displayTeacherDetails(String subject) {
System.out.println("Teacher: " + name + ", Subject: " + subject);
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Person person = new Person("Alice");
// Bad: Need to modify existing code for each new type
person.displayStudentDetails(8);
person.displayTeacherDetails("Math");
}
}
// Person.java (Interface)
public interface Person {
void displayDetails();
}
// Student.java (Implementation)
public class Student implements Person {
private String name;
private int grade;
public Student(String name, int grade) {
this.name = name;
this.grade = grade;
}
@Override
public void displayDetails() {
System.out.println("Student: " + name + ", Grade: " + grade);
}
}
// Teacher.java (Implementation)
public class Teacher implements Person {
private String name;
private String subject;
public Teacher(String name, String subject) {
this.name = name;
this.subject = subject;
}
@Override
public void displayDetails() {
System.out.println("Teacher: " + name + ", Subject: " + subject);
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Person student = new Student("Alice", 8);
Person teacher = new Teacher("Mr. Smith", "Math");
student.displayDetails();
teacher.displayDetails();
}
}
//Bad Example: Without Interface and Implementation
// Person.java
public class Person {
protected String name;
public Person(String name) {
this.name = name;
}
public void displayStudentDetails(int grade) {
System.out.println("Student: " + name + ", Grade: " + grade);
}
public void displayTeacherDetails(String subject) {
System.out.println("Teacher: " + name + ", Subject: " + subject);
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Person person = new Person("Alice");
// Bad: Need to modify existing code for each new type
person.displayStudentDetails(8);
person.displayTeacherDetails("Math");
}
}
3. Liskov Substitution Principle(LSP):
In object-oriented programming, the Liskov Substitution Principle states that objects of a parent class should be able to be replaced with objects of a child class without causing any errors or issues within the program. This principle is particularly relevant to inheritance hierarchies, where child classes should extend the functionality of their parent class without altering its behavior.
// Bad: Violating LSP
class Bird {
void fly() {
// ...
}
}
class Penguin extends Bird {
// Penguins can't fly
void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
// Good: Adhering to LSP
interface FlyingBird {
void fly();
}
class Sparrow implements FlyingBird {
@Override
public void fly() {
// ...
}
}
class Penguin implements SwimmingBird {
// Penguins can swim
void swim() {
// ...
}
}
In the improved code, we use interfaces and adhere to LSP by allowing classes to extend behavior appropriately without violating expected functionality.
4. Interface segregation principle(ISP):
According to the Interface Segregation Principle (ISP), it is considered good practice in programming to avoid making classes implement interfaces that they do not need. This means that classes should not be required to implement irrelevant interfaces, as it can lead to unnecessary complexity and hinder the class's functionality. In essence, it is important to only implement interfaces that are essential to a class's purpose and avoid adding unnecessary overhead.
// Bad: One large interface
interface Worker {
void work();
void eat();
}
// Good: Segregated interfaces
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Robot implements Workable {
@Override
public void work() {
// ...
}
}
class Human implements Workable, Eatable {
@Override
public void work() {
// ...
}
@Override
public void eat() {
// ...
}
}
5. Dependency Inversion Principle:
The Dependency Inversion Principle (DIP) suggests that modules with a high level of abstraction should not rely on modules with a lower level of abstraction. Instead, both should depend on abstractions, such as interfaces and abstract classes. This means that abstractions should not depend on specific details, but instead, the details should depend on abstractions.
// Bad: High-level module depending on low-level module
class LightBulb {
void turnOn() {
// ...
}
}
class Switch {
private LightBulb bulb;
Switch(LightBulb bulb) {
this.bulb = bulb;
}
void operate() {
bulb.turnOn();
}
}
// Good: Abstractions used for dependency inversion
interface Switchable {
void turnOn();
}
class LightBulb implements Switchable {
@Override
public void turnOn() {
// ...
}
}
class Switch {
private Switchable device;
Switch(Switchable device) {
this.device = device;
}
void operate() {
device.turnOn();
}
}
Conclusion
Feel free to contact me via my email or leave your comments, questions, or suggestions in the comment box as I'll be happy to receive them and try my best to answer them.
. . .
contact me at ngiate24@gmail.com for general inquiries, collaborations, etc..
Thank you for reading!
See you on my next post 👋🏽✨
- Get link
- X
- Other Apps
Comments
Post a Comment