jueves, 24 de octubre de 2013

The Single Responsibility Principle (Principio de responsabilidad única)

Single Responsibility Principle (SRP)

"Una clase sólo debe tener una responsabilidad. O lo que es lo mismo, una clase debe tener una y sólo una razón para cambiar". (Robert C. Martin)

Este post pertenece a la serie Principios SOLID.

Realmente este parece un principio simple, sin embargo no hay que dejarse engañar por las apariencias. A lo largo de este post veremos es necesario conocer en profundidad todas las repercusiones de este principio para poder aplicarlo correctamente.


Debemos considerar una responsabilidad como una razón para cambiar. En esencia este principio establece que si una clase tiene dos responsabilidades deberemos dividir la clase en dos y cada una de ellas gestionará una sola responsabilidad. De esta manera si en el futuro cambia un requerimiento y esto se traduce en un cambio en una responsabilidad deberemos hacer el cambio solamente en la clase que maneja esa responsabilidad.

¿Pero, por que es importante separar responsabilidades en clases separadas?
Si tenemos una clase con dos responsabilidades que no cumple el SRP y debemos hacer cambios en una de sus responsabilidades corremos el riesgo de que el cambio afecte también al funcionamiento de la otra responsabilidad.

Veamos un ejemplo:


En el diagrama la clase Rectangle tiene dos métodos. Uno muestra un rectángulo en la pantalla y otro calcula el área del rectángulo. Hay un módulo de cálculo de geometrías que usa la clase Rectangle para realizar el cálculo de áreas y nunca dibuja la figura en pantalla. Por otro lado una aplicación de diseño gráfico que usa la clase Rectangle fundamentalmente para dibujar la figura en la pantalla.

Este diseño rompe el SRP. La clase Rectangle tiene dos responsabilidades. Una realizar el cálculo del área del rectángulo y otra dibujar la figura en pantalla.

La violación del SRP presenta en este caso diversos problemas. Primero necesitamos incluir el GUI en el módulo de cálculo de geometrías por la dependencia que Rectangle tiene sobre GUI. Segundo si un cambio en los requisitos de la aplicación de diseño gráfico provoca un cambio en la clase Rectangle, nos obligaría a recompilar y distribuir de nuevo el módulo de cálculo de geometrías.

Un mejor diseño sería separar las dos responsabilidades de la clase Rectangle en dos clases distintas, como se muestra a continuación:


La responsabilidad de cálculo del área ahora se ha traspasado a la clase GeometricRectangle. Ahora los cambios que se realicen en la responsabilidad de dibujar el rectángulo no afectará en modo alguno al cálculo del área.

Una interpretación más amplia del SRP

En su articulo "Taking the Single Responsibility Principle Seriously" Ralf Westphal redefine y amplia el alcance el SRP. Para Ralf Westphal la definición de Robert C. Martin se circunscribe a las clases, pero el SRP es un principio de deben aplicarse a otro tipo de estructuras de código como métodos o estructuras superiores como módulos o incluso aplicaciones.

Ralf Westphal sugiere esta definición para el SRP:
"Una unidad funcional en un determinado nivel de abstracción sólo debe ser responsable de un solo aspecto de los requisitos de un sistema. Un aspecto de los requisitos es un rasgo o característica de requisitos, que puede cambiar de forma independiente de otros aspectos".

Esto necesita ser revisado con un poco más de detenimiento. Aunque la definición va en línea con la de Robert C. Martin, reconoce responsabilidades a varios niveles por lo que no se centra en la estructura de la clase sino que habla de unidad funcional. Una aplicación tiene una responsabilidad (de grano grueso, claro) y un evento también tiene una responsabilidad (de grano muy fino).

Al no estar esta definición centrada en el código, se pueden relacionar el SRP con los requisitos de una unidad funcional incluso aunque aún no exista código. Por lo tanto podemos analizar los diferentes aspectos que se derivan de los requisitos funcionales y extraer cada uno de esos aspectos en unidades funcionales distintas. Este proceso se irá repitiendo con una granularidad cada vez más fina guiando así el diseño de software.

Las responsabilidades no deben solaparse al mismo nivel de abstracción. Dos métodos de la misma clase deben ser responsables de diferentes aspectos, pero ambos métodos deben estar preocupados de la misma responsabilidad de nivel superior, por ejemplo la persistencia en un repositorio.

Aplicar esta perspectiva del SRP requiere de una sólida comprensión de que aspectos encontramos en los requisitos. Esto es fácil para los aspectos más típicos como los relacionados con la tecnología (acceso a una base de datos, leer/escribir en el sistema de archivos, conectar a servicios web, etc.). Pero hay más aspectos de los que parece a primera vista. Hay aspectos sutiles que se suelen pasar por alto y que nos llevan a escribir un código que no cumple completamente con el SRP y hace que mantenerlo y evolucionarlo sea más difícil de lo que nos gustaría.

Aspectos típicos.
Como ya he mencionado, los aspectos típicos son fáciles de detectar y suelen estar relacionados con la tecnología. En general, cualquier interacción de nuestro software con el entorno es un aspecto y puede cambiar de forma independiente. Pueden ser interacciones del entorno con nuestro software (un usuario interactua con nuestro software y puede hacerlo a través de una consola, una página web, un cliente de escritorio etc.); como de nuestro software con el entorno (al arrancar el software lee un archivo xml para establecer la configuración, accede a una base de datos, interactua  con un componente de GIS para posicionar objetos en un mapa, etc.). Todos los anteriores son aspectos y debemos crear para cada uno de ellos una unidad funcional que es responsable de gestionar ese aspecto.

