SOLID foundations of your code part 1

Łukasz Pawłowski
codeburst
Published in
10 min readMay 18, 2020

--

What are the SOLID principles? What SOLID stands for? SOLID is an acronym from five class design principles presented by Robert C. Martin in his paper “Design Principles and Design Patterns”:

  • single responsibility principle
  • open-closed principle
  • Liskov substitution principle
  • interface segregation principle
  • dependency inversion principle

All of them will help you create a better, more readable object-oriented code. They are all about designing your code, dividing code into classes/interfaces, building abstractions. Even if they were created with some specific thing in mind, they are universal and scalable. I want to present and explain them to you. I’ll also tell you why you should use them.

In this article, I’ll focus on two of them: the single responsibility principle and the open-closed principle.

TLDR:

  • Single responsibility principle — make sure your class does one thing. It does not mean one function but one functionality. Your class should have one reason to exist. For example, your Data model class should only represent data, move complex calculations, and data formatting to other objects. Simple classes are easier to manage, replace, and extend. You can implement that principle by splitting your code into multiple files with one goal to achieve per class. It also has an impact on naming classes and file structures, which should indicate the responsibilities and logic of modules and objects.
  • Open-closed principle — make sure that functionality in class can be extended without changing class code. You can achieve this with inheritance and dependency injection. Imagine that you need to create an income report with taxes. If you have one class to generate a report instead of putting code for tax calculation inside that class, create new objects for a specific type of tax. Than pass tax objects along with items. You can extend reports with many kinds of fees without changing even one line of code. It is much easier to accomplish if you follow the single responsibility principle. Using the open-closed principle will simplify code management and ensure that the extension of code does not break its current usage.

Single responsibility principle — A class should have one, and only one, a reason to change.

What does it mean?

One class should be responsible for one thing. A class should have one main goal of existence. It can’t do multiple things, which can change independently. If changes in two or more independent functionalities require changes in the same class, you probably violated a single responsibility principle.

The responsibility of class depends on this, what object we created. What does it implement? What do we expect from it? Is this representation of data, functionality, or solution for some technical issue? In most cases, we think about responsibility as functionality in the context of features or business requirements like file generation, adding invoices to the system, report generation, etc. Sometimes we talk about more technical requirements like controllers, which have to control the flow of data for multiple different requests (edit, add, find data). Such a controller supports multiple user stories but does one thing — control flow. Another example can be a message bus that can transfer multiple messages connected with different business requirements but has one functionality — transport data.

Single responsibility in practice

Let’s think about user profile in simple social media. User profile has some data which represent user, but also is used in multiple places: we can search for friends, we can communicate with other users, we can watch users actions, etc. If we want to put all functionality connected with a user to one class it would look more or less like that:

class User {
constructor(name, email) {... }
sendMessage(message) { ... }
watchUser() { ... }
static find(query) { ... }
}

As you can see, the class will grow with the requirements and will do multiple things. Part of functions will depend on each other and reuse the same class attributes. Instead, we should split code into smaller pieces. We can leave the User as a model with an option to read and write data from DB. Sending a message will be separate functionality presented in other classes, the same we can say about following other users. We can go even further and split User into User and UserRepository. The first would be responsible for data representation. Second for querying DB. We would get something like this:

class User {
constructor(name, email) {...}
}
class UserRepository {
static find(query) { ... }
}
class Messenger {
send(receiver, sender, message) { ... }
}
class Watcher {
watch(watcher, user) { ... }
}

Why?

Single responsibility affects flexibility. It is easier to manage dependencies in class with a single responsibility. Imagine invoice generation. If we have an invoice object with all functionality for each type of invoice and different taxes, then it is hard to add a new tax or replace one tax with another. If you split code to keep taxes in a model, invoice generation as a class with logic, and invoice model itself as another one, you can easily extend taxes by adding a new one. You can easily generate different types of invoices with a new generator without changing the abstract invoice model. When your class is responsible for too many things, one change can affect multiple elements. It is not easy to find and manage such dependencies in big applications and enormous classes.

More specialized classes are easier to manage by themselves. It is much easier to look at the specific class and get your head around if it has only a few functions. Think about a class to manage invoices, let’s say you create only one class for everything: database manipulation, formatting response for API, file creation, sending data to other systems including full communication with such systems, verification access to data, calculations, etc. Such a class would be vast and unreadable. One function in such a class can affect each of this functionality. If you split such class per functionality, it would be much easier to read and manage.

Small class with single responsibility is easier to use in multiple places. It implements specific functionality in a particular way. If you want to make your system work differently under certain circumstances, you just swap that class for other without worrying about dependencies, etc.

This principle also affects the naming convention. When you have classes with a single responsibility, it is easier to name them. You do not need to think about how to present a whole range of functionality implemented in the class. You just said what part of the system it implements and what it does like AuthController, InvoiceModel, InvoicePdfGenerator, etc. That has an impact on the whole folder/file structure of your application.

Additional comment

This principle is the easiest to understand, but, in my opinion, the hardest to implement.

I believe that it is the most scalable principle. It was presented in the context of class, but you can transfer it to lower levels of abstraction like functions or higher levels like modules.

Think about functions. The function should be small and do one specific thing. Complex functions should be divide into a few smaller, more specific ones. The same about modules — your module should be responsible for specific functionality. Sure it could depend on other modules, but still, it is responsible for one specific part of your system, for example, report creation, invoices, authorization. We can go even further — that principle has an impact on the infrastructure of the whole application. Think about microservices and how single responsibility impacts them.

