design-patternsarchitecturesolid-principlessingle-responsibility-principle

Single responsibility principle on complex process


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.


Solution

  • 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