DEV Community

Cover image for 5 Simple Rules for Better Object-Oriented Programming: SOLID
Mukhil Padmanabhan
Mukhil Padmanabhan

Posted on

5 Simple Rules for Better Object-Oriented Programming: SOLID

So, you’re new to software development and in particular object-oriented programming (OOP), but you’ve probably heard of the SOLID principles. These principles are like a blueprint that help point you towards writing cleaner, more maintainable and more scalable code. In this blog I’ll introduce you to the SOLID principles in a way that’s easy to understand if you’re just starting out in the tech industry.

Read on, and in just a few minutes — about the time it takes to drink a couple cups of coffee — you will have learned what each of the SOLID principles is, what they stand for, why they are important, and some code samples showing how to apply them in your own projects.


What Are the SOLID Principles?

The SOLID principles are a set of five guidelines for writing software that makes your code more maintainable, easier to understand, and less likely to break. They were created by Robert C. Martin, often referred to as “Uncle Bob”, and each letter in the word SOLID represents one of these five principles:

  1. S – Single Responsibility Principle (SRP)
  2. O – Open/Closed Principle (OCP)
  3. L – Liskov Substitution Principle (LSP)
  4. I – Interface Segregation Principle (ISP)
  5. D – Dependency Inversion Principle (DIP)

These principles are given by Uncle Bob to write the clean code which is easily maintainable. By these, we can achieve the less or no effect of the existing functionality in case when we are adding new features.


Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. In other words, every class or module in your code should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.

Why It’s Important:

  • Simplicity: Classes that assume multiple responsibilities become complex and hard to maintain.
  • Maintainability: If a class has a single responsibility, it’s easier to update, test, and debug.

Example:

Shopping cart system as an example. You would probably have a Cart class that would deal with adding and removing items, calculating the total cost and processing the payment. That’s too much work for one class!

To follow SRP, you should split the responsibilities. For example:

  • Cart class should be responsible to add and remove items.
  • PaymentProcessor class should be responsible for handling payment transactions.
  • OrderSummary class should be calculating the total cost.

This way if something changes in the payment process you only need to modify the PaymentProcessor class and don’t have to touch anything else like the Cart class or other parts of your program.


Open-Closed Principle (OCP)

Software entities (classes, modules, functions) should be open for extension, but closed for modification. In other words, you should be able to add new functionality to a class without changing its existing code.

Why It’s Important:

  • Stability - modifying existing code can sometimes break things, especially if you don’t have tests that are act as your safety net. You want to be able to make changes and know that the core functionality of your system does what it’s supposed to.
  • Extensibility: You can add new features or functionality to a system without modifying the existing system.

Example:

Suppose you’re building a notification system for an app, and you have a Notification class that sends emails. Later, you need to add functionality for sending SMS and push notifications.

Instead of modifying the Notification class directly, you could extend it:

  • Create a new EmailNotification class for email functionality.
  • Create a new SMSNotification class for SMS functionality.
  • Create a new class PushNotification for push notifications.

And again, if you add more notification types in the future (like WhatsApp or Slack notifications), you will be able to extend the notification system without modifying anything within the core Notification class.


Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without breaking the application. Or, in other words, if a class B is a subtype of class A then we should be able to substitute B wherever we use A without any issue.

Why It’s Important:

  • Polymorphism: LSP helps you use polymorphism in object-oriented programming where an interface can be used for objects of a base class or any sub class.
  • Predictability: If you adhere to LSP, callers of your base classes can assuming certain behaviors of derived classes, hence Predictability in the sense that if works on a particular instance then it must also work on all instances of same sub-type and makes your system more transparent and better maintainable.

Example:

Imagine you have class called Bird and you have method fly() in it. Now you inherit class Penguin from Bird. But penguins can’t fly so this method would be either unnecessary or breaks your code.

This is in violation of the Liskov substitution principle because replacing a Penguin object with a Bird object will cause our program to go awry. We could fix this issue by introducing another subclass called FlyingBird and placing the fly() method there. After doing so, we could derive different kinds of birds (Eagle, Sparrow,...) from FlyingBird, while Penguin and other flightless birds would simply inherit from Bird.


Interface Segregation Principle (ISP)

The Interface Segregation Principle states that a client should not be forced to implement interfaces they don’t use. This means that instead of creating large, all-encompassing interfaces, you should create smaller, more specific ones.

Why It’s Important:

  • Flexibility: Smaller interfaces are easier to write and give developers more control over what they want a type to do or not do.
  • Decoupling: By splitting interfaces, you reduce unnecessary dependencies and decouple components more effectively.

Example:

Imagine you’re building a system for a printer, and you have an interface called IMachine with methods like print(), scan(), and fax(). Now, what if some printers only need the print() method and don’t support scanning or faxing?

Instead of forcing every printer to implement all the methods, you can create smaller, more focused interfaces:

  • IPrinter with a print() method.
  • IScanner with a scan() method.
  • IFaxMachine with a fax() method.

This way, each printer only implements the interfaces it needs.


Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. In other words, instead of classes depending on specific classes, the contracts they follow need to depend on interfaces or abstracts.

Why It’s Important:

  • Loose Coupling: Depending on abstractions instead of concrete implementations, you make your system more flexible and change friendly.
  • Testablity: It’s easier to test your code when classes rely on interfaces because you can mock the dependencies in tests.

Example:

Suppose you have a DatabaseService class which is responsible to perform database related operation and your OrderService class directly depends on the DatabaseService to r to fetch data from the database.

Now, if you'd need to change database or maybe cloud services, you'd have to rewrite the OrderService class but with Dependency Inversion principle in mind you can create an interface called IDataService and make OrderService depend on this interface instead and then DatabaseService class (or any other data service) can implement this interface.


Benefits of Following SOLID Principles

  • Maintainability: Code becomes more understandable, changeable and less prone to errors.
  • Scalability: New functionality can be added with low risk of breaking existing functionality.
  • Testability: By writing modular code it is easier to test/mocks.
  • Flexibility: The codebase becomes more adaptable to changes in requirements, technology or scope.

Cons

  • Overhead for Beginners: Learning and using all five SOLID principles, it may look as an overhead for some beginners.
  • Over-Engineering: In some cases, by using SOLID we might end up in over engineering i.e. the solution provides more flexibility even for the simple things.
  • It’s not easy to set up a proper architecture following SOLID, especially when starting a project (note: the more components in your system, the more coding you’ll need, obviously).

Conclusion: Why SOLID matters

The SOLID principles are a blueprint for writing good object-oriented code. If you follow these five simple rules, it will be a lot easier to maintain your code, and your applications will be more flexible and scalable. In fact, the longer you work with these principles, the more you realize they not only make your code better, but also make you write code faster and in an optimized way. By following the SOLID principles, you will avoid pitfalls like duplicating code or having a tightly coupled system that is hard to maintain and extend. You will write software that is easy to debug and refactor. Most importantly though — you’ll be able to build higher-quality applications, and everyone in the team would appreciate working with cleaner pieces of software. Once you go further down this path into software engineering practices and start learning development methodologies for larger projects (Agile or Scrum) as well as stuff all over distributed systems — you’ll see how mastering those principles at the beginning of your career was crucial for everything else.

Top comments (0)