18 de junio de 2010

Principio de responsabilidad única - SOLID

Los principios SOLID son una serie de herramientas o técnicas introducidas por Robert C. Martin a principios de 2000, los cuales aplicados en conjunto deberían llevar a obtener diseños que sean mas mantenibles, flexibles y extensibles.

SOLID es un acrónimo formado por la primer letra del nombre de cada uno de los cinco principios, los cuales son:

SRP, alias Single responsibility principle, alias Principio de responsabilidad única

OCP, alias Open/closed principle, alias Principio de abierto/cerrado

LSP, alias Liskov substitution principle, alias Principio de substitución de Liskov

ISP, alias Interface segregation principle, alias Principio de separación de interfaces

DIP, alias Dependency inversion principle, alias Principio de inversión de dependencias

Para obtener el mayor beneficio de estos principios, los mismos se deberían aplicar en conjunto, ya que unos refuerzan a otros (algo así como las prácticas de extreme programming, las cuales también se refuerzan entre sí)

Como por algún lado hay que empezar, en este primer post vamos a tratar el principio de responsabilidad única (SRP)

La definición del principio de responsabilidad única dice lo siguiente:

Nunca debería haber más de un motivo por el cual una clase deba cambiar

Que significa esto? si pensamos en el concepto de responsabilidad como tareas que se le asignan a una clase, imaginemos que tenemos una clase con mas de una tarea por realizar, es más, imaginemos que tiene muchas tareas que realizar, por ejemplo pensemos en una clase Factura donde cada método de la misma es una tarea que la Factura realiza:



NOTA: claro que a esta clase Factura le falta información básica como fecha, número, etc... pero para este ejemplo vamos a obviar esta información

Ahora pensemos qué pasa si queremos cambiar la forma en que se imprime la Factura, esto implica cambiar mi clase Factura, que pasaría si quiero cambiar la forma en que se calculan los descuentos o los impuestos, también esto implica cambiar mi clase factura, y así podríamos seguir...

Qué nos dice esto? que la clase Factura tiene muchos motivos por los cuales podría cambiar. Veamos lo que se dice en el libro "Head First Design Patterns" relacionado con el tema del cambio.

No importa donde estés trabajando, qué estés construyendo o qué lenguaje de programación estés usando, cual es la única y verdadera constante que siempre va a estar con nosotros? El CAMBIO, es decir, no importa qué tan bien diseñes tu aplicación, a medida que pase el tiempo una aplicación deberá crecer y cambiar o inevitablemente morirá.

Parece que es importante que una aplicación esté diseñada para afrontar el cambio...

Imaginemos que GenerarReporte, en la versión inicial de nuestra aplicación, lo único que hacía era generar en formato de texto plano la información contenida dentro de la factura, pero a medida que pasa el tiempo, por requerimientos de nuestro cliente, se decide realizar un cambio y que el reporte sea mas elaborado y además se genere en formato PDF. Esto significaría referenciar desde nuestra clase Factura a librerías de generación de PDF, lo cual hace que nuestra clase se termine acoplando a una librería que sólo es necesaria para generar un reporte y no para calcular descuentos, subtotales, etc...

Si nuestra clase factura la estuviéramos utilizando desde una aplicación que realiza procesamientos en forma desatendida, esto implicaría que nuestra aplicación debería incluir librerías para generar PDF, aún cuando nunca fuera necesario utilizarlas (pensemos que ocurriría si además tuviéramos que pagar una licencia adicional para instalar esta librería en la máquina que corre procesos batch ;)

De haber aplicado SRP en nuestro diseño, quizás este problema se hubiera evitado? Veamos si esto se cumple

Por ahora apliquemos SRP sólo para solucionar este problema particular, mas adelante rediseñaremos otras partes de la clase Factura





De esta manera, la clase Factura puede ser utilizada en forma desacoplada del generador de reportes y, por lo tanto, de la librería de generación de PDF.


