I think one of the most important sets of principles that should be understood by all software engineers is SOLID principles (not the only one, but the most basic). By understanding this principle, we assume that we'll be able to deliver good, cleaner and maintainable codes.
SOLID is an acronym that stands for Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. It's a best practice for developing a program using object-oriented programming.
There are so many articles that have discussed it. But, I think it is still a worthy topic to rediscuss about. Let's try to discuss this in the easiest way possible. This article also includes example codes in Java.
Single Responsibility Principle
The main goal of this principle is a single class should only responsible for one purpose. That means we will violate this principle if we create a class that consists of multiple purposes.
By following this principle, it will give us benefits:
Our code will be more modular.
This can be easier for us to trace our code in the future as we already determine the specific purpose of each class.
Wrong example
public class Person {
private String firstname;
private String lastname;
private String origin;
private String age;
private String sex;
//constructor, getters and setters
// methods that directly relate to the person properties
public String returnFullName() {
return firstname + " " + lastname;
}
}
Then we add the storeInDatabase
method.
public class Person {
private String firstname;
private String lastname;
private String origin;
private String age;
private String sex;
//constructor, getters and setters
// methods that directly relate to the person properties
public String returnFullName() {
return firstname + " " + lastname;
}
public void storeInDatabase(Person person) {
// codes that perform database action
}
}
What goes wrong :
-
storeInDatabase
is not directly related to or responsible forPerson
class.
Correct example
To fix this, we can put storeInDatabase
method in a new class, let say PersonDatabase
class.
public class PersonDatabase {
public void storeInDatabase(Person person) {
// codes that perform database action
}
}
Open-Closed Principle
This principle emphasizes in a class should be open for extension but closed for modification.
The benefit of following this principle is we don't modify the existing code which can probably cause new bugs. (But, it's an exception for bugs in existing code, you should fix it, not extend it 😁).
Example
Suppose that we have Car
class like below.
public class Car {
private String brand;
private String color;
// constructors, getters, and setters
}
Then, we want a Truck
class that has the same properties with Car
class, with some additions. Instead of modifying Car
class, we can extend it to Truck
class like below.
public class Truck extends Car {
private int cargoVolume;
// constructors, getters, and setters
}
By doing so, we don't need to modify the Car
class that might be already used in some part of our program.
If you want to create another class like Ambulance
class in the future, you can extend the Car
class like you did with Truck
class.
public class Ambulance extends Car {
private hasbloodPressureGague;
private hasThermometer;
// constructors, getters, and setters
}
Liskov Substitution Principle
This principle is arguably the most complex principle among these 5 principles. The simple meaning of this principle may be a subclasses should be substitutable for their base classes.
Let say, Toyota
class is a subclass of Car
class. Toyota
class should be able to replace Car
class without disrupting the behavior of our program. Show this example :
class Car {
private String brand;
public Car(String brand) {
this.brand = brand;
}
public void startEngine() {
System.out.println("Engine started.");
}
public void stopEngine() {
System.out.println("Engine stopped.");
}
}
class Toyota extends Car {
public Toyota() {
super("Toyota");
}
@Override
public void startEngine() {
System.out.println("Toyota engine started.");
}
@Override
public void stopEngine() {
System.out.println("Toyota engine stopped.");
}
public void engageAutopilot() {
System.out.println("Autopilot engaged.");
}
}
In this Java example, the Car
class represents a generic car and a Toyota
class represents a specific type of car (Toyota brand) that extends the behavior of the base class.
We can use the Car
class and the Toyota
class interchangeably, as demonstrated below:
public class Main {
public static void startCar(Car car) {
car.startEngine();
}
public static void stopCar(Car car) {
car.stopEngine();
}
public static void engageAutopilot(Car car) {
if (car instanceof Toyota) {
Toyota toyota = (Toyota) car;
toyota.engageAutopilot();
} else {
System.out.println("Autopilot not available for this car.");
}
}
public static void main(String[] args) {
Car myCar = new Car("SomeBrand");
startCar(myCar); // Output: Engine started.
stopCar(myCar); // Output: Engine stopped.
Toyota myToyota = new Toyota();
startCar(myToyota); // Output: Toyota engine started.
stopCar(myToyota); // Output: Toyota engine stopped.
engageAutopilot(myCar); // Output: Autopilot not available for this car.
engageAutopilot(myToyota); // Output: Autopilot engaged.
}
}
The startCar()
and stopCar()
methods accept a Car
object as a parameter. We can pass both a Car
instance (myCar
) and a Toyota
instance (myToyota
) to these methods without any issues.
The engageAutopilot()
method checks if the provided Car object is an instance of Toyota
. If it is, it casts it to a Toyota
object and calls the engageAutopilot()
method. Otherwise, it handles the case where autopilot functionality is not available for non-Toyota
cars. This demonstrates the usage of the Toyota
class as a subtype of the Car
class without violating the Liskov substitution principle.
Interface Segregation Principle
Segregation means keeping different things separate. This principle is about separating interfaces based on their purpose.
The principle states that many client-specific interfaces are better than one general-purpose interface. Clients should not be forced to implement a function they do not need.
interface Worker {
void work();
void sleep();
}
Let's say that we have 2 classes that implement Worker
interface, Programmer
and Robot
class.
class Programmer implements Worker {
public void work() {
System.out.println("Programmer is working.");
// Perform programming tasks
}
public void sleep() {
System.out.println("Programmer is sleeping.");
// Sleep at night
}
}
class Robot implements Worker {
public void work() {
System.out.println("Robot is working.");
// Perform robotic tasks
}
// Can't be implemented
public void sleep() {
// Robots don't sleep!
throw new UnsupportedOperationException("Robots don't sleep!");
}
}
As we see, sleep() method can be implemented by the Programmer class, but not by Robot
class. To solve this, we can split Worker
interface.
interface Worker {
void work();
}
interface LifeBeing {
void sleep();
}
The Programmer
class now implements all two interfaces, as it performs all the associated actions. However, the Robot
class only implements the Worker
interface, as it doesn't need to sleep.
class Programmer implements Worker, LifeBeing {
public void work() {
System.out.println("Programmer is working.");
// Perform programming tasks
}
public void sleep() {
System.out.println("Programmer is sleeping.");
// Sleep at night
}
}
class Robot implements Worker {
public void work() {
System.out.println("Robot is working.");
// Perform robotic tasks
}
}
Dependency Inversion Principle
This principle refers to the decoupling of the software modules. Dependency inversion is a design principle in object-oriented programming that states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions.
// High-level module
class EmployeeTracker {
private EmployeeService employeeService;
public EmployeeTracker(EmployeeService employeeService) {
this.employeeService = employeeService;
}
// other methods
}
}
// Abstraction (interface) for employee service
interface EmployeeService {
String getBranch(String employeeId);
String getWage(String employeeId);
String getPosition(String employeeId);
}
// Low-level module implementing the employee service
class EmployeeServiceImpl implements EmployeeService {
public String getBranch(String employeeId) {
// Logic to retrieve employee data
return employee.getBranch();
}
public String getWage(String employeeId) {
// Logic to retrieve employee data
return employee.getWage();
}
public String getPosition(String employeeId) {
// Logic to retrieve employee data
return employee.getPosition();
}
}
High-level module called EmployeeTracker
needs to retrieve employee data. Instead of directly depending on the specific implementation, it depends on the EmployeeService
abstraction/interface.
The EmployeeServiceImpl
class is a low-level module that implements the EmployeeService
interface. It contains the specific implementation details for retrieving employee data, such as making API calls.
EmployeeTracker
class doesn't depend on these implementation details, promoting loose coupling and ensuring that the high-level module is not tightly coupled to specific low-level modules.
By applying this principle, we invert the dependency relationship between high-level and low-level modules. The high-level module depends on abstractions, while the low-level modules depend on those abstractions. This promotes modular and flexible code design.
Conclusion
That's all a short explanation of SOLID principles. You can boost your code quality by applying these principles to your code. There are several benefits we'll get by following these principles :
Maintainability: Easier code maintenance and modification.
Scalability: Ability to add new features without modifying existing code.
Testability: Facilitates effective unit testing and code verification.
Reusability: Creation of reusable and interchangeable components.
Flexibility: Greater adaptability and ability to switch implementations.
Readability and Understandability: Clear, organized code structure for easier comprehension and collaboration.
Additional Resources
A Solid Guide to SOLID Principles from Baeldung
The SOLID Principles of Object-Oriented Programming Explained in Plain English from freeCodeCamp
What are the SOLID principles in Java? from educative
Top comments (1)
Really good article.
A very nice quick&simple recap of these mandatory principles.
Just a little typo in the end
I suppose you mean that low-level ones implement those abstractions.