Single Responsibility Principle (SRP)

Table of contents

Loading...

Introduction

The slim-example-project, the slim-api-starter and the slim-starter aim to follow the five SOLID principles, and the first one is the Single Responsibility Principle (SRP). It states that a class should have only one reason to change and only a single responsibility.

This is a radically different approach than what I was being thought in school and how we coded in the company I worked for.
It took some time to fully understand and embrace it, but now I'm beyond convinced that it's the way to go for small and large projects.

"Use case" based approach

I think of it as use cases. One use case is one reason to change something.

At the root, a use case is a single action that a user can perform. This could be "creating a specific resource", "submitting a login request", "displaying a page", etc.

Within one of those use cases, there are often multiple different business logic steps to fulfill the action.
For instance, after the user submits a form to create a new resource, the required steps could be:

These steps are different aspects of a problem and really separate responsibilities, and should, therefore, be in separate classes. These are like "accessory" use cases inside a use case.

Each of those accessory services should work independently and be potentially reusable.

If we take the above example and say that the resource the user wants to create is a Client, the validation would be done in the ClientValidator service which may be available and used in other use cases of the Client module such as when updating a client.

The same goes for the authorization checker, the event logger and repository, which have their own classes and functions.

And I usually create one main service class (e.g. ClientCreator) called by the action, that coordinates all the other services and calls the repositories.

Why do it this way?

Separating each use-case into its own class helps in ensuring that each part of the codebase has a clear, focused purpose, making the code more maintainable, understandable and adaptable to change.
Additionally, having a single responsibility, a class or module becomes less likely to be affected by modifications in other parts of the system and encourages a better separation of concerns.

This will inevitably lead to more classes, but that's a good thing. They will be smaller, tailored to exactly one use case and easy to understand and change.

I even find such specific classes easier to find than individual functions in big cluttered classes (hint CTRL + N in PHPStorm or Ctrl + P in VSCode).

At first, I was sceptical about implementing this full-on. For instance, once when two use cases mostly needed the same data structure, I returned the same "result" data object from the repository thinking it'd save me time.
Documentation on why this was a bad idea can be found in this practical SRP example with DTOs.

Instead of being scared to have too many classes or some duplications when use cases share similarities, the fact that it's so much more simple to understand and maintain (because the responsibilities are decoupled) easily outweigh those concerns for me.

The "classic" way

My experience with big frameworks is limited, but I imagine a reason why we created ORM Entities and big "manager" classes that are used in many different use cases, is to centralize the data structure and make it "more comfortable" to work with those components as we have access to a lot of "pre-made" functions or "preloaded" attributes.
Also, if the data structure changes, there are only a few places that have to be modified thanks to the additional abstraction Entities offer.

And that does make sense to some extent, but I realized that it brings a strong rigidity with it. Like a spiderweb, things are closely interconnected and dependent on one another, which makes it a lot harder to understand and have a feel for the whole system. As the application grows and different developers code in their own ways, with their understanding of the codebase, it becomes more and more complex and refactoring a hell work.

Duplications that such systems try to avoid for the case something must be changed are often simple and solved by search and replace (Ctrl + Shift + R in PHPStorm).

And functions that are reused eventually grow as they have to be changed in different ways to englobe more and more deviations from use case to use case.

All that is to say, in my opinion, it's easily worth having to change something trivial at more places and having more (agile) functions and classes even if their job is not very different from one another.
It makes it easy and fun to change the "logic" of a single use-case when we don't have to worry about other use-cases at all. And when refactoring, we have a clear overview and path to follow, as we can change one use-case at a time.

Such a weight off the shoulders!

^