public class FacturaPrinter
{
public void ImprimirFactura(Factura factura)
{
string ruta = getRutaGeneracion();

var generadorReporteFactura = new GeneradorReporteFactura();
generadorReporteFactura.GenerarReporte(ruta, factura);

imprimir(ruta);
}

private string getRutaGeneracion()
{
//obtengo la ruta donde se debe generar la factura
}

private void imprimir(string rutaReporte)
{
//imprimo el reporte generado
}
}


Esto fue logrado ya que le hemos quitado a la clase Factura la responsabilidad de saber imprimirse y hemos delegado esa responsabilidad en una nueva clase GeneradorReporteFactura.

SRP nos salvó de tener que pagar licencias adicionales de la librería para generación de PDF!!

Veamos que SRP no solo minimiza el impacto ante los cambios, sino que es útil en otros aspectos.

Testeo unitario

Imaginemos que el método CalcularDescuentos está programado de tal manera que llama internamente al método CalcularSubtotal, y en base a ese valor determina los descuentos a aplicar (por ej, por montos superiores a $1000 se realiza un descuento del 5%). Luego, en forma adicional, también se calculan otros descuentos en base a otras condiciones, como por ejemplo si el cliente es preferencial, si la forma de pago es en efectivo, etc...

Para poder hacer un testeo unitario que sólo verifique el algoritmo de CalcularDescuentos, necesito armar una instancia de factura que contenga lineas con datos como la cantidad, el precio unitario, etc... de forma tal que cuando se llame a CalcularSubtotal, el método me dé un valor X, para luego poder realizar el cálculo de los descuentos. Qué ocurre si además el constructor de líneas de factura necesita que le pasemos una instancia de un Producto, el cual a su vez es complejo de generar? Digamos que cada vez se hace mas complejo realizar un testeo unitario del método CalcularDescuentos...

Apliquemos SRP para intentar solucionar este problema



En este caso, a diferencia del caso anterior, el calculador de descuentos es una dependencia de la factura, de esta manera Factura.CalcularDescuentos delega el cálculo a la clase CalculadorDescuentosFactura.


public class Factura
{
public Factura(CalculadorDescuentosFactura calculadorDescuentosFactura)
{
_calculadorDescuentosFactura = calculadorDescuentosFactura;
}

private CalculadorDescuentosFactura _calculadorDescuentosFactura;

public void CalcularDescuentos()
{
Descuentos = _calculadorDescuentosFactura.CalcularDescuentos(this);
}

// ....
}


Luego, a la hora de hacer un testeo unitario del método CalculadorDescuentosFactura.CalcularDescuentos, me alcanza con pasar como parámetro un mock de la clase Factura (un objeto factura falso) configurado de forma tal, que devuelva un valor X definido por mi cuando el método CalcularSubtotal sea llamado.
NOTA: cabe aclarar que para poder realizar esto, Factura debería ser una abstracción (una interfaz, por ejemplo) y no una clase concreta. De esta forma se podría substituir por un mock de la misma. De esto vamos a hablar cuando tratemos los otros principios SOLID

En este caso SRP nos ayudó a tener un código que es mas fácil de testear.

Claridad del código

Pensemos que existe la remota posibilidad de que alguien necesite modificar la forma en que se calculan los impuestos de una factura y esa persona no es quien originalmente programó la clase... o efectivamente es quien la programó pero lo hizo hace tiempo y ya no se acuerda cómo estaba diseñada la clase. (suele pasar, no? ;)

Esta persona deberá recorrer toda la clase factura, tratando de ubicar en qué parte de la misma está la lógica de cálculo de impuestos. En el ejemplo esta tarea parece bastante fácil, ya que hay un método CalcularImpuestos, pero pensemos qué pasaría si el cálculo de impuestos es más complejo y no fuera tan fácil de identificar esta lógica.

Aplicando SRP podríamos aislar esta lógica en una clase separada, y de esta forma sería mucho mas fácil de entender y seguir el diseño. Este caso es similar al del calculador de descuentos, así que aplicaremos la misma solución.




