-
-
Notifications
You must be signed in to change notification settings - Fork 3
Single Responsibility Principle (SRP)
The slim-example-project, the slim-api-starter and the slim-starter strive 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 and used in other use cases of the Client module
such as when 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.
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 such 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.
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.
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, 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 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