Want to Write Better Code? Here’s How to Do It! Explained with memes 😏

S.O.L.I.D Principle: The Superhero’s Guide to Writing Clean Code!

Save Your Code from the Dark Abyss of Spaghetti Code with These Simple Principles!

Shuhan Mirza
8 min readFeb 17, 2023

--

Scene from Bruce Almighty

If you’re a software developer, you may have heard about S.O.L.I.D. It’s an acronym that stands for:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

These principles were first introduced by Robert C. Martin (a.k.a Uncle Bob), and they aim to help developers build software that is easier to maintain, extend and understand.

Let’s dive into each principle in more detail, with some example Java code to help illustrate the concepts.

1. Single Responsibility Principle (SRP)

The SRP states that a class should have only one sole purpose. In other words, a class should have only one responsibility. This principle helps to ensure that your code is focused, readable, and easy to maintain. We should not burden a class with excessive functionality.

Made by the author using imgflip.com

Let’s say you have a User class that handles user authentication and email notifications. Here's an example of how you can refactor this class to follow the SRP:

// Before refactoring

public class User {
public void login(String username, String password) {
// Authenticate user
}

public void sendEmail(String to, String subject, String body) {
// Send email
}
}
// After refactoring

public class User {
public void login(String username, String password) {
// Authenticate user
}
}

public class EmailService {
public void sendEmail(String to, String subject, String body) {
// Send email
}
}

2. Open/Closed Principle (OCP)

The OCP states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. In other words, you should be able to add new functionality to your code without changing the existing code.

Made by the author using imgflip.com

Let’s say you have a PaymentProcessor class that handles payment processing for different payment methods. Here's an example of how you can refactor this class to follow the OCP:

// Before refactoring

public class PaymentProcessor {
public void processPayment(Payment payment) {
if (payment.getMethod() == PaymentMethod.CREDIT_CARD) {
// Process credit card payment
} else if (payment.getMethod() == PaymentMethod.PAYPAL) {
// Process PayPal payment
} else if (payment.getMethod() == PaymentMethod.BITCOIN) {
// Process Bitcoin payment
}
}
}
// After refactoring

public interface PaymentMethodProcessor {
void processPayment(Payment payment);
}

public class CreditCardProcessor implements PaymentMethodProcessor {
public void processPayment(Payment payment) {
// Process credit card payment
}
}

public class PayPalProcessor implements PaymentMethodProcessor {
public void processPayment(Payment payment) {
// Process PayPal payment
}
}

public class BitcoinProcessor implements PaymentMethodProcessor {
public void processPayment(Payment payment) {
// Process Bitcoin payment
}
}

public class PaymentProcessor {
private Map<PaymentMethod, PaymentMethodProcessor> processors = new HashMap<>();

public PaymentProcessor() {
processors.put(PaymentMethod.CREDIT_CARD, new CreditCardProcessor());
processors.put(PaymentMethod.PAYPAL, new PayPalProcessor());
processors.put(PaymentMethod.BITCOIN, new BitcoinProcessor());
}

public void processPayment(Payment payment) {
PaymentMethodProcessor processor = processors.get(payment.getMethod());
if (processor != null) {
processor.processPayment(payment);
}
}
}

Now, we can add a ‘Stripe Payment Processor’ class without changing existing codes.

3. Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without breaking the application. In other words, a subclass should be able to be used in place of its superclass without causing unexpected behavior or errors.

Made by the author using imgflip.com

Let’s say you have a Rectangle class that calculates the area of a rectangle. Here's an example of how you can refactor this class to follow the LSP:

// Before refactoring

public class Rectangle {
private int width;
private int height;

public int getWidth() {
return width;
}

public void setWidth(int width) {
this.width = width;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

public int getArea() {
return width * height;
}
}

public class Square extends Rectangle {
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}

public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}

// Test code
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // Expected output: 50

In this example, the Square class inherits from the Rectangle class, but it violates the LSP because it changes the behavior of the superclass. The Square class sets both the width and height to the same value, which is not what you would expect from a rectangle. To fix this, you can change the design to use separate Square and Rectangle classes.

// After refactoring

public class Rectangle {
protected int width;
protected int height;

public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}

public int getWidth() {
return width;
}

public void setWidth(int width) {
this.width = width;
}

public int getHeight() {
return height;
}

public void setHeight(int height) {
this.height = height;
}

public int getArea() {
return width * height;
}
}

public class Square extends Rectangle {
public Square(int size) {
super(size, size);
}
}

// Test code
Rectangle rect = new Rectangle(5, 10);
System.out.println(rect.getArea()); // Expected output: 50

