Principio de responsabilidad única, él es el principio de responsabilidad única,
él es el principio de la variabilidad uniforme: un tipo extremadamente resbaladizo de entender y una pregunta tan nerviosa en la entrevista de un programador.
La primera vez que conocí seriamente este principio fue para mí al comienzo del primer año, cuando los jóvenes y los verdes fueron llevados al bosque para formar verdaderos estudiantes de las larvas.
En el bosque, nos dividimos en grupos de 8-9 personas en cada uno y organizamos una competencia, cuyo grupo beberá una botella de vodka más rápido, siempre que la primera persona del grupo vierta vodka en un vaso, la segunda bebida y la tercera mordida. Una vez completada su operación, la unidad se encuentra al final de la cola del grupo.
El caso cuando el tamaño de la cola era un múltiplo de tres, y era una buena implementación de SRP.
Definición 1. Responsabilidad individual.
La definición oficial del principio de responsabilidad única (SRP) sugiere que cada objeto tiene su propia responsabilidad y razón de existir, y esta responsabilidad tiene solo una.
Considere el objeto Tippler.
Para cumplir con el principio de SRP, dividimos las responsabilidades en tres:
- Uno vierte ( PourOperation )
- One Drinks ( DrinkUpOperation )
- Una merienda ( 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 comer algo.
El alcohol, a su vez, es la fachada de estas operaciones:
lass Tippler { //... void Act(){ _pourOperation.Do() // _drinkUpOperation.Do() // _takeBiteOperation.Do() // } }
Por qué
El programador humano escribe el código para el hombre mono, y el hombre mono es desatento, estúpido y siempre tiene prisa en alguna parte. Puede sostener y comprender de 3 a 7 términos a la vez.
En el caso de la bebida, estos términos son tres. Sin embargo, si escribimos el código con una hoja, aparecerán en él manos, anteojos, masacres y debates interminables 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 es encarcelado por modelar objetos del mundo real en su cabeza. En su imaginación, puede juntarlos, recoger nuevos objetos de ellos y desarmarlos de la misma manera. Imagina un viejo modelo de automóvil. Puede abrir la puerta en su imaginación, desenroscar el borde de la puerta y ver los mecanismos de elevación de la ventana allí, dentro de los cuales habrá engranajes. Pero no puede ver todos los componentes de la máquina al mismo tiempo, en una "lista". Al menos el "hombre mono" no puede.
Por lo tanto, los programadores humanos descomponen mecanismos complejos en un conjunto de elementos menos complejos y de trabajo. Sin embargo, la descomposición se puede hacer de diferentes maneras: en muchos autos viejos (el conducto sale por la puerta y en los modernos), la falla de la cerradura electrónica impide que el motor arranque, lo que funciona durante la reparación.
Entonces, SRP es un principio que explica CÓMO descomponer, es decir, dónde dibujar la línea de separación .
Él dice que la descomposición debe basarse en el principio de separación de "responsabilidad", es decir, de acuerdo con las tareas de varios objetos.
Volvamos a la bebida y las ventajas que obtiene una persona mono cuando se descompone:
- El código se ha vuelto extremadamente claro en todos los niveles.
- Varios programadores pueden escribir código a la vez (cada uno escribe un elemento separado)
- Las pruebas automatizadas se simplifican: cuanto más simple es el elemento, más fácil es probar
- A partir de estas tres operaciones, en el futuro, puede sumar el glotón (usando solo TakeBitOperation ), el alcohólico (usando solo DrinkUpOperation directamente de la botella) y satisfacer muchos otros requisitos comerciales.
Y, por supuesto, los contras:
- Tendrá que crear más tipos.
- Un bebedor beberá por primera vez un par de horas más tarde de lo que podría
Definición 2. Variabilidad unificada.
Permitan caballeros! La clase de bebida también cumple 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 volcados en el poste.
Considere dos implementaciones de bingo. El primero, mencionado anteriormente, contiene tres clases: verter, beber y comer algo.
El segundo está escrito a través de la metodología Forward and Only Forward 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, se ven exactamente iguales y cumplen con la responsabilidad única de "beber".
Vergüenza!
Luego navegamos por Internet y descubrimos otra definición de SRP: el principio de variabilidad uniforme.
Esta definición establece que " El módulo tiene una y solo una razón para el cambio ". Es decir, "la responsabilidad es una ocasión para el cambio".
Ahora todo cae en su lugar. Por separado, puede cambiar los procedimientos para verter, beber y morder, y en el alcohol solo podemos cambiar la secuencia y la composición de las operaciones, por ejemplo, mover el refrigerio antes de beber o agregar una lectura de pan tostado.
En el enfoque Forward and Only Forward, todo lo que se puede cambiar se cambia solo en el método Act . Esto puede ser legible y efectivo en el caso cuando hay poca lógica y rara vez cambia, pero a menudo termina con métodos terribles de 500 líneas cada uno, con más números si que los necesarios para la entrada de Rusia en 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 un registro detallado.
Comencemos a iniciar sesión 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 PourOperation , actuamos sabiamente en términos de responsabilidad y encapsulación, pero ahora con el principio de variabilidad, ahora estamos avergonzados. Además de la operación en sí, que puede cambiar, el registro en sí se vuelve variable. Tendremos que separar 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{ //... business logic _log.LogAfter(_hand, _bottle"); } catch(exception e){ _log.OnError(_hand, _bottle, e) } } }
Un lector meticuloso notará que LogAfter , LogBefore y OnError también se pueden cambiar individualmente y, por analogía con los pasos anteriores, crearán tres clases: PourLoggerBefore , PourLoggerAfter y PourErrorLogger .
Y recordando que hay tres operaciones para un atracón: obtenemos nueve clases de registro. Como resultado, toda la bebida consta de 14 (!!!) clases.
Hipérbole? Apenas! Un hombre mono con una granada de descomposición aplastará 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 el próximo trimestre tratará de desentrañar las dependencias sin variables globales. Y créeme, no se detendrá.
Es en este punto que muchos llegan a la conclusión de que los SRP son cuentos de los reinos rosados, y se van a torcer los fideos ...
... sin saber nunca sobre la existencia de la tercera definición de Srp:
" Las cosas que son similares al cambio deben almacenarse en un solo lugar ". o " Lo que cambia juntos debe mantenerse 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 de SRP que se mencionaron anteriormente dicen que los tipos deben dividirse mientras están divididos, es decir, imponen un "límite superior" en el tamaño del objeto, y ahora estamos hablando de un "límite inferior" . En otras palabras, SRP no solo requiere "aplastar mientras se aplasta", sino también no exagerar: "no aplastar cosas vinculadas" . No te compliques innecesariamente. ¡Esta es la gran batalla de la navaja de Occam con el hombre mono!
Ahora la bebida debería ser más fácil. Además de no dividir el registrador IPourLogger en tres clases, también podemos combinar todos los registradores en un 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 se nos agrega el cuarto tipo de operación, entonces el registro está listo para ello. Y el código de las operaciones en sí es limpio y libre de ruido de infraestructura.
Como resultado, tenemos 5 clases para resolver el problema de la bebida:
- Operación de colada
- Operación de bebida
- Operación de atasco
- Registrador
- Fachada de los boolers
Cada uno de ellos es responsable estrictamente de una funcionalidad, tiene una razón para el cambio. Todas las reglas similares a los cambios se encuentran cerca.
Ejemplos de la vida real
Serialización y DeserializaciónComo parte del desarrollo del protocolo de transferencia de datos, es necesario serializar y deserializar algún tipo de "Usuario" en una cadena.
User{ String Name; Int Age; }
Puede pensar que la serialización y la deserialización deben realizarse en clases separadas:
UserDeserializer{ String deserialize(User){...} } UserSerializer{ User serialize(String){...} }
Dado que cada uno de ellos tiene su propia responsabilidad y una razón para el cambio.
Pero tienen una razón común para el cambio: "cambiar el formato de la serialización de datos".
Y al cambiar este formato, la serialización y la deserialización siempre cambiarán.
De acuerdo con el principio de localización de cambios, debemos combinarlos en una clase:
UserSerializer{ String deserialize(User){...} User serialize(String){...} }
Esto nos salva de una complejidad innecesaria y la necesidad de recordar que cada vez que cambie el serializador, debe recordar el deserializador.
Cuenta y ahorraDebe calcular los ingresos anuales de la empresa y guardarlos en el archivo C: \ results.txt.
Rápidamente resolvemos esto con un método:
void SaveGain(Company company){ // // }
Desde la definición de la tarea, está claro que hay dos subtareas: "Calcular ingresos" y "Guardar ingresos". Cada uno de ellos tiene una razón para los cambios: "un cambio en la metodología de cálculo" y "un cambio en el formato de guardado". Estos cambios no se superponen. Además, no podemos responder monosilábicamente a la pregunta: "¿Qué hace el método SaveGain?". Este método AND calcula los ingresos Y guarda los resultados.
Por lo tanto, debe dividir este método en dos:
Gain CalcGain(Company company){..} void SaveGain(Gain gain){..}
Pros:
- se puede probar por separado CalcGain
- es más fácil localizar errores y hacer cambios
- legibilidad del código aumentada
- el riesgo de error en cada uno de los métodos se reduce debido a su simplificación
Lógica empresarial sofisticadaUna vez que escribimos un servicio para el registro automático de un cliente b2b. Y había un método DIOS con 200 líneas de contenido similar:
- Vaya a 1C y obtenga una cuenta
- Con esta cuenta, vaya al módulo de pago y consígalo allí.
- Compruebe que no se haya creado una cuenta con dicha cuenta en el servidor principal
- Crea una cuenta nueva
- El resultado de registro en el módulo de pago y el número 1c se agregan al servicio de resultados de registro
- Agregar información de cuenta a esta tabla
- Cree un número de punto para este cliente en el servicio de puntos. Dé a este servicio el número de cuenta 1s.
Hubo alrededor de 10 operaciones comerciales más con una terrible conexión en esta lista. El objeto de la cuenta era necesario para casi todos. La identificación 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 de trabajar con la cuenta en métodos / clases separados. El método de Dios se hizo más fácil, pero quedaban 100 líneas de código que no querían ser descifradas.
Solo unos días más tarde llegó el entendimiento de que la esencia de este método "aliviado" es el algoritmo comercial. Y que la descripción inicial de los conocimientos tradicionales era bastante complicada. Y es un intento de romper este método en pedazos que será una violación de SRP, y no al revés.
Es hora de dejar nuestra bebida en paz. Limpie las lágrimas, definitivamente volveremos a hacerlo de alguna manera. Ahora formalizamos el conocimiento de este artículo.
- Separe los elementos para que cada uno de ellos sea responsable de una cosa.
- La responsabilidad significa "causa de cambio". Es decir, cada elemento tiene una sola razón para el cambio, en términos de lógica empresarial.
- Posibles cambios en la lógica de negocios. debe ser localizado. Los elementos que son mutables juntos deben estar cerca.
No he cumplido con los criterios suficientes para la implementación de SRP. Pero hay condiciones necesarias:
1) Hágase una pregunta: ¿qué hace esta clase / método / módulo / servicio? debes responderlo con una definición simple. (gracias a Brightori )
explicacionesSin embargo, a veces es muy difícil encontrar una definición simple
2) Corregir un error o agregar una nueva característica afecta el número mínimo de archivos / clases. Idealmente, uno.
explicacionesDado que la responsabilidad (por una característica o error) está encapsulada en un solo archivo / clase, entonces usted sabe exactamente dónde buscar y qué editar. Por ejemplo: la función de cambiar la salida del registro de operación requerirá cambiar solo el registrador. No se requiere correr por el resto del código.
Otro ejemplo es la adición de un nuevo control de IU similar a los anteriores. Si esto le obliga a agregar 10 entidades diferentes y 15 convertidores diferentes, parece que se ha "roto".
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.
explicacionesSi al agregar una nueva operación "Verter vodka debajo de la mesa" necesita tocar el registrador, la operación de beber y verter, entonces parece que las responsabilidades se dividen torcidamente. Por supuesto, esto no siempre es posible, pero debe intentar reducir esta cifra.
4) Al aclarar una pregunta sobre la lógica de negocios (de un desarrollador o gerente), se sube estrictamente a una clase / archivo y recibe información solo de allí.
explicacionesLas características, reglas o algoritmos se escriben de manera compacta en un solo lugar, y no están dispersos por banderas en todo el espacio de código.
5) El nombramiento es claro.
explicacionesNuestra clase o método es responsable de una cosa, y la responsabilidad se refleja en su nombre.
AllManagersManagerService: muy probablemente, clase de Dios
Pago local: probablemente no
Al comienzo del diseño, el hombre mono no sabe ni siente todas las sutilezas del problema que se está resolviendo y puede dar un error. Puede cometer errores de diferentes maneras:
- Hacer objetos demasiado grandes pegando diferentes responsabilidades
- Dividir, dividir una sola responsabilidad en muchos tipos diferentes
- Límites de responsabilidad incorrectamente definidos
Es importante recordar la regla: "es mejor cometer un gran error" o "no estoy seguro, no se divida". Si, por ejemplo, su clase reúne dos responsabilidades, entonces es comprensible y se puede dividir en dos con un cambio mínimo en el código del cliente. Recoger un vaso de fragmentos de vidrio suele ser más difícil debido al contexto extendido en varios archivos y la falta de dependencias necesarias en el código del cliente.
Es hora de redondear
El alcance de SRP no se limita a OOP y SOLID. Es aplicable a métodos, funciones, clases, módulos, microservicios y servicios. Se aplica tanto al desarrollo "figax-figax-and-in-prod" como al desarrollo "rocket-sainz", lo que hace que el mundo sea un poco mejor en todas partes. Si lo piensa, este es casi el principio fundamental de toda la ingeniería. La ingeniería mecánica, los sistemas de control y, de hecho, todos los sistemas complejos están construidos a partir de componentes, y la "fragmentación incompleta" priva a los diseñadores de flexibilidad, "fragmentación", de eficiencia y límites incorrectos, de razón y tranquilidad.

SRP no es inventado por la naturaleza y no es parte de la ciencia exacta. Se arrastra de nuestras limitaciones biológicas y psicológicas, esta es solo una forma de controlar y desarrollar sistemas complejos utilizando el cerebro de un mono humano. Nos dice cómo descomponer el sistema. La redacción original requería una buena cantidad de telepatía, pero espero que este artículo disipe ligeramente la cortina de humo.