El principio de responsabilidad única, también conocido como principio de única responsabilidad,
también es el principio de única variabilidad — un concepto bastante resbaladizo para entender y una pregunta tan nerviosa en la entrevista de un programador.
Mi primer encuentro serio con este principio ocurrió al comienzo de mi primer año, cuando nos llevaron, jóvenes e inexpertos, al bosque, para convertir del estado larval de estudiantes en auténticos estudiantes.
En el bosque, nos dividieron en grupos de 8-9 personas cada uno y organizamos una competencia: ¿qué grupo consume más rápidamente una botella de vodka bajo la condición de que la primera persona del grupo vierte el vodka en un vaso, la segunda lo bebe y la tercera lo acompaña con un aperitivo? El participante que realiza su operación se coloca al final de la cola del grupo.
El caso en que el tamaño de la cola es un múltiplo de tres representa una buena implementación del SRP.
Definición 1. Responsabilidad única.
La definición oficial del principio de responsabilidad única (SRP) afirma que cada objeto tiene su propia responsabilidad y razón de existir y que esta responsabilidad es única.
Consideremos el objeto "Bebedor" (Tippler).
Para cumplir con el principio de SRP, dividimos las responsabilidades entre tres:
- Uno vierte (PourOperation)
- Uno bebe (DrinkUpOperation)
- Uno acompaña (TakeBiteOperation)
Cada uno de los participantes en el proceso es responsable de un componente del proceso, es decir, tiene una responsabilidad atómica: beber, verter o acompañar.
El Bebedor, a su vez, actúa como un fachada para estas operaciones:
class Tippler {
//...
void Act(){
_pourOperation.Do() // verter
_drinkUpOperation.Do() // beber
_takeBiteOperation.Do() // acompañar
}
}
¿Por qué?
El programador humano escribe código para un humano-mono, y el humano-mono es descuidado, tonto y siempre está apurado. Solo puede mantener y entender alrededor de 3 a 7 términos en un momento dado.
En el caso del Bebedor, estos términos son tres. Sin embargo, si escribimos el código en una sola hoja, aparecerán manos, vasos, peleas y discusiones interminables sobre política. Y todo esto estará en el cuerpo de un solo método. Estoy seguro de que has visto este tipo de 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 modelar objetos del mundo real en su mente. En su imaginación, puede chocar entre ellos, ensamblar nuevos objetos y desarmarlos de la misma manera. Imagina un viejo modelo de coche. Puedes abrir la puerta en tu imaginación, quitar el revestimiento de la puerta y ver los mecanismos de las ventanas eléctricas, donde habrá engranajes. Pero no puedes ver todos los componentes del coche al mismo tiempo, en una sola "lista". Al menos, el "hombre-mono" no puede.
Por eso, los programadores 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 sale hacia la puerta, y en los modernos, una falla en la electrónica de la cerradura impide que el motor arranque, lo que complica la reparación.
Así que, SRP es un principio que explica CÓMO descomponer, es decir, dónde trazar la línea de separación..
Dice que se debe descomponer según el principio de 'responsabilidad', es decir, según las tareas de ciertos objetos.
Volvamos al bebedor y las ventajas que obtiene el hombre-mono al descomponer:
- El código se vuelve absolutamente claro en cada nivel.
- Varios programadores pueden escribir código al mismo tiempo (cada uno escribe un elemento separado).
- Se simplifica la prueba automatizada: cuanto más simple es un elemento, más fácil es probarlo.
- Surge la composicionalidad del código: se puede reemplazar. DrinkUpOperation por una operación en la que el bebedor derrama líquido bajo la mesa. O reemplazar la operación de servir por una en la que mezclas vino y agua o vodka y cerveza. Dependiendo de las exigencias del negocio, puedes hacer todo eso sin tocar el código del método. Tippler.Act.
- De estas operaciones, puedes formar un glotón (usando solo TakeBitOperation), un alcohólico (usando solo DrinkUpOperation directamente de la botella) y satisfacer muchos otros requisitos del negocio.
(Oh, parece que este es ya el principio OCP, y he violado la responsabilidad de esta publicación.)
Y, por supuesto, los inconvenientes:
- Tendrás que crear más tipos.
- El bebedor beberá por primera vez unas horas más tarde de lo que podría.
Definición 2. Variabilidad única.
¡Permítanme, señores! La clase del bebedor también tiene una única responsabilidad: ¡beber! Y en general, la palabra "responsabilidad" es un concepto extremadamente difuso. Algunos son responsables del destino de la humanidad, mientras que otros son responsables de levantar pingüinos volcados en el polo.
Consideremos dos implementaciones de bebedor. La primera, mencionada anteriormente, contiene tres clases: verter, beber y comer aperitivos.
La segunda, escrita a través de la metodología "Adelante y solo adelante" y contiene toda la lógica en el método Act:
//Не тратьте время на изучение этого класса. Лучше съешьте печеньку
с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, lucen absolutamente idénticas y cumplen con la única responsabilidad de "beber".
¡Confusión!
Entonces, buscamos en internet y descubrimos otra definición de SRP — Principio de Responsabilidad Única (Single Responsibility Principle).
SCP establece que "Un módulo tiene una y solo una razón para cambiar". Es decir, "La responsabilidad es la razón para el cambio".
(Parece que los chicos que inventaron la definición original estaban seguros de las habilidades telepáticas del hombre-mono)
Ahora todo tiene sentido. Se pueden cambiar por separado los procedimientos de verter, beber y comer aperitivos, y dentro del bebedor solo podemos cambiar el orden y la composición de las operaciones, por ejemplo, moviendo el aperitivo antes de beber o añadiendo la lectura de un brindis.
En el enfoque "Adelante y solo adelante", todo lo que se puede cambiar, se cambia solo en el método Act. Esto puede ser legible y efectivo en un caso donde hay poca lógica y raramente cambia, pero a menudo termina con horribles métodos de 500 líneas cada uno, con más ifs de los necesarios para la adhesión de Rusia a la OTAN.
Definición 3. Localización de cambios.
Los bebedores a menudo no entienden por qué se despertaron en un apartamento ajeno, o dónde está su móvil. Es hora de agregar un registro detallado.
Comencemos a registrar el proceso de verter:
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Antes de verter con {_hand} y {_bottle}");
// Lógica de negocio de verter ...
_log.Log($"Después de verter con {_hand} y {_bottle}");
}
}Incrustándola en PourOperation, hemos actuado sabiamente en términos de responsabilidad y encapsulamiento, pero ahora tenemos un dilema con el principio de variabilidad. Además de la operación en sí, que puede cambiar, también se vuelve variable el propio registro. Tendremos que dividir y hacer 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{
//... lógica de negocio
_log.LogAfter(_hand, _bottle);
}
catch(exception e){
_log.OnError(_hand, _bottle, e);
}
}
}Un lector minucioso notará que LogAfter, LogBefore y OnError también pueden cambiarse por separado, y por analogía con las acciones anteriores, se crearán tres clases: PourLoggerBefore, PourLoggerAfter y PourErrorLogger.
Y recordando que hay tres operaciones para el borracho, obtenemos nueve clases de registro. Al final, el borracho consiste en 14 (!!!) clases.
¿Hipérbole? ¡Difícilmente! Un hombre-mono con una granada de descomposición descompondrá al 'vertedor' en una jarra, un vaso, operadores de vertido, un servicio de suministro de agua, un modelo físico de colisión de moléculas y en el siguiente trimestre intentará desenredar las dependencias sin variables globales. Y créanme, no se detendrá.
Es en este momento cuando muchos llegan a la conclusión de que SRP son cuentos de hadas de reinos rosa y se van a enrollar...
…sin conocer la existencia de una tercera definición de SRP:
«El principio de responsabilidad única establece que las cosas similares que deben cambiarse deben estar en un solo lugar«. o “Lo que cambia en conjunto debería estar en un solo lugar.”
Es decir, si cambiamos el registro de la operación, debemos cambiarlo en un solo lugar.
Este es un punto muy importante, ya que todas las explicaciones sobre SRP que fueron anteriores hablaban de dividir tipos hasta que se dividan, es decir, imponían una “restricción superior” sobre el tamaño del objeto, y ahora también estamos hablando de una “restricción inferior”. En otras palabras, SRP no solo exige ‘dividir hasta que se divida’, sino también no exagerar: ‘no descomponer elementos relacionados’. ¡Es una gran batalla entre la navaja de Occam y el hombre-mono!
Ahora al borracho debería resultarle más fácil. Además 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 nos añadimos un cuarto tipo de operación, ya tenemos la lógica de registro lista. Y el código de las operaciones es limpio y libre de ruido de infraestructura.
Como resultado, tenemos 5 clases para resolver la tarea de consumo:
- Operación de vertido
- Operación de consumo
- Operación de acompañamiento
- Registrador
- Fachada de consumo
Cada uno de ellos es responsable estrictamente de una funcionalidad, tiene una sola razón para cambiar. Todas las reglas similares para cambios están juntas.
Ejemplo de la vida real
Una vez escribimos un servicio de registro automático de clientes b2b. Y apareció un método GOD de 200 líneas de contenido similar:
- Ve a 1C y abre una cuenta
- Con esta cuenta ve al módulo de pagos y regístrala allí
- Verifica que no haya una cuenta con esa identificación en el principal servidor
- Crea una nueva cuenta
- Añade el resultado del registro en el módulo de pagos y el número 1C al servicio de resultados del registro
- Añade a esta tabla la información sobre la cuenta
- Crea un número de punto para este cliente en el servicio de puntos. Pasa el número de cuenta 1C a este servicio.
Y había en esta lista unas diez operaciones comerciales más con una horrible interdependencia. El objeto cuenta era necesario para casi todos. El identificador del punto y el nombre del cliente eran necesarios en la mitad de las llamadas.
Después de una hora de refactorización, pudimos separar el código de infraestructura y algunos matices del trabajo con la cuenta en métodos/clases separados. El método God se aligeró, pero quedaron 100 líneas de código que no querían desenredarse.
Sólo después de unos días vino la comprensión de que la esencia de este método "aligerado" es el algoritmo de negocio. Y que la descripción inicial del pliego de condiciones era bastante complicada. Y que el intento de dividir este método en partes sería una violación del SRP, no al revés.
Formalismo.
Ha llegado el momento de dejar en paz a nuestro consumidor. Seca tus lágrimas: definitivamente volveremos a él en algún momento. Ahora vamos a formalizar el conocimiento de este artículo.
Formalismo 1. Definición de SRP
- Divide los elementos de tal manera que cada uno de ellos sea responsable de algo único.
- La responsabilidad se traduce como "razón para cambiar". Es decir, cada elemento tiene solo una razón para cambiar, en términos de lógica empresarial.
- Los cambios potenciales en la lógica de negocio deben ser localizados. Los elementos que se modifican de forma sincrónica deben estar juntos.
Formalismo 2. Criterios necesarios para la autoevaluación.
No he encontrado criterios suficientes para cumplir con el SRP. Pero hay condiciones necesarias:
1) Pregúntese: ¿qué hace esta clase/método/módulo/servicio? Debe poder responder con una definición sencilla. (Gracias) )
explicaciones
Sin embargo, a veces es muy difícil encontrar una definición simple.
2) La corrección de un error o la adición de una nueva característica afecta la menor cantidad de archivos/clases posible. Idealmente, uno.
explicaciones
Dado que la responsabilidad (por la característica o el error) está encapsulada en un solo archivo/clase, sabe exactamente dónde buscar y qué corregir. Por ejemplo: la funcionalidad de cambiar la salida del registro de operaciones solo requiere modificar el registrador. No es necesario recorrer todo el resto del código.
Otro ejemplo: agregar un nuevo control UI similar a los anteriores. Si esto le obliga a añadir 10 entidades diferentes y 15 convertidores diferentes, parece que ha “sobrecargado”.
3) Si varios desarrolladores trabajan en diferentes características de su proyecto, la probabilidad de un conflicto de fusión, es decir, la probabilidad de que un mismo archivo/clase sea modificado por varios desarrolladores al mismo tiempo, es mínima.
explicaciones
Si al agregar una nueva operación "Verter vodka debajo de la mesa" necesita tocar el registrador, la operación de beber y verter, parece que las responsabilidades están mal distribuidas. Sin duda, esto no siempre es posible, pero se debe intentar reducir este indicador.
4) Al realizar una pregunta aclaratoria sobre la lógica de negocio (por parte de un desarrollador o gerente), solo se accede a un archivo/clase y se obtiene información solo de allí.
explicaciones
Las características, reglas o algoritmos están escritos de manera compacta, cada uno en un solo lugar, y no dispersos por todo el código como banderas.
5) La nomenclatura es comprensible.
explicaciones
Nuestra clase o método es responsable de algo único, y la responsabilidad se refleja en su nombre.
AllManagersManagerService — probablemente, una clase God.
LocalPayment — probablemente, no.
Formalismo 3. La metodología de desarrollo "Ockham-first".
Al comienzo del diseño, el hombre-mono no conoce ni siente todas las sutilezas de la tarea que se resuelve y puede cometer errores. Se puede errar de diversas maneras:
- Crear objetos demasiado grandes al unir diferentes responsabilidades
- Fragmentar, dividiendo una única responsabilidad en muchos tipos diferentes
- Definir incorrectamente los límites de responsabilidad
Es importante recordar la regla: "es mejor errar por el lado grande", o "si no estás seguro, no fragmentes". Por ejemplo, si tu clase agrupa dos responsabilidades, sigue siendo comprensible y puede dividirse en dos con un cambio mínimo en el código del cliente. Reunir un vaso a partir de fragmentos de vidrio es, por lo general, más complejo debido al contexto repartido en varios archivos y la falta de dependencias necesarias en el código del cliente.
Es hora de concluir
El ámbito de aplicación de SRP no se limita a OOP y SOLID. Es aplicable a métodos, funciones, clases, módulos, microservicios y servicios. Se aplica tanto en desarrollo “finito” como en “ciencia de cohetes”, mejorando el mundo en todos lados. Si lo piensas, es quizás un principio fundamental de toda la ingeniería. La ingeniería mecánica, los sistemas de control, y en general, todos los sistemas complejos, se construyen a partir de componentes, y la "no fragmentación" priva a los diseñadores de flexibilidad, la "fragmentación" de eficiencia, y los límites incorrectos de razonamiento y paz mental.
SRP no está inventado por la naturaleza ni es parte de una ciencia exacta. Surge de nuestras limitaciones biológicas y psicológicas. Es simplemente un modo de controlar y desarrollar sistemas complejos utilizando el cerebro humano primate. Nos indica cómo descomponer un sistema. La formulación original requería considerable habilidad telepática, pero espero que este artículo haya despejado un poco la cortina de humo.
Fuente: habr.com
