[SRP] Principio de responsabilidad única

Descripción general

Principios del diseño orientado a objetos; Principios SOLID; [SRP] Principio de responsabilidad única
"Cada objeto en el sistema deben tener una simple responsabilidad, y todos los servicios de los objetos deben cumplir con esa simple responsabilidad"

[TOC] Tabla de Contenidos


↑↑↑

[SRP] Principio de responsabilidad única


↑↑↑

Los cinco principios SOLID


↑↑↑

[SRP] Principio de responsabilidad única

El Principio de responsabilidad única (Single Responsability Principle - SRP) fue acuñado por Robert C. Martin en un artículo del mismo título y popularizado a través de su conocido libro [patrones Gof]. SRP tiene que ver con el nivel de acoplamiento entre módulos dentro de la ingeniería del software.

Este principio nos viene a decir que una clase sólo debería tener una única razón para cambiar.

"Una clase debe tener una única razón para cambiar."

En términos prácticos, este principio establece que:

Lo que trata de decirnos este principio es que debemos huir de aquellas clases monolíticas que aglutinen varias responsabilidades. Pero, ¿qué es una responsabilidad? Desde el punto de vista de SRP se podría decir que una responsabilidad en una clase es una razón para cambiar esa clase. Es decir, si encontramos que hay más de una razón por la que una clase pueda cambiar entonces es que esa clase tiene más de una responsabilidad.

¿Y no sería más sencillo decir que una clase debería tener una sola razón para existir en lugar de para cambiar? Cuidado, porque esto nos podría llevar a hacer muy malos diseños de sistemas. Llevado al pie de la letra podría encontrarme con cientos de clases en mi sistema, cada una con una única función. Lo que haría al sistema nada fácil de mantener.

El punto clave que nos dice las razones por la que una clase puede cambiar va a depender del contexto en el que se va a dar uso a esa clase. Pongamos por ejemplo una clase que represente al motor de un coche. ¿Necesitamos conocer el régimen de revoluciones del motor?, ¿el peso?, ¿número de cilindros?, ¿presión del inyector de gasolina?, ¿o lo que nos interesa es simplemente poder arrancarlo y esperar que haga andar a un coche para llevaros de un sitio a otro? La respuesta a estas preguntas va a depender del contexto en el cual usemos la clase motor. No va a tener las mismas necesidades sobre esta clase un fabricante de coches que un usuario que usa el coche para ir de un sitio a otro. El fabricante de coches va a notar un número mayor de responsabilidades en el motor que el usuario del coche. Por tanto, para el fabricante, este principio recomendaría dividir la clase motor en otras más pequeñas que cumplan con las especificaciones de manera individual.


↑↑↑

Veamos un ejemplo típico de violación del SRP:

Si una clase tiene dos responsabilidades, entonces asume dos motivos por los cuales puede ser modificada. Por ejemplo, supongamos una clase llamada Factura, la cual dentro de un contexto determinado ofrece un método para calcular el importe total, tal y como muestra la siguiente figura.

Imagen 01

Figura 01


↑↑↑

Detectando responsabilidades

La piedra angular de este principio es la identificación de la responsabilidad real de la clase. Según SRP, una responsabilidad es "un motivo de cambio"; algo que en ocasiones es difícil de ver, ya que estamos acostumbrados a pensar un conjunto de operaciones como una sola responsabilidad.

Listado 01

