Introduction:
SOLID is an acronym that represents a set of five design principles for writing maintainable and scalable software. These principles were introduced by Robert C. Martin and have become fundamental concepts in object-oriented programming. In this blog article, we will explore each of the SOLID principles and provide Java code examples to illustrate their application.
Single Responsibility Principle (SRP):
The SRP states that a class should have only one reason to change. In other words, a class should have only one responsibility.
Let's consider an example where we have a Customer class that handles both customer data and customer-related calculations:
public class Customer {
private String name;
private double balance;
public void setName(String name) {
this.name = name;
}
public void setBalance(double balance) {
this.balance = balance;
}
public double calculateDiscount() {
// Calculate discount based on balance
// ...
}
public void saveToDatabase() {
// Save customer data to the database
// ...
}
}
In this case, the Customer class violates SRP. We can refactor it into two classes, Customer for data and DiscountCalculator for calculations, to adhere to SRP.
Solution:
Refactoring the Customer class into two separate classes, one for handling customer data and another for calculating discounts, is a great way to adhere to the Single Responsibility Principle (SRP). This separation of concerns makes your code more maintainable and easier to understand. It also allows for better code reuse, as the discount calculation logic can be used elsewhere in your application without duplicating code.
Here's a revised version of the code following this separation:
public class Customer {
private String name;
private double balance;
public Customer(String name, double balance) {
this.name = name;
this.balance = balance;
}
public String getName() {
return name;
}
public double getBalance() {
return balance;
}
}
public class DiscountCalculator {
public double calculateDiscount(Customer customer) {
if (customer.getBalance() >= 100.0) {
return customer.getBalance() * 0.10;
}
return 0.0;
}
}
In this code, we have a DiscountCalculator class that calculates the discount based on the customer's balance. When you run the program with a customer whose balance is $120, it will calculate and display a $12 discount (which is 10% of $120).
public class Main {
public static void main(String[] args) {
// Create a customer with a balance of $120
Customer customer = new Customer("Alice", 120.0);
// Create a DiscountCalculator
DiscountCalculator calculator = new DiscountCalculator();
// Calculate the discount for the customer
double discount = calculator.calculateDiscount(customer);
// Display the discount
System.out.println("Discount for " + customer.getName() + ": $" + discount);
}
}
Open/Closed Principle (OCP):
The OCP states that software entities (classes, modules, functions) should be open for extension but closed for modification. This encourages the use of interfaces and abstract classes.
Let's create a simple example:
public interface Shape {
double area();
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
Here, we can easily add new shapes (e.g., Triangle) without modifying existing code.
Liskov Substitution Principle (LSP):
The LSP states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
Let's demonstrate this with a Java example:
public class Bird {
void fly() {
// Default fly behavior
}
}
public class Sparrow extends Bird {
@Override
void fly() {
// Specific flying behavior for Sparrow
}
}
public class Ostrich extends Bird {
// Ostrich cannot fly, so we don't override fly()
}
In this case, the Ostrich class violates the rule. We can refactor it. Here's a corrected example that better illustrates the principle:
public interface Flyable {
void fly();
}
public class Bird {
void move() {
// Default move behavior
}
}
public class Sparrow extends Bird implements Flyable {
@Override
void move() {
// Specific flying behavior for Sparrow
System.out.println("Sparrow is flying.");
}
@Override
public void fly() {
move(); // Call the overridden move method to demonstrate flying
}
}
public class Ostrich extends Bird {
@Override
void move() {
// Specific running behavior for Ostrich
System.out.println("Ostrich is running.");
}
}
public class Main {
public static void main(String[] args) {
Sparrow sparrow = new Sparrow();
Ostrich ostrich = new Ostrich();
// Demonstrate flying for Sparrow
sparrow.fly();
// Demonstrate running for Ostrich
ostrich.move();
}
}
Now, we have a Flyable interface, and the Sparrow class implements it by providing the fly method. The Ostrich class, which can't fly, doesn't implement the Flyable interface.
In the Main class, you can see how to use the fly and move methods to demonstrate flying for sparrows and running for ostriches.
Interface Segregation Principle (ISP):
This principle suggests that clients (classes or modules) should not be forced to depend on interfaces they don't use. In other words, it promotes the idea of having smaller, more specific interfaces rather than large, monolithic ones.
Consider a scenario where we have an interface called Worker that represents workers who have both working and eating behaviors:
public interface Worker {
void work();
void eat();
}
Now, let's say we have two classes, Engineer and Waiter, that implement this interface:
public class Engineer implements Worker {
@Override
public void work() {
// Engineer-specific work
}
@Override
public void eat() {
// Engineer-specific eating
}
}
public class Waiter implements Worker {
@Override
public void work() {
// Waiter-specific work
}
@Override
public void eat() {
// Waiter-specific eating
}
}
In this scenario, both Engineer and Waiter must implement both the work and eat methods, even though they may not use both behaviors. This violates the ISP because clients (in this case, the Engineer and Waiter classes) are forced to depend on methods they may not need.
To adhere to the ISP, we can create smaller, more specific interfaces for each behavior:
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
Now, our Engineer and Waiter classes can implement only the interfaces they need:
public class Engineer implements Workable {
@Override
public void work() {
// Engineer-specific work
}
}
public class Waiter implements Workable, Eatable {
@Override
public void work() {
// Waiter-specific work
}
@Override
public void eat() {
// Waiter-specific eating
}
}
By doing this, we adhere to the Interface Segregation Principle, and clients are not forced to implement methods they don't use. This results in more flexible and maintainable code.
Dependency Inversion Principle (DIP):
The DIP states that high-level modules should not depend on low-level modules; both should depend on abstractions. Let's illustrate this with an example:
public class LightBulb {
void turnOn() {
// Turn on the light bulb
}
}
public class Switch {
private LightBulb bulb;
public Switch(LightBulb bulb) {
this.bulb = bulb;
}
void operate() {
bulb.turnOn();
}
}
In this case, Switch directly depends on LightBulb, violating DIP. We can introduce an abstraction to decouple them:
Solution:
public interface Switchable {
void turnOn();
}
public class LightBulb implements Switchable {
@Override
public void turnOn() {
// Turn on the light bulb
}
}
public class Switch {
private Switchable device;
public Switch(Switchable device) {
this.device = device;
}
void operate() {
device.turnOn();
}
}
Conclusion:
Understanding and applying the SOLID principles in your Java code can lead to more maintainable, extensible, and robust software. By following these principles, you can improve code quality and make it easier to adapt to changing requirements. Remember that SOLID is not about following rules blindly but about guiding you to write better software.
Top comments (2)
Hi. Consider using syntax highlighting with your code examples. It will improve readability. Here is how to do it.
The above produces:
It works for most languages. Just change Java to relevant language name.
Thank you.