¿Cuáles son las formas prácticas de implementar el SRP?

11

¿Cuáles son las técnicas prácticas que las personas usan para verificar si una clase viola el principio de responsabilidad única?

Sé que una clase debería tener una sola razón para cambiar, pero esa oración carece de una forma práctica de implementar eso realmente.

La única forma que encontré es usar la oración "El ......... debería ......... en sí mismo". donde el primer espacio es el nombre de la clase y el segundo es el nombre del método (responsabilidad).

Sin embargo, a veces es difícil determinar si una responsabilidad realmente viola el SRP.

¿Hay más formas de verificar el SRP?

Nota:

La pregunta no es sobre qué significa el SRP, sino más bien una metodología práctica o una serie de pasos para verificar e implementar el SRP.

ACTUALIZAR

Clase de informe

He agregado una clase de muestra que viola claramente el SRP. Sería genial si las personas pudieran usarlo como un ejemplo para explicar cómo abordan el principio de responsabilidad única.

El ejemplo es de aquí .

Songo
fuente
Esta es una regla interesante, pero aún podría escribir: "Una clase de persona puede representarse a sí misma". Esto puede considerarse una violación para SRP, ya que incluir la GUI en la misma clase que contiene reglas de negocios y persistencia de datos no está bien. Así que creo que debe agregar el concepto de dominios arquitectónicos (niveles y capas) y asegurarse de que esta declaración sea válida solo con 1 de esos dominios (como GUI, acceso a datos, etc.)
NoChance
@EmmadKareem Esta regla se mencionó en Head First Object-Oriented Analysis and Design y eso es exactamente lo que pensé al respecto. Le falta algo una forma práctica de implementarlo. Mencionaron que a veces las responsabilidades no serán tan evidentes para el diseñador y que debe usar mucho sentido común para juzgar si el método realmente debería estar en esta clase o no.
Songo
Si realmente quieres entender SRP, lee algunos de los escritos del tío Bob Martin. Su código es uno de los más bonitos que he visto, y confío en que lo que sea que diga sobre SRP no solo sea un buen consejo, sino que sea más que solo agitar las manos.
Robert Harvey
¿Y el votante en contra explicaría por qué mejorar la publicación?
Songo

Respuestas:

7

El SRP establece, en términos inequívocos, que una clase solo debería tener una razón para cambiar.

Deconstruyendo la clase "informe" en la pregunta, tiene tres métodos:

  • printReport
  • getReportData
  • formatReport

Ignorando el uso redundante Reporten todos los métodos, es fácil ver por qué esto viola el SRP:

  • El término "imprimir" implica algún tipo de interfaz de usuario, o una impresora real. Por lo tanto, esta clase contiene cierta cantidad de IU o lógica de presentación. Un cambio en los requisitos de la interfaz de usuario requerirá un cambio en la Reportclase.

  • El término "datos" implica una estructura de datos de algún tipo, pero realmente no especifica qué (XML? JSON? CSV?). De todos modos, si el "contenido" del informe cambia alguna vez, este método también lo hará. Hay acoplamiento a una base de datos o un dominio.

  • formatReportes simplemente un nombre terrible para un método en general, pero supongo que al mirarlo una vez más tiene algo que ver con la interfaz de usuario, y probablemente un aspecto diferente de la interfaz de usuario que printReport. Entonces, otra razón no relacionada para cambiar.

Por lo tanto, esta clase posiblemente se combina con una base de datos, un dispositivo de pantalla / impresora y alguna lógica de formato interno para registros o salida de archivos o cualquier otra cosa. Al tener las tres funciones en una clase, está multiplicando el número de dependencias y triplicando la probabilidad de que cualquier cambio de dependencia o requisito rompa esta clase (o algo más que dependa de ella).

Parte del problema aquí es que has elegido un ejemplo particularmente espinoso. Probablemente no debería tener una clase llamada Report, incluso si solo hace una cosa , porque ... ¿qué informe? ¿No son todos los "informes" bestias completamente diferentes, basadas en diferentes datos y diferentes requisitos? ¿Y no es un informe algo que ya ha sido formateado, ya sea para pantalla o para impresión?