public class Factura
{
public Factura(CalculadorDescuentosFactura calculadorDescuentosFactura,
CalculadorImpuestosFactura calculadorImpuestosFactura)
{
_calculadorImpuestosFactura = calculadorImpuestosFactura;
_calculadorDescuentosFactura = calculadorDescuentosFactura;
}

private CalculadorDescuentosFactura _calculadorDescuentosFactura;
private CalculadorImpuestosFactura _calculadorImpuestosFactura;

public void CalcularImpuestos()
{
Impuestos = _calculadorImpuestosFactura.CalcularImpuestos(this);
}

// ....
}


SRP es un principio que nos permite tener diseños que respondan bien ante cambios, fáciles de testear y claros, asi que la pregunta que podríamos plantear ahora es, debo aplicar este principio SIEMPRE?

Si llevamos SRP al extremo, terminaríamos con clases que sólo tengan un único método, lo cual tampoco es recomendable, ya que estaríamos incrementando la complejidad de nuestro diseño (pensemos que se incrementa altamente la cantidad de dependencias entre clases y esto no es algo "gratis"). Es por esto que, como todo principio SOLID, siempre es importante tenerlo presente para identificar posibles falencias que pueda tener nuestro diseño, y luego decidir en cada caso puntual si es conveniente aplicarlo o no. (si estamos aprendiendo, ante la duda quizás lo recomendable sea aplicar el principio, luego la experiencia nos dirá...)

Algunos "olores" que nos pueden indicar que conviene aplicar SRP


  • Tengo clases muy extensas que hacen de todo
  • Cuando toco código asociado a una funcionalidad A, se termina rompiendo la funcionalidad B
  • Cuando toco código asociado a una funcionalidad A, tengo que testear las funcionalidades A, B y C porque no sé qué se podría romper
  • Tengo clases con nombres como FacturaManager, FacturaHandler, FacturaAdmin, etc... (si tienen esos nombres muy probablemente es porque no quedaba clara cual era su responsabilidad a la hora de nombrarlas)


Por último dejo algunos links a material de referencia sobre el principio SRP y principios SOLID en general, en el próximo post vamos a estar tratando el principio abierto/cerrado (OCP)

Grupo de estudio sobre principios SOLID del cual participo en la comunidad Alt.net hispano, con la coordinación de Fernando Claverino

SOLID - Wikipedia

Libro Head first object-oriented analysis and design (en el capítulo 8 se tratan varios principios de diseño, incluyendo SRP)

Artículos de Robert C. Martin sobre principios SOLID

Libro Agile principles, patterns, and practices in C# de Robert C. Martin

Espero que sea de utilidad, cualquier comentario será bienvenido.

5 comentarios:

  1. Muy bien explicado, cuando continuará con las otras entregas?

    ResponderEliminar
  2. La verdad que no he tenido mucho tiempo para dedicarle al blog, pero pronto espero continuar con el resto de los principios SOLID. Muchas gracias por tu comentario!

    ResponderEliminar
  3. Excelente blog, siguiendo tu ejemplo, cada método debería ser una clase independiente, ya todos ellos pueden cambiar. CalcularTotal, CalcularSubtotal, hasta el mismo eliminarlinea podria tener una logica asociada factible de ser alterada. Como manejas esta situación en una aplicación real?

    ResponderEliminar
    Respuestas
    1. Este comentario ha sido eliminado por el autor.

      Eliminar
    2. Hola MID, ante todo, gracias por el comentario. Yo no diría que cada método "debería" ser una clase independiente, sinó mas bién que "podría" ser una clase independiente (si realmente te trae mas beneficios que problemas). Es importante siempre tener en cuenta que tanto este principio como el resto de los principios SOLID (y en general cualquier buena práctica de OOA/D) se deberían aplicar a consciencia y analizando el caso puntual que tenemos entre manos. Saludos!

      Eliminar