Continuamos con el Principio de Responsabilidad Única, una de las bases de la programación oriendada a objetos.
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.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.
Listado 1
public class Factura
{
public string _codigo;
public DateTime _fechaEmision;
public decimal _importeFactura;
public decimal _importeIVA;
public decimal _importeDeduccion;
public decimal _importeTotal;
public ushort _porcentajeDeduccion;
// 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;
}
}
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 CalcularTotal1:- Aplica una deducción. En base a la base imponible se calcula un descuento porcentual.
- Aplica la tasa de IVA del 16% en base a la base imponible.
- Calcula el total de la factura, teniendo en cuenta el descuento y el impuesto.
Listado 2
public class IVA
{
public readonly decimal _iva = 0.16m;
public decimal CalcularIVA(decimal importe)
{
return importe * _iva;
}
}
public class Deduccion
{
private decimal _deduccion;
public Deduccion(ushort porcentaje)
{
_deduccion = porcentaje;
}
public decimal CalcularDeduccion(decimal importe)
{
return (importe * _deduccion) / 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 Deduccion.
Listado 3
public class Factura
{
public string _codigo;
public DateTime _fechaEmision;
public decimal _importeFactura;
public decimal _importeIVA;
public decimal _importeDeduccion;
public decimal _importeTotal;
public ushort _porcentajeDeduccion;
// 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
IVA iva = new IVA();
_importeIVA = iva.CalcularIVA(_importeFactura);
// Calculamos el total
_importeTotal = (_importeFactura - _importeDeduccion) + _importeIVA;
}
}
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:
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 poco mantenible, 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.José Miguel Torres
MVP de Device Application Development