Pero, mirando más allá de eso, y creando un nombre concreto hipotético, llamémoslo IncomeStatement(un informe muy común), una arquitectura "SRPed" adecuada tendría tres tipos:

  • IncomeStatement- el dominio y / o la clase de modelo que contiene y / o calcula la información que aparece en los informes formateados.

  • IncomeStatementPrinter, que probablemente implementaría alguna interfaz estándar como IPrintable<T>. Tiene un método clave Print(IncomeStatement), y tal vez algunos otros métodos o propiedades para configurar ajustes específicos de impresión.

  • IncomeStatementRenderer, que maneja el renderizado de pantalla y es muy similar a la clase de impresora.

  • También podría eventualmente agregar más clases específicas de características como IncomeStatementExporter/ IExportable<TReport, TFormat>.

Esto se hace significativamente más fácil en los idiomas modernos con la introducción de genéricos y contenedores IoC. La mayor parte del código de su aplicación no necesita depender de la IncomeStatementPrinterclase específica , puede usar IPrintable<T>y, por lo tanto, operar en cualquier tipo de informe imprimible, que le brinda todos los beneficios percibidos de una Reportclase base con un printmétodo y ninguna de las violaciones habituales de SRP . La implementación real solo debe declararse una vez, en el registro del contenedor de IoC.

Algunas personas, cuando se enfrentan con el diseño anterior, responden con algo como: "¡pero esto parece un código de procedimiento, y el objetivo de OOP era alejarnos de la separación de datos y comportamiento!" A lo que digo: mal .

No IncomeStatementse trata solo de "datos", y el error antes mencionado es lo que hace que mucha gente de OOP sienta que está haciendo algo mal al crear una clase tan "transparente" y, posteriormente, comienza a atascar todo tipo de funcionalidades no relacionadas en IncomeStatement(bueno, eso y pereza general). Esta clase puede comenzar solo como datos pero, con el tiempo, garantizada, terminará siendo más un modelo .

Por ejemplo, una cuenta de resultados real tiene ingresos totales , los gastos totales y los ingresos netos líneas. Es muy probable que un sistema financiero diseñado adecuadamente no almacene estos datos porque no son datos transaccionales; de hecho, cambian en función de la adición de nuevos datos transaccionales. Sin embargo, el cálculo de estas líneas siempre será exactamente el mismo, sin importar si está imprimiendo, renderizando o exportando el informe. Por lo que su IncomeStatementclase va a tener una buena cantidad de comportamiento a ella en la forma de getTotalRevenues(), getTotalExpenses()y getNetIncome()métodos, y probablemente varios otros. Es un objeto genuino de estilo OOP con su propio comportamiento, incluso si realmente no parece "hacer" mucho.

Pero el formaty printmétodos, que no tienen nada que ver con la propia información. De hecho, no es demasiado improbable que desee tener varias implementaciones de estos métodos, por ejemplo, una declaración detallada para la administración y una declaración no tan detallada para los accionistas. La separación de estas funciones independientes en diferentes clases le brinda la posibilidad de elegir diferentes implementaciones en tiempo de ejecución sin la carga de un print(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)método único para todos . ¡Qué asco!

Esperemos que pueda ver dónde falla el método anterior, parametrizado masivamente, y dónde van bien las implementaciones separadas; en el caso de un solo objeto, cada vez que agrega una nueva arruga a la lógica de impresión, debe cambiar su modelo de dominio ( Tim en finanzas quiere números de página, pero solo en el informe interno, ¿puede agregar eso? ) en lugar de simplemente agregando una propiedad de configuración a una o dos clases de satélite en su lugar.

Implementar el SRP correctamente se trata de administrar dependencias . En pocas palabras, si una clase ya hace algo útil, y está considerando agregar otro método que introduciría una nueva dependencia (como una interfaz de usuario, una impresora, una red, un archivo, lo que sea), no lo haga . Piense en cómo podría agregar esta funcionalidad en una nueva clase y cómo podría hacer que esta nueva clase se ajuste a su arquitectura general (es bastante fácil cuando diseña alrededor de la inyección de dependencia). Ese es el principio / proceso general.


