Principio de Responsabilidad Única. No es tan simple como parece

Principio de Responsabilidad Única. No es tan simple como parece Principio de responsabilidad única, también conocido como principio de responsabilidad única,
También conocido como el principio de variabilidad uniforme: un tipo extremadamente resbaladizo de entender y una pregunta tan nerviosa en una entrevista con un programador.

Mi primer conocimiento serio de este principio tuvo lugar a principios del primer año, cuando los jóvenes y verdes fueron llevados al bosque para convertir las larvas en estudiantes, verdaderos estudiantes.

En el bosque, nos dividimos en grupos de 8 a 9 personas cada uno y organizamos una competencia: qué grupo bebería más rápido una botella de vodka, siempre que la primera persona del grupo sirviera vodka en un vaso, la segunda lo bebiera y y el tercero toma un refrigerio. La unidad que ha completado su operación pasa al final de la cola del grupo.

El caso en el que el tamaño de la cola era múltiplo de tres fue una buena implementación de SRP.

Definición 1. Responsabilidad única.

La definición oficial del Principio de Responsabilidad Única (PRS) establece que cada entidad tiene su propia responsabilidad y razón de existencia, y tiene una sola responsabilidad.

Considere el objeto “Bebedor” (Bebedor).
Para implementar el principio SRP, dividiremos las responsabilidades en tres:

  • Uno vierte (Operación de vertido)
  • Uno bebe (DrinkUpOperación)
  • Uno toma un refrigerio (Operación TakeBite)

Cada uno de los participantes en el proceso es responsable de un componente del proceso, es decir, tiene una responsabilidad atómica: beber, servir o picar.

El abrevadero, a su vez, es una fachada para estas operaciones:

сlass Tippler {
    //...
    void Act(){
        _pourOperation.Do() // налить
        _drinkUpOperation.Do() // выпить
        _takeBiteOperation.Do() // закусить
    }
}

Principio de Responsabilidad Única. No es tan simple como parece

¿Por qué?

El programador humano escribe código para el hombre mono, y el hombre mono es distraído, estúpido y siempre tiene prisa. Puede retener y comprender entre 3 y 7 términos a la vez.
En el caso de un borracho, existen tres de estos términos. Sin embargo, si escribimos el código en una sola hoja, contendrá manos, vasos, peleas y un sinfín de discusiones sobre política. Y todo esto estará en el cuerpo de un método. Estoy seguro de que has visto ese código en tu práctica. No es la prueba más humana para la psique.

Por otro lado, el hombre mono está diseñado para simular objetos del mundo real en su cabeza. En su imaginación, puede juntarlos, montar nuevos objetos a partir de ellos y desmontarlos de la misma forma. Imagínese un modelo de coche antiguo. En su imaginación, puede abrir la puerta, desenroscar el revestimiento de la puerta y ver allí los mecanismos de elevación de la ventana, dentro de los cuales habrá engranajes. Pero no se pueden ver todos los componentes de la máquina al mismo tiempo, en un "listado". Al menos el “hombre mono” no puede hacerlo.

Por lo tanto, los programadores humanos descomponen mecanismos complejos en un conjunto de elementos menos complejos y funcionales. Sin embargo, se puede descomponer de diferentes maneras: en muchos coches antiguos, el conducto de aire va hacia la puerta, y en los coches modernos, un fallo en la electrónica de la cerradura impide que el motor arranque, lo que puede ser un problema durante las reparaciones.

Y por eso, SRP es un principio que explica CÓMO descomponer, es decir, dónde trazar la línea divisoria.

Dice que es necesario descomponerse según el principio de división de “responsabilidades”, es decir, según las tareas de determinados objetos.

Principio de Responsabilidad Única. No es tan simple como parece

Volvamos a la bebida y los beneficios que recibe el hombre mono durante la descomposición:

  • El código se ha vuelto extremadamente claro en todos los niveles.
  • El código puede ser escrito por varios programadores a la vez (cada uno escribe un elemento separado)
  • Las pruebas automatizadas se simplifican: cuanto más simple sea el elemento, más fácil será probarlo.
  • Aparece la composicionalidad del código: puede reemplazar DrinkUpOperación a una operación en la que un borracho vierte líquido debajo de la mesa. O reemplace la operación de vertido con una operación en la que mezcle vino y agua o vodka y cerveza. Dependiendo de los requisitos comerciales, puede hacer todo sin tocar el código del método. Tippler.Act.
  • A partir de estas operaciones puedes doblar el glotón (usando solo Operación TakeBit), Alcohólico (usando sólo DrinkUpOperación directamente de la botella) y cumplir con muchos otros requisitos comerciales.

