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 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 IncomeStatementPrinter
clase 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 Report
clase base con un print
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 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 IncomeStatement
clase 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 format
y print
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 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.