You’ll need time and practice before you make it the right way. You need to be able to see the abstraction of objects and separate them into new classes from which you’ll inherit. You need to see connections between existing elements to extract them to new classes, which will be reused. You need to define what specific class is created for and how it is used. Start from refactoring your code. Try to analyze each class — what does it do? Is this something more abstract or specific thing? Is this implementation of some requirements, some exceptional flow, technical solution for a more general problem? Try to create small functions and compare functions in a specific class. If you have multiple functions, which do different things like getting data, parsing data, making a calculation, then group them and make for each group a new class.

Open-closed principle — You should be able to extend a classes behavior, without modifying it.

What does it mean?

Class does a specific thing in a specific way, but you should be able to extend that behavior without changing class code. You should be able to make functionality more complicated than it is right now or change it a bit without modifying the current code. If you need to extend specific behavior or functionality, you should have some option to make it without touching existing classes you want to extend.

In the beginning, it is hard to imagine how to achieve this, but it is doable by using dependencies and inheritance. You can create a class which overite your class, but it does not exactly cover that principle, because you change some code with overwrites. Propper solution would be using dependencies — you can inject specific parts of functionality to make your class extensible. When you need to extend how a class works, you just swap injected objects.

Open-closed principle in practice

Let’s assume we have a list of users. We need to display it on the page for admin. We can filter and sort them by name and location. We create controller and class to the received list:

class UserController {
public function get($filters) {
$validFilters = SimpleValidator::validate($filters);
$users = UserRepository::all($validFilters)
return {"data": $users}
}
}
class UserRepository {
public static all($filters) {...}
}
class SimpleValidator {
public static validate($filters) { ... }
}

Now the client decides that he want to export such a list to pdf or CSV. For each kind of export, we want a different filtering possibility. You can write multiple ifs in a controller to select specific validation and response type. But it can break the single responsibility principle. You can build new classes, but part of the functionality will be repeated. You can also create your ClassController to give you the possibility to extends it with dependency injection:

class UserController {
public function get($filters, Validator $validator, ResponseFormatter $responseFormatter) {
$validFilters = $validator->validate($filters);
return $responseFormatter->formatResponse(UserRepository::all($validFilters))
}
}
class UserRepository {
public static function all($filters) {...}
}
class SimpleValidator extends Validator{
public function validate($filters) { ... }
}
class CsvValidator extends Validator {
public function validate($filters) { ... }
}
class PdfValidator extends Validator {
public function validate($filters) { ... }
}
class ApiResponseFormatter extends ResponseFormatter {
public function formatResponse($data) { ... }
}
class PdfResponseFormatter extends ResponseFormatter {
public function formatResponse($data) { ... }
}
class CsvResponseFormatter extends ResponseFormatter {
public function formatResponse($data) { ... }
}

Why?

Requirements for our applications change all the time. We need to adjust some details, change the specific flow, give users new options, etc. In complex applications, we end up with a lot of small repeatable elements. With a single responsibility principle, we make sure that we can reuse such small parts without writing them repeatedly. What happens when we need to change some behavior in a specific context? We can try to write some switch, ifs, etc. But this way we break single responsibilities. This is where the open-closed principle comes in. If we can build some base classes and then extend them in specific situations without changing class itself, we can maintain clean code and add any new feature, functionality, or exception we are requested. Giving the possibility to extend the class by dependencies or by inheritance, we make specific elements more flexible and easier to maintain.

Any change in the code of a class, which is already in use, may cause errors in multiple places and affect how the current system works. We make sure that we can extend functionality without changing specific code, but by swapping it parts in a specific moment. We do not worry that our change breaks anything. Of course, there will still be multiple elements that implement specific solutions, but they will be used to extend more abstract elements.

Additional comment

This principle is very important for frameworks, libraries, and more complex/abstract elements of applications. It gives developers the option to extend your library or write plugins for your solutions without touching code, which you provide. It is making your code reusable in multiple places and for multiple different purposes. Think about any library you use. How many times did you need to create something specific that was not covered by the library? Probably a lot. How many times did you need to change the library code to cover your problem? Probably there was no such situation. More likely, you extended specific classes or injected your own dependencies.

The principle is also visible on how we extend the functionality of our applications, tools, and libraries. Look on webpack, gulp, jest, or other javascript tools. All of them are extensible. There are thousands of plugins to extend the specific tool. You can write your own plugins and extend the functionality of a tool without changing even one line of the tool’s code. In many cases, you can even inject your own code to overwrite custom behavior again without touching the tool’s code. As you see, it is not just class level, but also on wider space like whole systems and modules.

To follow this principle, you need to isolate small pieces of code that represent specific solutions and can change based on context. If you follow single responsibility, it should be easier to find such parts of code because it would be isolated. Next, you need to make it swappable and extendible. You have two ways to achieve that. The first is to extend your existing classes. Avoid final classes and private functions/attributes for classes that can be extended. The second thing is dependency injection. For that, make sure that you do not build an object you depend on in class, but you inject your dependencies from outside, and you can swap them any time you need. Ensure that part of the code which is changed is outsourced in other classes and injected as a dependency.

That would be all for now. I hope you enjoyed this article and it helped you understand first two SOLID principles.

In the second article, which you can read here, I’ll discuss the rest of them. I hope you’ll join me.

--

--

Web developer, tech advisor, manager, husband & father. Tech Manager at Boozt