(Oh, parece que esto ya es un principio de OCP y violé la responsabilidad de esta publicación)

Y, por supuesto, las desventajas:

  • Tendremos que crear más tipos.
  • Un borracho bebe por primera vez un par de horas más tarde de lo que lo habría hecho de otra manera.

Definición 2. Variabilidad unificada.

¡Permítanme, señores! La clase que bebe también tiene una única responsabilidad: ¡bebe! Y, en general, la palabra "responsabilidad" es un concepto extremadamente vago. Alguien es responsable del destino de la humanidad y alguien es responsable de criar a los pingüinos que fueron volcados en el polo.

Consideremos dos implementaciones del bebedor. El primero, mencionado anteriormente, contiene tres clases: servir, beber y refrigerio.

El segundo está escrito mediante la metodología “Adelante y solo hacia adelante” y contiene toda la lógica del método. Actúe:

//Не тратьте время  на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
   //...
   void Act(){
        // наливаем
    if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
        throw new OverdrunkException();

    // выпиваем
    if(!_hand.TryDrink(from: _glass,  size: _glass.Capacity))
        throw new OverdrunkException();

    //Закусываем
    for(int i = 0; i< 3; i++){
        var food = _foodStore.TakeOrDefault();
        if(food==null)
            throw new FoodIsOverException();

        _hand.TryEat(food);
    }
   }
}

Ambas clases, desde el punto de vista de un observador externo, parecen exactamente iguales y comparten la misma responsabilidad de "beber".

¡Confusión!

Luego nos conectamos a Internet y descubrimos otra definición de SRP: el principio de cambiabilidad única.

SCP afirma que "Un módulo tiene una y sólo una razón para cambiar". Es decir, “La responsabilidad es motivo de cambio”.

(Parece que los chicos a los que se les ocurrió la definición original confiaban en las habilidades telepáticas del hombre mono)

Ahora todo encaja. Por separado, podemos cambiar los procedimientos de vertido, bebida y refrigerio, pero en el propio bebedero solo podemos cambiar la secuencia y composición de las operaciones, por ejemplo, moviendo el refrigerio antes de beber o agregando la lectura de un brindis.

En el enfoque "Adelante y sólo hacia adelante", todo lo que se puede cambiar se cambia sólo en el método Actúe. Esto puede ser legible y eficaz cuando hay poca lógica y rara vez cambia, pero a menudo termina en métodos terribles de 500 líneas cada uno, con más afirmaciones condicionales de las necesarias para que Rusia se una a la OTAN.

Definición 3. Localización de cambios.

Los bebedores a menudo no entienden por qué se despertaron en el apartamento de otra persona o dónde está su teléfono móvil. Es hora de agregar registros detallados.

Comencemos a registrar con el proceso de vertido:

class PourOperation: IOperation{
    PourOperation(ILogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.Log($"Before pour with {_hand} and {_bottle}");
        //Pour business logic ...
        _log.Log($"After pour with {_hand} and {_bottle}");
    }
}

Al encapsularlo en Operación de vertido, actuamos sabiamente desde el punto de vista de la responsabilidad y la encapsulación, pero ahora estamos confundidos con el principio de variabilidad. Además de la operación en sí, que puede cambiar, el registro en sí también se vuelve modificable. Tendrás que separar y crear un registrador especial para la operación de vertido:

interface IPourLogger{
    void LogBefore(IHand, IBottle){}
    void LogAfter(IHand, IBottle){}
    void OnError(IHand, IBottle, Exception){}
}

