¿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
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í .
Respuestas:
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
Report
en 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
Report
clase.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.
formatReport
es 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 queprintReport
. 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 comoIPrintable<T>
. Tiene un método clavePrint(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
IncomeStatementPrinter
clase específica , puede usarIPrintable<T>
y, por lo tanto, operar en cualquier tipo de informe imprimible, que le brinda todos los beneficios percibidos de unaReport
clase base con unprint
mé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
IncomeStatement
se 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 enIncomeStatement
(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
IncomeStatement
clase va a tener una buena cantidad de comportamiento a ella en la forma degetTotalRevenues()
,getTotalExpenses()
ygetNetIncome()
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
format
yprint
mé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 unprint(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.
fuente
IncomeStatement
. ¿Su diseño propuesto que la mediaIncomeStatement
tendrá instancias deIncomeStatementPrinter
YIncomeStatementRenderer
de manera que cuando llamoprint()
enIncomeStatement
que va a delegar la llamada a laIncomeStatementPrinter
vez?IncomeStatement
clase no tiene unprint
método, o unformat
mé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 laIPrintable<IncomeStatement>
interfaz que está registrada en el contenedor.Printer
instancia en laIncomeStatement
clase? La forma en que me imagino que es cuando lo llamoIncomeStatement.print()
lo delegaráIncomeStatementPrinter.print(this, format)
. ¿Qué hay de malo en este enfoque? ... Otra pregunta, mencionó queIncomeStatement
deberí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 llamadaIncomeStatement
?IncomeStatementPrinter
dependiendoIncomeStatement
yIncomeStatement
dependiendo deIncomeStatementPrinter
. Esa es una dependencia cíclica. Y es simplemente un mal diseño; no hay ninguna razónIncomeStatement
para saber algo sobre unaPrinter
oIncomeStatementPrinter
: 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 unaIncomeStatementPrinter
. No hay una buena razón para tener alguna noción de impresión en el modelo de dominio.IncomeStatement
desde 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.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.
fuente
Aquí hay una cita de la regla 8 de Object Calisthenics :
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.
fuente
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.
fuente
if (reportData == null)
supongo que quieres decir en sudata
lugar. 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 unaprinter
clase separada que toma unreport
en su constructor?Printer
clase que toma un informe o unaReport
clase 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 laparse()
llamada se delega en él.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:
Los métodos son tan simples que no tiene sentido tener
ReportFormatter
oReportPrinter
clases. El único problema deslumbrante en la interfaz esgetReportData
porque 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,
Report
entonces tiene sentido delegar la responsabilidad (también más comprobable):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:
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.
fuente
Printer
clase que toma un informe o unaReport
clase 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.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:
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).
fuente