> Manuales > Principios fundamentales de la Programación Orientada a Objetos

Continuamos con el Principio de Responsabilidad Única, una de las bases de la programación oriendada a objetos.

Continuando con el artículos anterior sobre el principio de resposabilidad única dentro del manual sobre los principios fundamentales de la Programación Orientada a Objetos, pasamos a ver:

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: 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
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;
   }
}

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

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

Manual