class PourOperation: IOperation{
    PourOperation(IPourLogger log /*....*/){/*...*/}
    //...
    void Do(){
        _log.LogBefore(_hand, _bottle);
        try{
             //... business logic
             _log.LogAfter(_hand, _bottle");
        }
        catch(exception e){
            _log.OnError(_hand, _bottle, e)
        }
    }
}

El lector meticuloso notará que Iniciar sesión después, Iniciar sesión antes и OnError También se puede cambiar individualmente y, por analogía con los pasos anteriores, se crearán tres clases: PourLoggerAntes, PourLoggerDespués и PourErrorLogger.

Y recordando que hay tres operaciones para un bebedor, obtenemos nueve clases de registro. Como resultado, todo el círculo de bebedores consta de 14 (!!!) clases.

¿Hipérbola? ¡Difícilmente! Un hombre mono con una granada de descomposición dividirá el "vertedor" en una jarra, un vaso, operadores de vertido, un servicio de suministro de agua, un modelo físico de la colisión de moléculas, y durante el próximo trimestre intentará desenredar las dependencias sin variables globales. Y créanme, no se detendrá.

Es en este punto que muchos llegan a la conclusión de que los SRP son cuentos de hadas de los reinos rosas, y se van a jugar a los fideos...

... sin siquiera conocer la existencia de una tercera definición de Srp:

“El Principio de Responsabilidad Única establece que las cosas que son similares al cambio deben almacenarse en un solo lugar". o "¿Qué cambios juntos deben mantenerse en un solo lugar?"

Es decir, si cambiamos el registro de una operación, entonces debemos cambiarlo en un solo lugar.

Este es un punto muy importante, ya que todas las explicaciones de SRP anteriores decían que era necesario triturar los tipos mientras se trituraban, es decir, impusieron un "límite superior" al tamaño del objeto, y ahora ya estamos hablando de un “límite inferior”. En otras palabras, SRP no sólo requiere "aplastar mientras se aplasta", sino también no exagerar: "no aplastar cosas entrelazadas". ¡Esta es la gran batalla entre la navaja de Occam y el hombre mono!

Principio de Responsabilidad Única. No es tan simple como parece

Ahora el bebedor debería sentirse mejor. Además del hecho de que no es necesario dividir el registrador IPourLogger en tres clases, también podemos combinar todos los registradores en un solo tipo:

class OperationLogger{
    public OperationLogger(string operationName){/*..*/}
    public void LogBefore(object[] args){/*...*/}       
    public void LogAfter(object[] args){/*..*/}
    public void LogError(object[] args, exception e){/*..*/}
}

Y si agregamos un cuarto tipo de operación, entonces el registro ya está listo. Y el código de las operaciones en sí es limpio y libre de ruido de infraestructura.

Como resultado, tenemos 5 clases para solucionar el problema de la bebida:

  • Operación de vertido
  • Operación de bebida
  • Operación de interferencia
  • registrador
  • Fachada de bebedero

Cada uno de ellos es responsable estrictamente de una funcionalidad y tiene un motivo de cambio. Todas las reglas similares al cambio se encuentran cerca.

Ejemplo de la vida real

Una vez escribimos un servicio para registrar automáticamente un cliente b2b. Y apareció un método DIOS para 200 líneas de contenido similar:

  • Vaya a 1C y cree una cuenta
  • Con esta cuenta, vaya al módulo de pago y créela allí
  • Compruebe que no se haya creado una cuenta con dicha cuenta en el servidor principal
  • Crea una cuenta nueva
  • Agregue los resultados de registro en el módulo de pago y el número 1c al servicio de resultados de registro
  • Agregar información de la cuenta a esta tabla
  • Cree un número de punto para este cliente en el servicio de puntos. Pase su número de cuenta 1c a este servicio.

Y había alrededor de 10 operaciones comerciales más en esta lista con una conectividad terrible. Casi todo el mundo necesitaba el objeto de cuenta. En la mitad de las llamadas se necesitaba el ID del punto y el nombre del cliente.

Después de una hora de refactorización, pudimos separar el código de infraestructura y algunos de los matices de trabajar con una cuenta en métodos/clases separados. El método de Dios lo hizo más fácil, pero quedaban 100 líneas de código que simplemente no querían ser desenredadas.

Sólo después de unos días quedó claro que la esencia de este método "ligero" es un algoritmo empresarial. Y que la descripción original de las especificaciones técnicas era bastante compleja. Y es el intento de romper este método en pedazos lo que violará el SRP, y no al revés.

Formalismo.

Es hora de dejar en paz a nuestro borracho. Seca tus lágrimas: definitivamente regresaremos algún día. Ahora formalicemos el conocimiento de este artículo.

Formalismo 1. Definición de SRP