Square square = new Square(5);
System.out.println(square.getArea()); // Expected output: 25

4. Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) states that a client should not be forced to implement interfaces they do not use. In other words, you should separate interfaces into smaller, more focused ones that are easier to implement and change.

Made by the author using imgflip.com

Let’s say you have a Printer interface that has two methods: print() and scan(). Here's an example of how you can refactor this interface to follow the ISP:

// Before refactoring

public interface Printer {
void print();
void scan();
}

public class LaserPrinter implements Printer {
public void print() {
// Print document
}

public void scan() {
// Scan document
}
}

public class InkjetPrinter implements Printer {
public void print() {
// Print document
}

public void scan() {
// Do nothing
}
}
// After refactoring

public interface Printable {
void print();
}

public interface Scanable {
void scan();
}

public class LaserPrinter implements Printable, Scanable {
public void print() {
// Print document
}

public void scan() {
// Scan document
}
}

public class InkjetPrinter implements Printable {
public void print() {
// Print document
}
}

// Test code
Printable laserPrinter = new LaserPrinter();
laserPrinter.print();
((Scanable)laserPrinter).scan();

Printable inkjetPrinter = new InkjetPrinter();
inkjetPrinter.print();

In this example, the Printer interface has two methods, print() and scan(). The LaserPrinter and InkjetPrinter classes both implement the Printer interface, but the InkjetPrinter class doesn’t use the scan() method, which violates the ISP. To fix this, you can create separate Printable and Scanable interfaces that are easier to implement and use.

5. Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. In other words, your code should depend on interfaces or abstract classes, rather than concrete implementations.

Made by the author using imgflip.com

Let’s say you have a Book class that needs to retrieve data from a Database class. Here’s an example of how you can refactor this code to follow the DIP:

// Before refactoring

public class Book {
private Database database;

public Book() {
this.database = new Database();
}

public String getTitle(int id) {
return database.getTitle(id);
}
}
public class Database {
public String getTitle(int id) {
// Retrieve title from database
return "Title";
}
}
// After refactoring

public interface Database {
String getTitle(int id);
}
public class Book {
private Database database;

public Book(Database database) {
this.database = database;
}

public String getTitle(int id) {
return database.getTitle(id);
}
}
public class DatabaseImpl implements Database {
public String getTitle(int id) {
// Retrieve title from database
return "Title";
}
}
// Test code
Database database = new DatabaseImpl();
Book book = new Book(database);
System.out.println(book.getTitle(123)); // Expected output: "Title"

In this example, the Book class depends on the Database class, which violates the DIP. To fix this, you can create a Database interface that the Book class depends on, and create a separate DatabaseImpl class that implements the interface. This way, the Book class depends on an abstraction, rather than a concrete implementation. Following DIP makes the code pluggable. How? In the above example, we can switch to a different implementation of the database ( e.g. migrating to PgSQL from MySql) and the system won’t break.

Conclusion

The SOLID principles are a set of guidelines that can help you write better, more maintainable code. By following these principles, you can make your code more modular, flexible, and reusable.

Remember that these principles are not hard and fast rules, but rather guidelines to help you design better software. As with any guideline, there may be cases where it makes sense to deviate from the principles in order to achieve a specific goal. However, in general, following the SOLID principles will help you write cleaner, more maintainable, and more extensible code.

To summarize, the SOLID principles can be summed up as:

  • SRP: A class should have only one sole purpose.
  • OCP: Software entities should be open for extension but closed for modification.
  • LSP: Subtypes should be substitutable for their base types.
  • ISP: Clients should not be forced to depend on interfaces they do not use.
  • DIP: High-level modules should not depend on low-level modules. Both should depend on abstractions.

By applying these principles, you can improve the quality of your code and make it easier to maintain and extend over time. Remember that good design is an ongoing process, and it’s important to continually evaluate and refactor your code as necessary.

As a final note, it’s worth mentioning that the SOLID principles are just one aspect of good software design. There are many other design principles and best practices that you can apply to your code to make it more effective and maintainable. However, by following the SOLID principles, you can establish a solid foundation for your software and set yourself up for success in the long run.

If you want to learn more about the SOLID principles, there are many great resources available online, including books, articles, and videos. You can also explore other design patterns and best practices that can help you write better code.

I encourage you to continue learning and exploring new ideas and techniques for improving your software design. By investing in your skills and knowledge, you can become a better, more effective software developer and contribute to the success of your team and organization.

Thank you for reading, and happy coding!

Want to Connect? If you have any feedback, 
please ping me on my LinkedIn: https://linkedin.com/in/shuhanmirza/
Source

--

--