-
-
Notifications
You must be signed in to change notification settings - Fork 6
Single Responsibility Principle (SRP)
This project strives to follow the five SOLID principles, and the first one is the Single Responsibility Principle. 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.
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.
Everything that goes into performing that action from the Application layer to the Infrastructure has to have its own classes responsible only for this task.
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, the required steps could be:
- Validate the data
- Check if user is allowed to perform the action
- Log event for an audit trail
- Call a repository to create a new resource
These 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 side services should work independently and be potentially reusable.
If we take the above example and say that the resource is a Client, the validation would be done
in the ClientValidator
service which may be available for other use cases inside the Client module
such as updating a client.
The same goes for the authorization checker and the event logger.
Usually there is one main service class for each action that coordinates all the other services and calls the repositories.
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 (hint CTRL + N
in PHPStorm) than
individual functions in big cluttered classes.
I was the first being sceptical implementing this full-on, for instance, when
two use cases mostly need the same data structure.
I've declared the collection of table columns outside the repository functions and accessed
them in both use cases. Then, I returned the same data object.
Documentation on how I reverted this can be found
here.
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 its responsibilities are decoupled) easily outweigh those concerns.
Practical SRP example with DTOs
I'm not the best person to talk about this as my experience with big frameworks is quite limited (because I always found excuses to do other things in order not having to work in the old cluttered Symfony 3 codebase).
I imagine a reason why we created those ORM Entities or 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.
Everyone around me told me that it made sense, so I believed it for quite some time, but this view radically changed. 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, it's so 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.
Because it makes it easy and fun to change the "logic" of a use-case as we don't have to worry about
other use-cases at all.
Such a weight off the shoulders!
Slim app basics
- Composer
- Web Server config and Bootstrapping
- Dependency Injection
- Configuration
- Routing
- Middleware
- Architecture
- Single Responsibility Principle
- Action
- Domain
- Repository and Query Builder
Features
- Logging
- Validation
- Session and Flash
- Authentication
- Authorization
- Translations
- Mailing
- Console commands
- Database migrations
- Error handling
- Security
- API endpoint
- GitHub Actions
- Scrutinizer
- Coding standards fixer
- PHPStan static code analysis
Testing
Frontend
Other