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

53

Estoy trabajando en un equipo donde el líder del equipo es un defensor virulento de los principios de desarrollo de SOLID. Sin embargo, carece de mucha experiencia en la obtención de software complejo por 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 altamente 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 se han considerado como una "razón para cambiar" y se han extraído a clases públicas o internas e interfaces que no se ajustan a 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 hay algún enfoque 'refactorial lejos de SRP' que podamos usar para ayudar a solucionar algunos de estos problemas. He leído que no infringe SOLID si creo una cantidad de clases de grano grueso más vacías que "envuelven" una serie de clases estrechamente relacionadas para proporcionar un punto único de acceso a la suma de su funcionalidad (es decir, imitar una implementación de clase excesivamente SRP'd).

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

¿Alguna sugerencia?

    
pregunta Dean Chalk 29.05.2012 - 12:19

7 respuestas

80

Si su clase tiene 20 parámetros en el constructor, no parece que su equipo sepa muy bien qué es el SRP. Si tienes una clase que solo hace una cosa, ¿cómo tiene 20 dependencias? Es como ir de pesca y llevar una caña de pescar, una caja de aparejos, artículos para acolchar, una bola de bolos, un nunchucks, un lanzallamas, etc ... Si necesita todo eso para ir a pescar, no solo va a pescar.

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

Entonces, supongo que lo que estoy diciendo 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.

EDIT

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. Piensa primero: "¿Cómo me gustaría usar esta clase" y luego implementarlo? 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 piensas mucho en la usabilidad.

EDIT 2

Si está buscando consejos para refactorizar, puede comenzar a hacer lo que sugirió: cree clases más complejas para incluir otras. Asegúrese de que la clase de grano grueso aún se adhiera al SRP , pero en un nivel más alto. Entonces tienes dos alternativas:

  1. Si las clases de grano más fino ya no se usan en otras partes del sistema, puede jalar su implementación gradualmente a la clase de grano más grueso y eliminarlas.
  2. Deja las clases de grano fino solo. Quizás fueron bien diseñados y solo necesitabas la envoltura para que sean más fáciles de usar. Sospecho que este es el caso de gran parte de su proyecto.

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

    
respondido por el Phil 30.05.2012 - 05:28
28

Creo que es en la Refactorización de Martin Fowler que una vez leí una regla contraria a SRP, definiendo a dónde va demasiado lejos. Hay una segunda pregunta, tan importante como "¿todas las clases tienen una sola razón para cambiar?" y eso es "¿todos los cambios solo afectan a una clase?"

Si la respuesta a la primera pregunta es, en todos los casos, "sí", pero la segunda pregunta es "ni siquiera cerca", entonces debe observar cómo está implementando el 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ía repensar cómo ha implementado el 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. Así que decides crear un objeto CustomerPersistence también. Pero si lo hace de manera tal que agregar un campo STILL requiera un cambio en el objeto CustomerPersisitence, ¿cuál fue el punto? Todavía tiene un objeto con dos razones para cambiar: simplemente 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 el número de veces que digo "no, hay más de una razón para cambiar este objeto" y el número de veces que digo "no, este cambio afectará a más de un objeto ", entonces creo que tengo el equilibrio correcto entre el SRP y la fragmentación. Pero si ambos siguen siendo altos, entonces me pregunto si hay una manera diferente de separar las preocupaciones.

    
respondido por el pdr 30.05.2012 - 05:58
21

El hecho de que un sistema sea complejo no significa que tenga que hacerlo complicado . Si tienes 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? Apostaría si escribiera lo que MyAwesomeClass hace en una tarjeta CRC que no encajaría en una tarjeta de índice o tiene que escribir en letras ilegibles muy pequeñas.

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. Podría argumentar que las dependencias son objetos de dominio (lo que ocurre); sin embargo, tener una clase que maneje 20 objetos de dominio al mismo tiempo lo está estirando demasiado.

TDD le proporcionará un buen indicador de cuánto hace una clase. Poner bruscamente Si un método de prueba tiene un código de configuración que tarda mucho tiempo en escribir (incluso si refactoriza las pruebas), es probable que su MyAwesomeClass tenga demasiadas cosas que hacer.

Entonces, ¿cómo resuelves este enigma? Mueve 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 realiza su clase con sus dependencias.
  2. Agrupe las acciones según dependencias estrechamente relacionadas.
  3. Redelegate! I.e. refactorice cada una de las acciones identificadas a clases nuevas o, lo que es más importante, a otras.

Un ejemplo abstracto sobre las responsabilidades de refactorización

Deje que C sea una clase que tenga varias dependencias D1 , D2 , D3 , D4 que necesita refactorizar para usar menos. Cuando identificamos qué métodos llama C a las dependencias, podemos hacer una lista simple de ellos:

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

Mirando la lista, podemos ver que D1 y D2 están relacionados entre sí ya que la clase los necesita juntos de alguna manera. También podemos ver que D4 necesita D3 . Así que tenemos dos agrupaciones:

  • Group 1 - D1 < - > %código%
  • D2 - Group 2 - > %código%

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

  1. D4 : uno para manejar los objetos que llaman dos que se necesitan entre sí. Quizás pueda dejar que su clase D3 elimine la necesidad de manejar ambas dependencias y que una de ellas maneje esas llamadas en su lugar. En esta agrupación, es obvio que Group 1 podría tener una referencia a C .
  2. D1 - La otra responsabilidad necesita un objeto para llamar a otro. ¿No puede D2 manejar Group 2 en lugar de tu clase? Entonces, probablemente podamos eliminar D4 de la clase D3 permitiendo que D3 haga las llamadas en su lugar.

No tomes mi respuesta como está escrita en piedra, 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 podrían 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 suena como si su equipo supiera qué es SRP. Si tiene una clase que solo hace una cosa, ¿cómo tiene 20 dependencias?", creo que si tienes una clase de Cliente, 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 configurar en su constructor, y los parámetros generalmente son tipos simples como cadenas. Sin embargo, en el ejemplo de una clase C , aún puede agrupar sus propiedades dentro de otras clases para simplificar las cosas. Como tener una clase D4 con calles y una clase Customer que contiene el código postal y manejará la lógica de negocios como la validación de datos también:

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 detalladamente 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 utilizar un patrón de generador de fluidos .

    
respondido por el Spoike 30.05.2012 - 13:43
3

Estoy de acuerdo con todas las respuestas sobre SRP y sobre cómo se puede llevar demasiado lejos. En tu publicación mencionas que debido a la "sobre-refactorización" para adherirte a SRP, encontraste que la encapsulación se rompe o se modifica. Lo único que me ha funcionado es siempre atenerse a lo básico y hacer exactamente lo que se requiere para alcanzar un fin.

Cuando se trabaja con sistemas Legacy, el "entusiasmo" de arreglar todo para mejorar es generalmente bastante alto en los Líderes de equipo, especialmente aquellos que son nuevos en ese rol. SOLID, simplemente no tiene SRP. Eso es solo la S. Asegúrese de que si está siguiendo SOLID, no olvide el OLID también.

Estoy trabajando en un sistema Legacy en este momento y comenzamos a tomar un camino similar al principio. Lo que funcionó para nosotros fue una decisión colectiva del equipo de sacar lo mejor de ambos mundos: SOLID y K.I.S.S (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 pautas, no como "Leyes de Desarrollo de S / W". El equipo no se trata solo del líder del equipo, sino de todos los desarrolladores del equipo. Lo que siempre me ha funcionado es lograr que todos estén en una habitación y elaborar un conjunto de pautas compartidas que todo el equipo acuerda seguir.

En cuanto a cómo solucionar su situación actual, si usa un VCS y no ha agregado demasiadas funciones nuevas a su aplicación, siempre puede volver a una versión de código que todo el equipo considera comprensible, legible y mantenible. ¡Sí! Te estoy pidiendo que deseches el trabajo y comiences desde cero. Esto es mejor que intentar "arreglar" algo que se rompió y moverlo de nuevo a algo que ya existía.

    
respondido por el Sharath Satish 01.06.2012 - 10:38
3

Muchas de las respuestas aquí son realmente buenas, pero se centran en el aspecto técnico de este problema. Simplemente agregaré que suena como los intentos del desarrollador de seguir el sonido de 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 entre varias clases, se viola la responsabilidad del 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 al 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 "razón del cambio" y no "hacer una cosa", y que no necesita preocuparse por esa razón del cambio hasta que realmente ocurra un cambio. El segundo hombre paga por la abstracción.

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

En serio, si no bebes, ve a una cafetería. Sal de la oficina y en algún lugar relajado, donde puedas hablar informalmente sobre estas cosas. En lugar de intentar ganar una discusión en una reunión, lo que no harás es tener una discusión en algún lugar divertido. Intenta reconocer que este desarrollador, que te está volviendo loco, es un humano en funcionamiento que está intentando sacar el software "por la puerta" y no quiere enviar basura. Como es probable que compartas ese terreno común, puedes comenzar a discutir cómo mejorar el diseño mientras sigues cumpliendo con el SRP.

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

    
respondido por el Eric Smith 19.01.2016 - 14:27
2

La respuesta es la facilidad de mantenimiento y la claridad del código por encima de todo lo demás. 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 cuando agrego una nueva función, pienso en la cantidad de repetición necesaria en comparación con la lógica real. Si la respuesta es más del 50%, probablemente significa que estoy pensando demasiado.

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

    
respondido por el cmcginty 08.06.2012 - 22:21
-1

Estoy de acuerdo con la decisión del líder de su equipo [actualización = 2012.05.31] de que el SRP es generalmente una buena idea. Pero estoy totalmente de acuerdo con el comentario de @ Spoike -s de que un constructor con 20 argumentos de interfaz está muy lejos. [/ Actualización]:

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

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

Me temo que no puedes reducir la fragmentación de código sin sacrificar srp.

Pero puede "aliviar el dolor" de la inicialización de código 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(), ....)
      }
   }
    
respondido por el k3b 29.05.2012 - 14:08

Lea otras preguntas en las etiquetas