Nota al margen: Al igual que Robert, rechazo patentemente la noción de que una clase compatible con SRP debe tener solo una o dos variables de estado. Raramente se podría esperar que una envoltura tan delgada haga algo realmente útil. Así que no te excedas con esto.

Aaronaught
fuente
+1 gran respuesta de hecho. Sin embargo, estoy confundido acerca de la clase IncomeStatement. ¿Su diseño propuesto que la media IncomeStatementtendrá instancias de IncomeStatementPrinterY IncomeStatementRendererde manera que cuando llamo print()en IncomeStatementque va a delegar la llamada a la IncomeStatementPrintervez?
Songo
@Songo: ¡Absolutamente no! No debe tener dependencias cíclicas si está siguiendo SOLID. Aparentemente, mi respuesta no dejó en claro que la IncomeStatementclase no tiene un printmétodo, o un formatmétodo, o cualquier otro método que no se ocupe directamente de inspeccionar o manipular los datos del informe. Para eso están esas otras clases. Si desea imprimir uno, entonces depende de la IPrintable<IncomeStatement>interfaz que está registrada en el contenedor.
Aaronaught
Ah, ya veo tu punto. Sin embargo, ¿dónde está la dependencia cíclica si inyecto una Printerinstancia en la IncomeStatementclase? La forma en que me imagino que es cuando lo llamo IncomeStatement.print()lo delegará IncomeStatementPrinter.print(this, format). ¿Qué hay de malo en este enfoque? ... Otra pregunta, mencionó que IncomeStatementdebería contener la información que aparece en los informes formateados si quiero que se lea de la base de datos o de un archivo XML, si extraigo el método que carga los datos en una clase separada y delegarle la llamada IncomeStatement?
Songo
@Songo: Tienes IncomeStatementPrinterdependiendo IncomeStatementy IncomeStatementdependiendo de IncomeStatementPrinter. Esa es una dependencia cíclica. Y es simplemente un mal diseño; no hay ninguna razón IncomeStatementpara saber algo sobre una Printero IncomeStatementPrinter: es un modelo de dominio, no le preocupa la impresión, y la delegación no tiene sentido ya que cualquier otra clase puede crear o adquirir una IncomeStatementPrinter. No hay una buena razón para tener alguna noción de impresión en el modelo de dominio.
Aaronaught
En cuanto a cómo se carga IncomeStatementdesde la base de datos (o archivo XML), normalmente, esto se maneja mediante un repositorio y / o mapeador, no el dominio, y una vez más, no se delega a esto en el dominio; Si alguna otra clase necesita leer uno de estos modelos, entonces solicita explícitamente ese repositorio . A menos que esté implementando el patrón Active Record, supongo, pero realmente no soy un fanático.
Aaronaught
2

La forma en que verifico el SRP es verificar cada método (responsabilidad) de una clase y hacer la siguiente pregunta:

"¿Alguna vez tendré que cambiar la forma en que implemento esta función?"

Si encuentro una función que necesitaré implementar de diferentes maneras (dependiendo de algún tipo de configuración o condición), entonces estoy seguro de que necesito una clase adicional para manejar esta responsabilidad.

John Raya
fuente
1

Aquí hay una cita de la regla 8 de Object Calisthenics :

La mayoría de las clases deberían ser responsables de manejar una sola variable de estado, pero hay algunas que requieren dos. Agregar una nueva variable de instancia a una clase disminuye inmediatamente la cohesión de esa clase. En general, mientras programa bajo estas reglas, encontrará que hay dos tipos de clases, las que mantienen el estado de una variable de instancia única y las que coordinan dos variables separadas. En general, no mezcle los dos tipos de responsabilidades.

Dada esta visión (algo idealista), se podría decir que cualquier clase que contenga solo una o dos variables de estado es poco probable que viole el SRP. También podría decir que cualquier clase que contenga más de dos variables de estado puede violar SRP.

