Principio de responsabilidad única: ¿cómo puedo evitar la fragmentación del código?


57

Estoy trabajando en un equipo donde el líder del equipo es un defensor virulento de los principios de desarrollo SOLID. Sin embargo, carece de mucha experiencia en sacar el software complejo de la puerta.

Tenemos una situación en la que ha aplicado SRP a lo que ya era una base de código bastante compleja, que ahora se ha vuelto muy fragmentada y difícil de entender y depurar.

Ahora tenemos un problema no solo con la fragmentación del código, sino también con la encapsulación, ya que los métodos dentro de una clase que pueden haber sido privados o protegidos han sido considerados como una 'razón para cambiar' y han sido extraídos a clases e interfaces públicas o internas que no está de acuerdo con los objetivos de encapsulación de la aplicación.

Tenemos algunos constructores de clase que toman más de 20 parámetros de interfaz, por lo que nuestro registro y resolución de IoC se está convirtiendo en un monstruo por derecho propio.

Quiero saber si existe algún enfoque de "refactorización fuera de SRP" que podamos utilizar para ayudar a solucionar algunos de estos problemas. He leído que no viola SOLID si creo un número de clases vacías de grano grueso que 'envuelven' un número de clases estrechamente relacionadas para proporcionar un punto de acceso único a la suma de su funcionalidad (es decir, imitando un menor implementación de clase demasiado SRP'd).

Aparte de eso, no puedo pensar en una solución que nos permita continuar pragmáticamente con nuestros esfuerzos de desarrollo, mientras mantenemos contentos a todos.

Alguna sugerencia ?


18
Esa es solo mi opinión, pero creo que hay una regla más, que se olvida muy fácilmente bajo la pila de varios acrónimos: "Principio del sentido común". Cuando una 'solución' crea más problemas que realmente resuelve, entonces algo está mal. Mi opinión es que si un problema es complejo, pero está encerrado en una clase que se ocupa de sus complejidades y aún es relativamente fácil de depurar, lo dejo solo. En general, su idea de "envoltorio" me parece sólida, pero dejaré la respuesta a alguien más conocedor.
Patryk Ćwiek

66
En cuanto a la "razón para cambiar", no hay necesidad de especular sobre todas las razones prematuramente. Espere hasta que realmente tenga que cambiar eso, y luego vea qué se puede hacer para facilitar este tipo de cambio en el futuro.

6262
¡Una clase con 20 parámetros de constructor no me suena muy SRP!
MattDavey

1
Escribe "... Registro y resolución de IoC ..."; Esto suena como si usted (o el líder de su equipo) cree que "IoC" y "inyección de dependencia" (DI) son lo mismo, lo cual no es cierto. DI es un medio para lograr IoC, pero ciertamente no es el único. Debe analizar cuidadosamente por qué quiere hacer IoC; si es porque desea escribir pruebas unitarias, entonces también podría intentar usar el patrón de localización del servicio o simplemente las clases de interfaz ( ISomething). En mi humilde opinión, estos enfoques son mucho más fáciles de manejar que la inyección de dependencia y dan como resultado un código más legible.

2
cualquier respuesta dada aquí estaría en el vacío; Tendríamos que ver el código para dar una respuesta específica. 20 parámetros en un constructor? bueno, es posible que te falte un objeto ... o que todos sean válidos; o podrían pertenecer a un archivo de configuración, o podrían pertenecer a una clase DI, o ... Los síntomas ciertamente suenan sospechosos, pero como la mayoría de las cosas en CS, "depende" ...
Steven A. Lowe

Respuestas:


85

Si su clase tiene 20 parámetros en el constructor, no parece que su equipo sepa lo que es SRP. Si tiene una clase que solo hace una cosa, ¿cómo tiene 20 dependencias? Es como ir a un viaje de pesca y traer una caña de pescar, una caja de aparejos, suministros para acolchar, bola de boliche, nunchucks, lanzallamas, etc. Si necesita todo eso para ir a pescar, no solo va a pescar.

Dicho esto, SRP, como la mayoría de los principios, puede aplicarse en exceso. Si crea una nueva clase para incrementar enteros, entonces sí, esa puede ser una responsabilidad única, pero vamos. Eso es ridículo. Tendemos a olvidar que cosas como los principios SOLID están ahí para un propósito. SOLID es un medio para un fin, no un fin en sí mismo. El fin es la mantenibilidad . Si va a obtener esa granularidad con el Principio de responsabilidad única, es un indicador de que el celo por SOLID ha cegado al equipo a la meta de SOLID.

Entonces, supongo que lo que digo es ... El SRP no es tu problema. Es un malentendido del SRP o una aplicación increíblemente granular del mismo. Intenta que tu equipo mantenga lo principal como lo principal. Y lo principal es la mantenibilidad.

EDITAR

Haga que las personas diseñen módulos de una manera que fomente la facilidad de uso. Piense en cada clase como una mini API. Piense primero, "¿Cómo me gustaría usar esta clase" y luego impleméntela. No solo piense "¿Qué necesita hacer esta clase?" El SRP tiene una gran tendencia a hacer que las clases sean más difíciles de usar, si no piensa mucho en la usabilidad.

EDITAR 2

Si está buscando consejos sobre refactorización, puede comenzar a hacer lo que sugirió: crear clases más generales para envolver a otras. Asegúrese de que la clase de grano más grueso todavía se adhiera al SRP , pero en un nivel superior. Entonces tienes dos alternativas:

  1. Si las clases de grano más fino ya no se usan en otras partes del sistema, puede llevar gradualmente su implementación a la clase de grano más grueso y eliminarlas.
  2. Deja las clases más finas en paz. Tal vez se diseñaron bien y solo necesitaba el envoltorio para que sean más fáciles de usar. Sospecho que este es el caso de gran parte de su proyecto.

Cuando termine de refactorizar (pero antes de comprometerse con el repositorio), revise su trabajo y pregúntese si su refactorización fue realmente una mejora en la facilidad de mantenimiento y facilidad de uso.


2
Una forma alternativa de hacer que la gente piense en diseñar clases: permítales escribir tarjetas CRC (nombre de clase, responsabilidad, colaboradores) . Si una clase tiene demasiados colaboradores o responsabilidades, lo más probable es que no sea lo suficientemente SRP. En otras palabras, todo el texto tiene que caber en la tarjeta de índice, o de lo contrario está haciendo demasiado.
Spoike

18
Sé para qué sirve el lanzallamas, pero ¿cómo diablos pescas con una caña?
R. Martinho Fernandes

13
+1 SOLID es un medio para un fin, no un fin en sí mismo.
B Siete

1
+1: He argumentado antes que cosas como "La Ley de Deméter" están mal nombradas, debería ser "La línea Guía de Deméter". Estas cosas deberían funcionar para usted, no debería estar trabajando para ellas.
Binario Worrier

2
@EmmadKareem: Es cierto que se supone que los objetos DAO tienen varias propiedades. Pero, de nuevo, hay varias cosas que puede agrupar en algo tan simple como una Customerclase y tener un código más fácil de mantener. Ver ejemplos aquí: codemonkeyism.com/…
Spoike

33

Creo que es en la Refactorización de Martin Fowler que una vez leí una contra-regla a SRP, definiendo a dónde va demasiado lejos. Hay una segunda pregunta, tan importante como "¿tiene cada clase una sola razón para cambiar?" y eso es "¿cada cambio solo afecta a una clase?"

Si la respuesta a la primera pregunta es, en todos los casos, "sí", pero la segunda pregunta es "ni siquiera cerrada", entonces debe analizar nuevamente cómo está implementando SRP.

Por ejemplo, si agregar un campo a una tabla significa que tiene que cambiar un DTO y una clase de validador y una clase de persistencia y un objeto de modelo de vista, etc., entonces ha creado un problema. Tal vez deberías repensar cómo has implementado SRP.

Quizás haya dicho que agregar un campo es la razón para cambiar el objeto Cliente, pero cambiar la capa de persistencia (por ejemplo, de un archivo XML a una base de datos) es otra razón para cambiar el objeto Cliente. Entonces decide crear un objeto CustomerPersistence también. Pero si lo hace de tal manera que agregar un campo TODAVÍA requiere un cambio en el objeto CustomerPersisitence, ¿cuál fue el punto? Todavía tiene un objeto con dos razones para cambiar: ya no es Cliente.

Sin embargo, si introduce un ORM, es muy posible que pueda hacer que las clases funcionen de manera que si agrega un campo al DTO, cambiará automáticamente el SQL utilizado para leer esos datos. Entonces tienes una buena razón para separar las dos preocupaciones.

En resumen, esto es lo que tiendo a hacer: si hay un balance aproximado entre la cantidad de veces que digo "no, hay más de una razón para cambiar este objeto" y la cantidad de veces que digo "no, este cambio será afectar a más de un objeto ", entonces creo que tengo el equilibrio correcto entre SRP y la fragmentación. Pero si ambos siguen altos, empiezo a preguntarme si hay una manera diferente de separar las preocupaciones.


+1 para "¿cada cambio solo afecta a una clase?"
dj18

Un tema relacionado que no he visto discutido es que si las tareas que están vinculadas a una entidad lógica se fragmentan entre diferentes clases, entonces puede ser necesario que el código contenga referencias a múltiples objetos distintos que están vinculados a la misma entidad. Considere, por ejemplo, un horno con funciones "SetHeaterOutput" y "MeasureTemperature". Si el horno estuviera representado por objetos independientes de HeaterControl y TemperatureSensor, nada impediría que un objeto TemperatureFeedbackSystem contenga una referencia al calentador de un horno y a un sensor de temperatura del horno diferente.
supercat

1
Si, en cambio, esas funciones se combinaron en una interfaz IKiln, que fue implementada por un objeto Kiln, entonces el TemperatureFeedbackSystem solo necesitaría contener una sola referencia IKiln. Si fuera necesario usar un horno con un sensor de temperatura independiente del mercado de accesorios, se podría usar un objeto CompositeKiln cuyo constructor aceptó un IHeaterControl e ITemperatureSensor y los usó para implementar IKiln, pero dicha composición deliberada y suelta sería fácilmente reconocible en el código.
supercat

24

El hecho de que un sistema sea complejo no significa que deba complicarlo . Si tiene una clase que tiene demasiadas dependencias (o Colaboradores) como esta:

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

... entonces se volvió demasiado complicado y realmente no estás siguiendo SRP , ¿verdad? Apuesto a que si anota lo que MyAwesomeClasshace en una tarjeta CRC , no cabría en una tarjeta índice o si tiene que escribir en letras minúsculas ilegibles.

Lo que tienes aquí es que tus muchachos solo siguieron el Principio de segregación de interfaz y pueden haberlo llevado al extremo, pero esa es otra historia. Se podría argumentar que las dependencias son objetos de dominio (lo que sucede), sin embargo, tener una clase que maneje 20 objetos de dominio al mismo tiempo es demasiado extenso.

TDD le proporcionará un buen indicador de cuánto hace una clase. Sin rodeos puesto; Si un método de prueba tiene un código de configuración que tarda una eternidad en escribir (incluso si refactoriza las pruebas), entonces MyAwesomeClassprobablemente tenga demasiadas cosas que hacer.

Entonces, ¿cómo se resuelve este enigma? Mueves las responsabilidades a otras clases. Hay algunos pasos que puede seguir en una clase que tiene este problema:

  1. Identifique todas las acciones (o responsabilidades) que su clase hace con sus dependencias.
  2. Agrupe las acciones según dependencias estrechamente relacionadas.
  3. Redelegar! Es decir, refactorizar cada una de las acciones identificadas a nuevas o (más importante) otras clases.

Un ejemplo abstracto sobre refactorizar responsabilidades

Vamos a Cser una clase que tiene varias dependencias D1, D2, D3, D4que es necesario perfeccionar por usar menos. Cuando identificamos los métodos que Crequieren las dependencias, podemos hacer una lista simple:

  • D1- performA(D2),performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

Mirando la lista podemos ver eso D1y D2estamos relacionados entre sí ya que la clase los necesita juntos de alguna manera. También podemos ver que las D4necesidades D3. Entonces tenemos dos agrupaciones:

  • Group 1- D1<->D2
  • Group 2- D4->D3

Las agrupaciones son un indicador de que la clase ahora tiene dos responsabilidades.

  1. Group 1- Uno para manejar la llamada de dos objetos que se necesitan mutuamente. Tal vez pueda dejar que su clase Celimine la necesidad de manejar ambas dependencias y dejar que una de ellas maneje esas llamadas. En esta agrupación, es obvio que D1podría tener una referencia D2.
  2. Group 2- La otra responsabilidad necesita un objeto para llamar a otro. No se puede D4manejar en D3lugar de su clase? Entonces, probablemente podemos eliminar D3de la clase Cdejando en su lugar D4hacer las llamadas.

No tome mi respuesta tal como está escrita, ya que el ejemplo es muy abstracto y hace muchas suposiciones. Estoy bastante seguro de que hay más formas de refactorizar esto, pero al menos los pasos pueden ayudarlo a obtener algún tipo de proceso para mover las responsabilidades en lugar de dividir las clases.


Editar:

Entre los comentarios @Emmad Karem dice:

"Si su clase tiene 20 parámetros en el constructor, no parece que su equipo sepa qué es SRP. Si tiene una clase que solo hace una cosa, ¿cómo tiene 20 dependencias?" - Creo que si usted tener una clase Customer, no es extraño tener 20 parámetros en el constructor.

Es cierto que los objetos DAO tienden a tener muchos parámetros, que debe establecer en su constructor, y los parámetros generalmente son tipos simples como cadena. Sin embargo, en el ejemplo de una Customerclase, aún puede agrupar sus propiedades dentro de otras clases para simplificar las cosas. Como tener una Addressclase con calles y una Zipcodeclase que contenga el código postal y que también maneje la lógica comercial, como la validación de datos:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

Esto se trata más adelante en la publicación del blog "Nunca, nunca, nunca use String en Java (o al menos a menudo)" . Como alternativa al uso de constructores o métodos estáticos para hacer que los subobjetos sean más fáciles de crear, puede usar un patrón de generador de fluidos .


+1: ¡Gran respuesta! La agrupación es IMO, un mecanismo muy poderoso porque puede aplicar la agrupación de forma recursiva. Hablando en términos generales, con n capas de abstracción puede organizar 2 ^ n elementos.
Giorgio

+1: Tus primeros párrafos resumen exactamente a qué se enfrenta mi equipo. "Business Objects" que en realidad son objetos de servicio, y un código de configuración de prueba de unidad que es muy aburrido escribir Sabía que teníamos un problema cuando nuestras llamadas a la capa de servicio contendrían una línea de código; una llamada a un método de capa empresarial.
Hombre

3

Estoy de acuerdo con todas las respuestas sobre SRP y cómo se puede llevar demasiado lejos. En su publicación, menciona que debido a una "refactorización excesiva" para adherirse a SRP, encontró que la encapsulación se rompió o se modificó. Lo único que me ha funcionado es mantener siempre lo básico y hacer exactamente lo que se requiere para alcanzar un fin.

Cuando se trabaja con sistemas Legacy, el "entusiasmo" por arreglar todo para mejorarlo suele ser bastante alto en Team Leads, especialmente aquellos que son nuevos en ese rol. SOLID, simplemente no tiene SRP: esa es solo la S. Asegúrese de que si está siguiendo SOLID, no se olvide también de OLID.

Estoy trabajando en un sistema Legacy en este momento y comenzamos a seguir un camino similar al principio. Lo que funcionó para nosotros fue una decisión colectiva del equipo para sacar lo mejor de ambos mundos: SOLID y KISS (Keep It Simple Stupid). Discutimos colectivamente cambios importantes en la estructura del código y aplicamos el sentido común al aplicar varios principios de desarrollo. Son excelentes como directrices, no como "Leyes de desarrollo S / W". El equipo no se trata solo del Team Lead, se trata de todos los desarrolladores del equipo. Lo que siempre ha funcionado para mí es hacer que todos estén en una habitación y elaborar un conjunto de pautas compartidas que todo su equipo acuerde seguir.

Con respecto a cómo solucionar su situación actual, si usa un VCS y no ha agregado demasiadas características nuevas a su aplicación, siempre puede volver a una versión de código que todo el equipo considere comprensible, legible y fácil de mantener. ¡Si! Te pido que deseches el trabajo y comiences desde cero. Esto es mejor que tratar de "arreglar" algo que estaba roto y moverlo de nuevo a algo que ya existía.


3

La respuesta es la facilidad de mantenimiento y la claridad del código por encima de todo. Para mí eso significa escribir menos código , no más. Menos abstracciones, menos interfaces, menos opciones, menos parámetros.

Cada vez que evalúo una reestructuración de código o agrego una nueva característica, pienso en la cantidad de repeticiones necesarias en comparación con la lógica real. Si la respuesta es más del 50%, probablemente significa que ya no lo pienso.

Además de SRP, hay muchos otros estilos de desarrollo. En su caso, parece que definitivamente falta YAGNI.


3

Muchas de las respuestas aquí son realmente buenas, pero se centran en el aspecto técnico de este problema. Simplemente agregaré que parece que los intentos del desarrollador de seguir el sonido SRP como si realmente violaran el SRP.

Puede ver el blog de Bob aquí sobre esta situación, pero argumenta que si una responsabilidad se difunde en varias clases, entonces se viola la responsabilidad SRP porque esas clases cambian en paralelo. Sospecho que a tu desarrollador realmente le gustaría el diseño en la parte superior del blog de Bob, y podría estar un poco decepcionado de verlo destrozado. En particular porque viola el "Principio de cierre común": las cosas que cambian juntas permanecen juntas.

Recuerde que el SRP se refiere a la "razón del cambio" y no a "hacer una cosa", y que no necesita preocuparse por esa razón hasta que realmente ocurra un cambio. El segundo tipo paga por la abstracción.

Ahora está el segundo problema: el "defensor virulento del desarrollo SÓLIDO". Seguro que no parece que tengas una gran relación con este desarrollador, por lo que cualquier intento de convencerlo de los problemas en la base de código no funciona. Deberá reparar la relación para poder tener una discusión real de los problemas. Lo que recomendaría es cerveza.

No en serio, si no bebes dirígete a una cafetería. Sal de la oficina y relájate en un lugar donde puedas hablar de estas cosas de manera informal. En lugar de tratar de ganar una discusión en una reunión, que no lo hará, tener una discusión en algún lugar divertido. Trate de reconocer que este desarrollador, que lo está volviendo loco, es un humano que funciona y está tratando de sacar el software "por la puerta" y no quiere enviar basura. Como es probable que comparta ese punto en común, puede comenzar a discutir cómo mejorar el diseño sin dejar de ajustarse al SRP.

Si ambos pueden reconocer que el SRP es algo bueno, que simplemente interpretan los aspectos de manera diferente, probablemente puedan comenzar a tener conversaciones productivas.


-1

Estoy de acuerdo con su decisión de liderar el equipo [actualización = 2012.05.31] de que SRP es generalmente una buena idea. Pero estoy totalmente de acuerdo con el comentario de @ Spoike de que un constructor con 20 argumentos de interfaz es demasiado. [/ Update]:

La introducción de SRP con IoC mueve la complejidad de una "clase multi-responsable" a muchas clases srp y una inicialización mucho más complicada para el beneficio de

  • prueba de unidad / tdd más fácil (prueba de una clase srp aisladamente a la vez)
  • pero a costa de
    • una inicialización e integración de código mucho más difícil y
    • depuración más difícil
    • fragmentación (= distribución de código en varios archivos / directorios)

Me temo que no puede reducir la fragmentación del código sin sacrificar srp.

Pero puede "aliviar el dolor" de la codeinicialización implementando una clase de azúcar sintáctica que oculta la complejidad de la inicialización en un constructor.

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }

2
Creo que 20 interfaces es un indicador de que la clase tiene demasiado que hacer. Es decir, hay 20 razones para que cambie, lo que es más o menos una violación de SRP. El hecho de que el sistema sea complejo no significa que deba ser complicado.
Spoike
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.