Aspectos sutiles.
Es relativamente fácil detectar aspectos funcionales y no funcionales. Aislarlos en unidades funcionales con una única responsabilidad es cuestión de disciplina. Esta es la base para tener aplicaciones, módulos y clases limpias.
Pero, ¿y los métodos? ¿cómo aplicamos el SRP en los métodos? Al fin y al cabo es en los métodos donde se escribe el "payload" del código.
Vamos a ver una serie de aspectos que a menudo encontramos mezclados en los métodos. Estos son aspectos sutiles. No son fáciles de ver para todo el mundo porque en la mayoría de los casos esas mezclas de aspectos están arraigadas tanto en el pensamiento común como en las herramientas de desarrollo.

Es muy fácil caer en la mezcla de un aspecto funcional con otros no funcionales. Este es un ejemplo de Robert C. Martin

public interface IModem
{
  void Dial(String number);
  void Hangup();
  void Send(char c);
  char Recv();
}
Esto es muy común. A un alto nivel de abstracción esta interfaz describe un aspecto de comunicación. ¿Por qué no reunir todo lo necesario para la comunicación en una misma interfaz?.
Tras una inspección más detallada debemos entender que en una comunicación por modem existen dos aspectos distintos y que pueden cambiar de forma independiente; por un lado establecer la comunicación y por otro enviar datos. La forma en que se termina una comunicación puede cambiar de forma independiente de como se envían y reciben datos.

Efectuaremos un pequeño cambio a ver que pasa. Consideraremos a un modem como un recurso y haremos que implemente la interfaz IDisposable para facilitar su liberación. Así podríamos pasar de este código para usar el modem
IModem m = new IsdnModem();
m.Dial(…);
m.Send(…);
m.Hangup();

A este otro
using(IModem m = new IsdnModem())
{
  m.Dial(…);
  m.Send(…);
}

La interfaz ahora quedaría de esta forma:
public interface IModem : IDisposable
{
  void Dial(String number);
  void Send(char c);
  char Recv();
}
Este es un cambio simple que se centra en la forma de liberar la conexión, sin embargo afecta a código que nada tiene que ver con el manejo de la conexión del modem.

Ahora vamos a refactorizar este código para alinearlo con el SRP. Como ya hemos visto el manejo de la conexión del modem es un aspecto relacionado con la infraestructura, así que vamos a extraerlo a su propia interfaz. Ahora tendremos una interfaz para cada uno de los aspectos que hemos detectado, el manejo del la conexión y el envio/recepción de datos.
public interface IModemConnection : IDisposable
{
  void Dial(String number);
}
 
public interface IModemDataExchange
{
  void Send(char c);
  char Recv();
}

Bien, ya tenemos nuestros dos aspectos separados en diferentes unidades funcionales. Para la comunicación por modem el manejo de la conexión es un aspecto no funcional y el envío y recepción de datos es el aspecto funcional, ya que el propósito principal de la comunicación por modem es enviar y recibir datos. Tener que establecer una comunicación primero es un "mal necesario".
Ahora podríamos implementar estas dos interfaces en una misma clase. Esto probablemente sea lo más fácil en la practica. Sin embargo, si queremos hacer una aplicación más estricta del SRP creando implementaciones independientes para cada aspecto debemos hacer aún un pequeño cambio.
public interface IModemConnection : IDisposable
{
  IModemDataExchange Dial(String number);
}
 
public interface IModemDataExchange
{
  void Send(char c);
  char Recv();
}

La implementación de IModemConnection se convierte en una factoria para la implementación de IModemDataExchange. Esto parece un buen equilibrio. Consigue la separación de los dos aspectos de la comunicación por modem mientras que queda claro que ambos aspectos forman parte del mismo aspecto superior (de grano más grueso), la comunición por modem. Por otro lado tambien asegura que no se pueda usar la implemetación de IModemDataExchange antes de establecer la conexión (método Dial).

Otro ejemplo típico de mezcla de aspectos funcionales y no funcionales:
void StoreCustomer(Customer c)
{
  trace.Write("Storing customer…");
  using(db.Connect(…))
  {
    var tx = db.OpenTransaction();
    try
    {
      db.ExecuteSql(…); // Store name and address
      db.ExecuteSql(…); // Store contact data
      tx.Commit();
    }
    catch(Exception ex)
    {
      tx.Rollback();
      trace.Write("Failed to store customer");
      log.Log("Storing customer failed; exception: {0}", ex);
      throw new ApplicationException(…);
    }
  }
}

Este es un código bastante típico. Pero, ¿cuántos aspectos distintos mezcla este método?.
La responsabilidad obvia de este método es guardar los datos de un cliente, sin embargo las líneas que realmente hacen este trabajo están enterradas entre muchas líneas que manejan aspectos no funcionales. Además de guardar los datos de un cliente este método es también responsable de escribir trazas, abrir la conexión a la base de datos, manejar una transacción, manejar excepciones y escribir en el log.

Esta mezcla de aspectos funcionales y no funcionales hace que el mantenimiento de este método sea más difícil, pero además de "meter ruido" hace que intuir el propósito del método sea complicado.

La programación orientada a aspectos (AOP) trata de resolver este tipo de problema. Existen innumerable frameworks de AOP como PostSharp o AspectJ que ofrecen diferentes maneras de separar los aspectos no funcionales de los funcionales. Me dejo apuntado para el futuro escribir una serie sobre AOP.

 Encontrar y separar las responsabilidades unas de otras es gran parte de lo que el diseño de software es.

Como hemos visto el SRP es uno de los principios de diseño más simples en cuanto a concepto y a la vez dificil de aplicar correctamente. Encontrar y separar las responsabilidades unas de otras es gran parte del trabajo que debemos realizar en el diseño de software. De hecho en el resto de principios de diseño que iremos viendo volveremos a este concepto de una forma o de otra.


No hay comentarios:

Publicar un comentario en la entrada