SOLID: The First 5 Principles of Object-Oriented Design?

SOLID: The First 5 Principles of Object-Oriented Design?

Introduction

SOLID is a mnemonic acronym for five principles of object-oriented design that were introduced by Robert C. Martin, who is popularly referred to as Uncle Bob. These principles are intended to help software designers, architects, engineers, and developers create more flexible, maintainable, and scalable software. By following these principles, you can design classes that are easier to test, refactor, reuse, and extend.

The SOLID acronym stands for:

S – Single Responsibility Principle

O – Open-Closed Principle

L – Liskov Substitution Principle

I – Interface Segregation Principle

D – Dependency Inversion Principle

In this article, we will explain each principle individually to understand how it can help you write better code. Additionally, we will add code snippets for each principle to show you how you can apply them in your coding journey, as well as what you should avoid in clean code architecture. To demonstrate the concepts, we will be using the Kotlin programming language developed by JetBrains and Open-source contributors.

Single Responsibility Principle

The Single Responsibility Principle (SRP) is a software design principle that states that every class or module in a program should have a single, well-defined responsibility. This means that a class should have only one reason to change, and it should be responsible for only a single part of the functionality of the program.

Consider the following code snippet as an example of how this principle can be applied in Kotlin:

The data  keyword in Kotlin indicates that this class is a data class, which means that it is intended to hold data and does not have any complex behavior. Using this data class, we can create a UserService  class for managing users as shown below:

In this example, the UserService class has a single responsibility: managing users in a database. Each user is represented by the data class User  code shared earlier.  All of the methods in the UserService class are related to this responsibility and are therefore cohesive. This makes the class easier to understand and maintain, as it is clear that all of the methods in the class are related to a single, well-defined task.

A method that violates the Single Responsibility Principle (SRP) in the UserService class would be one that is not related to the primary responsibility of the class, which is managing users in a database. For example, consider the following variation of the UserService class:

In this example, the sendEmail  method does not relate to the primary responsibility of the UserService class, which is managing users in a database. This method is responsible for sending emails, which is a separate concern from managing users. As a result, this method violates the SRP, as it introduces a second reason for the UserService class to change.

To adhere to the Single Responsibility Principle, it would be better to separate the email-sending functionality into a separate class, such as an EmailService  class. This would allow the UserService class to focus on its primary responsibility of managing users, and the EmailService class to focus on its responsibility of sending emails.

You should note that the Single Responsibility Principle is not about the number of methods a class has, but rather about the cohesiveness of the methods and the clear separation of responsibilities within a class.

Open-Closed Principle

The Open Closed Principle (OCP) is a software design principle that states that software entities (such as classes, modules, or functions) should be open for extension, but closed for modification. This means that it should be possible to add new functionality to a class or module without changing its existing code.

Consider the UserService class from the previous example. Suppose we want to add a new feature to the UserService class that allows us to search for users by email address. One way to do this would be to add a new method to the UserService class as highlighted in the code below:

This approach works, but it violates the Open Closed Principle, as we had to modify the existing UserService class in order to add the new feature. A better approach would be to use inheritance or composition to extend the functionality of the UserService class without modifying its code.

To achieve this, we could create a new class called UserSearchService  that extends the UserService class and adds the email search functionality:

In this example, the UserSearchService class is open for extension, as it provides additional functionality beyond what is offered by the UserService class. At the same time, the UserService class remains closed for modification, as we did not need to modify its code in order to add the email search feature.

Liskov Substitution Principle

The Liskov Substitution Principle (LSP) is a software design principle that states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. This means that a subclass should be a valid substitute for its superclass, and should behave in the same way as the superclass when used in the same context.

We will continue the demonstration using the User and the UserService classes from the previous examples. To allow the User class to be extended, we use the open  keyword in Kotlin:

Here is the original UserService class that uses the User data class above:

Suppose we want to create a subclass of User called AdminUser that represents users with administrative privileges. We might do this like this:

In this example, the AdminUser class is a valid substitute for the User class, as it behaves in the same way as the User class and can be used wherever a User object is expected. For example, we can use the AdminUser class with the UserService  class like this:

This code is correct, as the AdminUser class is a valid substitute for the User class and can be used in the same way as a User object.

It’s important to note that the Liskov Substitution Principle is about more than just inheritance. It’s about ensuring that objects of a subclass behave in the same way as objects of the superclass, regardless of how the subclass is implemented. For example, if the AdminUser class were to behave differently from the User class in some way, it would violate the Liskov Substitution Principle, as it would not be a valid substitute for the User class.

Interface Segregation Principle

The Interface Segregation Principle (ISP) is a software design principle that states that clients should not be forced to depend on interfacesthey do not use. This means that it is generally a good idea to create small, focused interfaces that do one thing well, rather than creating large, general-purpose interfaces that try to do many things.

Here’s an example of how this principle can be applied by rewriting the User  and UserService  classes from the previous examples:

In this example, the UserService interface defines four methods that are related to managing users in a database. The DatabaseUserService  class implements this interface and provides concrete implementations for these methods.

Suppose we want to add a new feature to the UserService interface that allows us to search for users by email address. One way to do this would be to add a new method to the UserService interface:

Your code will not run, unless you also implement this method in all classes that implement the UserService interface:

While this approach works, it violates the Interface Segregation Principle, as it forces the DatabaseUserService  class to implement a method ( searchUsersByEmail ) that it may not need or use.

A better approach would be to create a separate interface  for the email search functionality:

Now we have separate, small, and focused interfaces, i.e. UserService  and UserSearchServicethat have a single responsibility. A class requiring all the functionalities of these interfaces can implement them as shown in the code snippet below:

This adheres to the Interface Segregation Principle, as it ensures that clients (such as the DatabaseUserService class) are not forced to depend on interfaces they do not use.

To understand this concept better, suppose we have another class called MemoryUserService  that implements the UserService  interface but does not need the email search functionality, we can write the code like this:

In this example, the MemoryUserService class only needs to implement the methods defined in the UserService interface, and does not need to worry about the email search functionality. This allows the MemoryUserService class to focus on its primary responsibility of managing users in memory, rather than being forced to implement unrelated functionality.

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is a software design principle that states that high-level modules should not depend on low-level modules, but rather both should depend on abstractions. This means that it is generally a good idea to design your software in such a way that high-level components are not tied to specific implementations of low-level components, but rather depend on abstractions (such as interfaces or abstract classes) that can be implemented in different ways.

Let’s look at an example of how this principle can be applied to the User and UserService classes used in previous code snippets:

In this example, the UserService class depends on an abstraction called the UserRepository interface, rather than depending on a specific implementation of a user repository. This allows us to implement the UserRepository interface in different ways, such as with a database or in-memory storage, without affecting the UserService class.

For example, here’s an implementation of the UserRepository interface that uses a database:

Here is another implementation of the UserRepository interface that uses in-memory storage:

This makes the system more flexible and maintainable, as it allows us to change the repository implementation without affecting the rest of the system. It also makes it easier to test the UserService class, as we can mock the UserRepository dependency in our tests.

Conclusion

In this article, we talked about the five principles of SOLID  code and shared code snippets satisfying each principle. Adhering to these principles can help you design software systems that are more flexible, maintainable, and scalable. However, it’s important to keep in mind that these principles are guidelines, rather than hard and fast rules, and it’s up to the developer to decide when and how to apply them in the context of their specific project. Continue your learning by checking out our blog for more in-depth and up-to-date articles and tutorials on cloud computing and DevOps, software design and development, technology trends to look out for, and more.

Happy Coding!