  1. Separa los elementos para que cada uno de ellos sea responsable de una cosa.
  2. Responsabilidad significa "razón para cambiar". Es decir, cada elemento tiene un solo motivo de cambio, en términos de lógica de negocio.
  3. Posibles cambios en la lógica empresarial. debe estar localizado. Los elementos que cambian sincrónicamente deben estar cerca.

Formalismo 2. Criterios de autoevaluación necesarios.

No he visto criterios suficientes para cumplir el SRP. Pero hay condiciones necesarias:

1) Pregúntese qué hace esta clase/método/módulo/servicio. debes responderla con una definición simple. ( Gracias Brightori )

explicaciones

Sin embargo, a veces es muy difícil encontrar una definición sencilla.

2) Corregir un error o agregar una nueva característica afecta a una cantidad mínima de archivos/clases. Idealmente, uno.

explicaciones

Dado que la responsabilidad (por una característica o error) está encapsulada en un archivo/clase, usted sabe exactamente dónde buscar y qué editar. Por ejemplo: la función de cambiar la salida de las operaciones de registro requerirá cambiar solo el registrador. No es necesario ejecutar el resto del código.

Otro ejemplo es agregar un nuevo control UI, similar a los anteriores. Si esto te obliga a agregar 10 entidades diferentes y 15 convertidores diferentes, parece que estás exagerando.

3) Si varios desarrolladores están trabajando en diferentes características de su proyecto, entonces la probabilidad de un conflicto de fusión, es decir, la probabilidad de que varios desarrolladores cambien el mismo archivo/clase al mismo tiempo, es mínima.

explicaciones

Si, al agregar una nueva operación "Verter vodka debajo de la mesa", es necesario afectar el registrador, la operación de beber y servir, entonces parece que las responsabilidades están divididas de manera torcida. Por supuesto, esto no siempre es posible, pero deberíamos intentar reducir esta cifra.

4) Cuando se le hace una pregunta aclaratoria sobre la lógica empresarial (de un desarrollador o administrador), ingresa estrictamente a una clase/archivo y recibe información solo desde allí.

explicaciones

Las características, reglas o algoritmos se escriben de forma compacta, cada uno en un solo lugar y no están dispersos con banderas por todo el espacio del código.

5) La denominación es clara.

explicaciones

Nuestra clase o método es responsable de una cosa y la responsabilidad se refleja en su nombre.

AllManagersManagerService: muy probablemente una clase de Dios
Pago local: probablemente no

Formalismo 3. Metodología de desarrollo Occam primero.

Al comienzo del diseño, el hombre mono no conoce ni siente todas las sutilezas del problema que se está resolviendo y puede cometer un error. Puedes cometer errores de diferentes maneras:

  • Haga que los objetos sean demasiado grandes fusionando diferentes responsabilidades
  • Reformular dividiendo una única responsabilidad en muchos tipos diferentes
  • Definir incorrectamente los límites de responsabilidad.

Es importante recordar la regla: "es mejor cometer un gran error" o "si no estás seguro, no lo dividas". Si, por ejemplo, su clase contiene dos responsabilidades, entonces aún es comprensible y se puede dividir en dos con cambios mínimos en el código del cliente. Ensamblar un vaso a partir de fragmentos de vidrio suele ser más difícil debido al contexto que se distribuye en varios archivos y a la falta de dependencias necesarias en el código del cliente.

Es hora de terminar el día

El alcance de SRP no se limita a OOP y SOLID. Se aplica a métodos, funciones, clases, módulos, microservicios y servicios. Se aplica tanto al desarrollo “figax-figax-and-prod” como a la “ciencia espacial”, haciendo que el mundo sea un poco mejor en todas partes. Si lo piensas bien, este es casi el principio fundamental de toda ingeniería. La ingeniería mecánica, los sistemas de control y, de hecho, todos los sistemas complejos se construyen a partir de componentes, y la "infrafragmentación" priva a los diseñadores de flexibilidad, la "sobrefragmentación" priva a los diseñadores de eficiencia y los límites incorrectos los privan de razón y tranquilidad.

Principio de Responsabilidad Única. No es tan simple como parece

SRP no es un invento de la naturaleza y no forma parte de la ciencia exacta. Rompe nuestras limitaciones biológicas y psicológicas y es simplemente una forma de controlar y desarrollar sistemas complejos utilizando el cerebro del hombre mono. Nos dice cómo descomponer un sistema. La formulación original requería bastante telepatía, pero espero que este artículo aclare parte de la cortina de humo.

Fuente: habr.com

Añadir un comentario