MattDavey
fuente
2
Este punto de vista es irremediablemente simplista. Incluso la famosa pero simple ecuación de Einstein requiere dos variables.
Robert Harvey
La pregunta de los OP fue "¿Hay más formas de verificar el SRP?" - Este es un posible indicador. Sí, es simplista y no se sostiene en todos los casos, pero es una forma posible de verificar que se haya violado SRP.
MattDavey
1
Sospecho que el estado mutable vs inmutable también es una consideración importante
jk.
La regla 8 describe el proceso perfecto para crear diseños que tienen miles y miles de clases, lo que hace que el sistema sea irremediablemente complejo, incomprensible e imposible de mantener. Pero el lado positivo es que puedes seguir SRP.
Dunk
@Dunk No estoy en desacuerdo con usted, pero esa discusión está completamente fuera de tema para la pregunta.
MattDavey
1

Una posible implementación (en Java). Me tomé libertades con los tipos de retorno, pero sobre todo creo que responde a la pregunta. TBH No creo que la interfaz con la clase Report sea tan mala, aunque podría estar en orden un nombre mejor. Dejé fuera las declaraciones de guardia y las afirmaciones por brevedad.

EDITAR: Observe también que la clase es inmutable. Entonces, una vez que se crea, no puede cambiar nada. Puede agregar un setFormatter () y un setPrinter () y no meterse en demasiados problemas. La clave, en mi humilde opinión, es no cambiar los datos en bruto después de la creación de instancias.

public class Report
{
    private ReportData data;
    private ReportDataDao dao;
    private ReportFormatter formatter;
    private ReportPrinter printer;


    /*
     *  Parameterized constructor for depndency injection, 
     *  there are better ways but this is explicit.
     */
    public Report(ReportDataDao dao, 
        ReportFormatter formatter, ReportPrinter printer)
    {
        super();
        this.dao = dao;
        this.formatter = formatter;
        this.printer = printer;
    }

    /*
     * Delegates to the injected printer.
     */
    public void printReport()
    {
        printer.print(formatReport());
    }


    /*
     * Lazy loading of data, delegates to the dao 
     * for the meat of the call.
     */
    public ReportData getReportData()
    {
        if (reportData == null)
        {
            reportData = dao.loadData();
        }
        return reportData;
    }

    /*
     * Delegate to the formatter for formatting 
     * (notice a pattern here).
     */
    public ReportData formatReport()
    {
        formatter.format(getReportData());
    }
}
Heath Lilley
fuente
Gracias por la implementación. Tengo 2 cosas, en la línea if (reportData == null)supongo que quieres decir en su datalugar. En segundo lugar, esperaba saber cómo llegaste a esta implementación. Por ejemplo, ¿por qué decidió delegar todas las llamadas a otros objetos? Una cosa más de la que siempre me he preguntado, ¿es realmente responsabilidad de un informe imprimirse? ¿Por qué no creaste una printerclase separada que toma un reporten su constructor?
Songo
Sí, reportData = data, perdón por eso. La delegación permite un control detallado de las dependencias. En tiempo de ejecución, puede proporcionar implementaciones alternativas para cada componente. Ahora puede tener una HtmlPrinter, PdfPrinter, JsonPrinter, ... etc. Esto también es útil para las pruebas, ya que puede probar sus componentes delegados de forma aislada e integrada en el objeto anterior. Ciertamente, podría invertir la relación entre la impresora y el informe, solo quería mostrar que era posible proporcionar una solución con la interfaz de clase proporcionada. Es un hábito trabajar en sistemas heredados. :)
Heath Lilley
hmmmm ... Entonces, si estuviera construyendo el sistema desde cero, ¿qué opción tomaría? ¿Una Printerclase que toma un informe o una Reportclase que toma una impresora? He encontrado un problema similar antes de tener que analizar un informe y discutí con mi TL si deberíamos crear un analizador que tome un informe o si el informe debería tener un analizador dentro y la parse()llamada se delega en él.
Songo
Haría ambas cosas ... printer.print (report) para comenzar y report.print () si fuera necesario más adelante. Lo mejor del enfoque de printer.print (informe) es que es altamente reutilizable. Separa la responsabilidad y le permite tener métodos convenientes donde los necesite. Tal vez no desee que otros objetos en su sistema tengan que saber sobre ReportPrinter, por lo que al tener un método print () en una clase, está logrando un nivel de abstación que aísla su lógica de impresión de informes del mundo exterior. Esto todavía tiene un vector de cambio estrecho y es fácil de usar.
Heath Lilley
0

