I always had a question on how to assure the single responsibility principle when the process I have to assure is quite complex.
I work with a 3 layers architecture Backend: Controller (my API endpoints) | Service (single responsibility functions) | Data (access to the DB)
Let's say I have a process ProcessA
that is composed by 4 tasks TasksA1
, TasksA2
, TasksA3
, TasksA4
.
If I have an endpoint exposed on my controller layer such as: POSTMethodProcessA
How should be composed my code in order to respect the single responsibility principle on my service layer?
The options I see:
Option 1 (the controller must know the process):
class MyController {
exports.processA = functions.https.onRequest(req, res) => {
myservice.doTaskA1(); // single responsability on task1
myservice.doTaskA2(); // single responsability on task1
myservice.doTaskA3(); // single responsability on task1
myservice.doTaskA4(); // single responsability on task1
});
}
Option 2 (the service know the process and loose the Single responsibility)
class MyController {
exports.processA = functions.https.onRequest(req, res) => {
myservice.doProcessA();
});
}
//inside the service (the doProcessA method must be in charge of multiples tasks and loose the single responsability principle :
class MyService {
function doProcessA() {
this.doTasksA1();
this.doTasksA2();
this.doTasksA3();
this.doTasksA4();
}
}
This question is even more complicated to me if the tasks are composed themselves by multiple jobs: FirstJobA1
, SecondJobA1
, ThirdJobA1
...
How those complexities layer should be handled on the code structure to respect the single responsibility principle is something that always blocked me.
A common misconception is that Single Responsibility Principle
means that a class
(or service or a system, etc.) should do only one thing. Instead, SRP means that
the subject must have a single reason to change.
Thus it's fine to have separate "service" implementations for each task, and then an "aggregate" service that orchestrates the whole process:
service TaskA;
service TaskB;
service TaskN;
service Process{
TaskA::run();
TaskB::run();
...
TaskN::run();
}
A change in a task should not affect unrelated tasks. Also, a change in the process should not affect the subtasks. This is related to the Common Closure Principle (CCP) which states:
The classes in a component should be closed together against the same kind of changes. A change that affects a component affects all the classes in that component and no other components.
or more informally:
gather into components those classes that change for the same reasons and at the same times
This means that if a change in TaskA
will inevitably lead to a change in TaskB
,
then maybe those two should be a single task, instead of two.
Apply that recursively to your sub-tasks: FirstJobA1
...FirstJobA-N