class Factura { public string Codigo { get; set; } public DateTime FechaEmision { get; set; } public decimal ImporteFactura { get; set; } public decimal ImporteIVA { get; set; } public decimal ImporteDeduccion { get; set; } public decimal ImporteTotal { get; set; } public decimal PorcentajeDeduccion { get; set; } // Método que calcula el total de la factura public void CalcularTotal() { // Calculamos la deducción ImporteDeduccion = (ImporteFactura * PorcentajeDeduccion) / 100; // Calculamos el IVA ImporteIVA = ImporteFactura * 0.16m; // Calculamos el total ImporteTotal = (ImporteFactura - ImporteDeduccion) + ImporteIVA; } }

Si implementamos la clase Factura tal y como se muestra en el listado 1, podríamos decir que la responsabilidad de esta clase es la de calcular el total de la factura y que, efectivamente, la clase cumple con su cometido. Sin embargo, no es cierto que la clase contenga una única responsabilidad. Si nos fijamos detenidamente en la implementación del método CalcularTotal, podremos ver que, además de calcular el importe base de la factura, se está aplicando sobre el importe a facturar un descuento o deducción y un 16% de IVA. El problema está en que si en el futuro tuviéramos que modificar la tasa de IVA, o bien tuviéramos que aplicar una deducción en base a una tarifa por cliente, tendríamos que modificar la clase Factura por cada una de dichas razones; por lo tanto, con el diseño actual las responsabilidades quedan acopladas entre sí, y la clase violaría el principio SRP.


↑↑↑

Separando responsabilidades

El primer paso para solucionar este problema es separar las responsabilidades; para separarlas, primero hay que identificarlas. Enumeremos de nuevo los pasos que realiza el método CalcularTotal:

En este método se identifican tres responsabilidades. Recuerde que una responsabilidad no es una acción, sino un motivo de cambio, y por lo tanto se deberían extraer las responsabilidades de deducción e impuestos en dos clases específicas para ambas operaciones; estableciendo por un lado la clase IVA y por otro la clase Deduccion, tal y como se presenta en el listado 2.

Listado 2

class IvaNormal { private const decimal PORCENTAJE_IVA_NORMAL = 0.16m; public readonly decimal PorcentajeIvaNormal { get { return PORCENTAJE_IVA_NORMAL; } } public decimal CalcularIVA(decimal importe) { return importe * PORCENTAJE_IVA_NORMAL; } } class Deduccion { private decimal m_PorcentajeDeduccion; public Deduccion(decimal porcentaje) { m_PorcentajeDeduccion = porcentaje; } public decimal CalcularDeduccion(decimal importe) { return (importe * m_PorcentajeDeduccion) / 100; } }

Ambas clases contienen datos y un método y se responsabilizan únicamente en calcular el IVA y la deducción, respectivamente, de un importe. Además, con esta separación logramos una mayor cohesión y un menor acoplamiento, al aumentar la granularidad de la solución. La correcta aplicación del SRP simplifica el código y se traduce en facilidad de mantenimiento, mayores posibilidades de reutilización de código y de crear unidades de testeo específicas orientadas a cada clase/responsabilidad. El listado 3 muestra la nueva versión de la clase Factura, que hace uso de las dos nuevas clases IVA y Deducción.

Listado 3

class FacturaFactorizada { public string Codigo { get; set; } public DateTime FechaEmision { get; set; } public decimal ImporteFactura { get; set; } public decimal ImporteIVA { get; set; } public decimal ImporteDeduccion { get; set; } public decimal ImporteTotal { get; set; } public decimal PorcentajeDeduccion { get; set; } // Método que calcula el total de la factura public void CalcularTotal() { // Calculamos la deducción Deduccion deduccion = new Deduccion(PorcentajeDeduccion); ImporteDeduccion = deduccion.CalcularDeduccion(ImporteFactura); // Calculamos el IVA IvaNormal iva = new IvaNormal(); ImporteIVA = iva.CalcularIVA(ImporteFactura); // Calculamos el total ImporteTotal = (ImporteFactura - ImporteDeduccion) + ImporteIVA; } }

Nota: La correcta aplicación del SRP simplifica el código y se traduce en facilidad de mantenimiento, mayores posibilidades de reutilización de código y de crear unidades de testeo específicas para cada responsabilidad


↑↑↑

Otro Ejemplo

Veamos otro ejemplo típico de violación del SRP: Supongamos que tenemos la clase Employee [Empleado] en un sistema de gestión de una empresa cualquiera. Esta clase nos permite realizar las tareas esperadas sobre un empleado: Cargarlo y almacenarlo en una base de datos, generar la nómina, información básica del empleado, etc.

Imagen 02

Figura 2

Ahora supongamos dos aplicaciones que hacen uso de la clase Employee. Una para ser usada por el departamento de recursos humanos [RRHH_System] para la gestión de las nominas del personal y otra para la gestión de los proyectos que lleva la empresa.

¿Podemos pensar que no se está siguiendo el SRP? Lo que sería lo mismo, ¿creemos que la clase Employee tiene más de una razón por la que pueda cambiar? A mí se me ocurren unas cuantas: Cambiar el formato de almacenamiento de base de datos, modificar los campos que definen a un empleado, cambiar la lógica de generación de nóminas, etc. Es decir, esta clase tiene varias responsabilidades: Es responsable de la persistencia de los clientes, responsable de caracterizar a un empleado, responsable de generar las nóminas, etc.

Las consecuencias de violar el SRP en este caso son dos:

Una mejora en el diseño sería separar las responsabilidades en clases distintas

Imagen 03

Figura 3

Hemos creado dos nuevas clases: una para la gestión de nóminas y otra para el almacenamiento en la base de datos. Hay que fijarse en el detalle de que la clase Employee no depende de las nuevas clases EmployeeAccount y de EmployeeStorage, sino que la dependencia la tienen las aplicaciones.


↑↑↑

Una forma de probar este principio

Una forma para probar este principio es escribir en un papel algo parecido a la siguiente imagen, en la primera línea se escribe el nombre de la clase, en las siguientes líneas escribiremos el nombre de la clase y los métodos de las clase, ocuparemos tantas líneas como métodos tengamos en la clase, luego leeremos cada línea y analizaremos si la frase tiene sentido y si la clase tiene la responsabilidad del método descrito.

Imagen 04

Figure 04

Veamos un ejemplo con la siguiente clase:

Imagen 05

Figura 5

Veamos el resultado de aplicar el análisis en base al documento que se definió.

Imagen 06

Figura 6

Aclarando que el método obtenerAceite() se refiere solo a una lectura de aceite ó con una posibilidad de asignar esta responsabilidad a otra clase.


↑↑↑

Ampliando el abanico de "responsabilidades"

Comentábamos anteriormente que no es fácil detectar las responsabilidades, ya que generalmente tendemos a agruparlas. No obstante, existen escenarios o casuísticas en los que "se permite" una cierta flexibilidad. Robert C. Martin expone un ejemplo utilizando la interfaz Modem:

Listado 4

interface Modem { void dial(int pNumber); void hangup(); void send(char[] data); char[] receive(); }

En este ejemplo se detectan dos responsabilidades, relacionadas con la gestión de la comunicación (dial y hangup) y la comunicación de datos (send y receive). Efectivamente, cada una de las funciones puede cambiar por diferentes motivos; sin embargo, ambas funciones se llamarán desde distintos puntos de la aplicación y no existe una dependencia entre ellas, con lo que no perderíamos la cohesión del sistema.


↑↑↑

Conclusión

Pensemos siempre en el ciclo de vida de una aplicación, y no únicamente en su diseño y desarrollo. Toda aplicación sufre modificaciones a causa de cambios en los requisitos o arreglo de fallos existentes, y el equipo de desarrollo puede variar; si a ello le sumamos que el código es difícil de mantener, los costes de mantenimiento se dispararán, y cualquier modificación se presentará como una causa potencial de errores en entidades relacionadas dentro del sistema.

En definitiva. Este principio es uno de los más simples de SOLID, y sin embargo de los más difíciles de implementar correctamente. Aplicando SRP, podemos alcanzar niveles más bajos de acoplamiento y una cohesión más alta del sistema.


↑↑↑

Referencia Bibliográfica


↑↑↑

A.2.Enlaces

[Para saber mas]
[Grupo de documentos]
[Documento Index]
[Documento Start]
[Imprimir el Documento]