En su ejemplo, no está claro si se está violando SRP. Tal vez el informe debería poder formatearse e imprimirse, si son relativamente simples:

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

Los métodos son tan simples que no tiene sentido tener ReportFormattero ReportPrinterclases. El único problema deslumbrante en la interfaz es getReportDataporque viola la pregunta no decir en un objeto sin valor.

Por otro lado, si los métodos son muy complicados o hay muchas formas de formatear o imprimir, Reportentonces tiene sentido delegar la responsabilidad (también más comprobable):

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

SRP es un principio de diseño, no un concepto filosófico, por lo que se basa en el código real con el que está trabajando. Semánticamente, puede dividir o agrupar una clase en tantas responsabilidades como desee. Sin embargo, como principio práctico, SRP debería ayudarlo a encontrar el código que necesita modificar . Las señales de que está violando SRP son:

  • Las clases son tan grandes que pierdes el tiempo desplazándote o buscando el método correcto.
  • Las clases son tan pequeñas y numerosas que pierdes el tiempo saltando entre ellas o buscando la correcta.
  • Cuando tiene que hacer un cambio, afecta tantas clases que es difícil hacer un seguimiento.
  • Cuando tiene que hacer un cambio, no está claro qué clases necesitan cambiar.

Puede solucionarlos mediante la refactorización mejorando los nombres, agrupando código similar, eliminando duplicaciones, utilizando un diseño en capas y dividiendo / combinando clases según sea necesario. La mejor manera de aprender SRP es sumergirse en una base de código y refactorizar el dolor.

Garrett Hall
fuente
¿podría verificar el ejemplo que adjunté a la publicación y elaborar su respuesta en función de ello?
Songo
Actualizado. SRP depende del contexto, si publicaste una clase completa (en una pregunta separada) sería más fácil de explicar.
Garrett Hall
Gracias por la actualización. Sin embargo, una pregunta: ¿es realmente responsabilidad de un informe imprimirse? ¿Por qué no creaste una clase de impresora separada que toma un informe en su constructor?
Songo
Solo digo que SRP depende del código en sí mismo, no debes aplicarlo dogmáticamente.
Garrett Hall
Sí, entiendo tu punto. Pero si estuviera construyendo el sistema desde cero, ¿qué opción tomaría? ¿Una Printerclase que toma un informe o una Reportclase que toma una impresora? Muchas veces me enfrento a una pregunta de diseño antes de determinar si el código resultará complejo o no.
Songo
0

El principio de responsabilidad única está muy unido a la noción de cohesión . Para tener una clase altamente coherente, debe tener una codependencia entre las variables de instancia de la clase y sus métodos; es decir, cada uno de los métodos debe manipular tantas variables de instancia como sea posible. Cuantas más variables use un método, más cohesivo será para su clase; La máxima cohesión suele ser inalcanzable.

Además, para aplicar SRP bien, usted comprende bien el dominio de la lógica de negocios; saber qué debe hacer cada abstracción. La arquitectura en capas también está relacionada con SRP, al hacer que cada capa haga una cosa específica (la capa de origen de datos debe proporcionar datos, etc.).

Volviendo a la cohesión, incluso si sus métodos no usan todas las variables, deberían estar acoplados:

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public Type1 method3() {
        //use var2 and var3
    }
}

No debería tener algo como el siguiente código, donde una parte de las variables de instancia se usan en una parte de los métodos, y la otra parte de las variables se usan en la otra parte de los métodos (aquí debe tener dos clases para cada parte de las variables).

public class MyClass {
    private Type1 var1;
    private Type2 var2;
    private Type3 var3;
    private TypeA varA;
    private TypeB varB;

    public Type3 method1() {
        //use var1 and var3
    }  

    public void method2() {
        //use var1 and var2
    }

    public TypeA methodA() {
        //use varA and varB
    }

    public TypeA methodB() {
        //use varA
    }
}
m3th0dman
fuente