Los principios SOLID han ganado popularidad como buenas practicas efectivas y las formuló Robert Martin. Cuando son aplicados a lo largo de toda una aplicación se conjugan para obtener un sistema modular, fácil de probar y receptivo al cambio.
Por otro lado, estos principios no son nuevos, siempre estuvieron presentes hasta que se los formuló y son una especie de refinamiento de los Principios Universales de Diseño y de las técnicas orientada a objetos.
Se observa que son relativamente fáciles de definir pero la dificultosos de adoptar como practica, ya que se requiere un criterio que se forma mayormente con la experiencia del desarrollador y una muy “buena educación” sobre la base de la orientación a objetos.
S | Single Responsability |
O | Open/Closed |
L | Liskov Substitution principle |
I | Interface Segregation |
D | Dependency Inversion |
Vamos a aclarar que los principios SOLID son una guía sobre cómo proceder, porque el abuso de ellos nos llevará al SOBRE-DISEÑO que es un efecto indeseable.
Single Responsability (Principio de responsabilidad única o acotada)
Este principio nos dice que un modulo de un sistema debe tener de una sola y única responsabilidad , no hacerse cargo de varios aspectos a la vez. Un aspecto único e irrepetible es la razón de su existencia, en otras palabras: si lo aplicamos a un modulo este tendrá una sola razón para cambiar cuando su responsabilidad se vea afectada.
En el fondo vemos que es una reformulación del principio de Alta Cohesión.
Debe haber una sola razón para que una clase cambie. Aquí se entiende que Razón de Cambio es igual a Responsabilidad.
En general se debería considerar dividir responsabilidades en dos o más clases si aquellas tienen conjuntos de métodos con poco en común. Así se aumentará la probabilidad de que un cambio afecte a pocas clases, en lugar de que muchos cambios afecten a una sola clase: varios cambios afectan a distintas clases y no a una sola. Una clase tiene una sola razón para modificarse.
Veamos un ejemplo de cómo este principio es roto en una clase trivial:
public class ErrorLoggerManager
{
public void LogErrorToEventLog(Exception e)
{
// Logging code event system
}
public void LogErrorToFile(Exception e)
{
// Logging code to file
}
}
La implementación de esta clase parece inofensiva pero se nota que tiene las responsabilidades de saber loguear errores al sistema de eventos del SO y también a un archivo. Sin duda son muchas responsabilidades que se acentuarán cuando imaginamos que cambiaría a medida que sumemos otros canales de logueo, tales como enviar mail (agregaríamos otro método?).
Open/Closed principle (abierto a la extensión, cerrado a la modificación)
Este principio nos da una guía sobre cómo escribir entidades de software (clases, módulos o métodos) que se puedan extender sin la necesidad de tocar el código fuente. Una clase debería estar abierta a la extensión y cerrada a la modificación.
No es sencilla la aplicación de este principio, pero se recomienda hacerlo abstrayendo los aspectos de las clases que se consideran pueden cambiar en el tiempo.Estos aspectos mutables se abstraen con interfaces, generics o inyección de código.
Otra formulación interesante es que OCP tienen que ver con que en lugar de agregar más y más responsabilidades y comportamientos a una clase (poniendole más código), por medio de abstracciones reorganicemos el concepto que la clase expresa de forma tal que nuevos comportamientos se adicionen usando la herencia.
Veamos un ejemplo de una clase diseñada según este principio:
public abstract class Shape
{
public abstract void Render();
}
//Cumple con el principio OCP
public class Renderer
{
public void Draw(IList<Shape> shapes)
{
foreach (Shape s in shapes)
s.Render();
}
}
La clase Renderer está cerrada a la modificación, pero puede extenderse su comportamiento mediante la abstracción Shape la cual nos dice que cualquier objeto que la implemente puede ser usado por Renderer sin que esta tenga que modificarse.
Aquí se cumplió que las partes que pueden variar en el tiempo (Shape) se abstrajeron en una interface y la clase que no se puede alterar, Renderer, puede evolucionar su comportamiento con implementaciones de Shape.
Consideraciones finales: se dice que este principio muchas veces es una utopía, porque no se puede predecir todos los aspectos cambiantes de una responsabilidad para ser abstraídas, no se podría predecir todas las futuras extensiones de una clase. Pero se conviene en que si logramos para algunas clases obtener una o dos abstracciones y hacer que esa clase funcione contra esas abstracciones ya tendríamos una excelente aplicación del principio. Recordemos que buscar que todas las clases sean “pluggables” nos llevará al sobre-diseño tan innecesario e indeseable.
Para el entendimiento de este principio me basé mucho en lo publicado por Dino Esposito. Así que gracias!! Un genio. Ver su libro sobre ASP.NET.
Liskov’s Substitution Principle
Fue formulado por una mujer, Bárbara Liskov, y nos dice que un objeto debería fácilmente ser reemplazable por la instancia de un subtipo sin suscitar inconvenientes en el comportamiento y reglas del objeto que lo usa. En otras palabras, si tenemos una Clase A, cuyo subtipo es B, si usamos una instancia de B donde se espera A, al uso debería ser totalmente transparente y no aparecer problemas en el objeto receptor de B.
Es muy abstracta la idea hasta que veamos un ejemplo:
public interface ISecurityProvider
{
User GetUser(string name);
void RemoveUser(User user);
}
public class DatabaseProvider : ISecurityProvider
{
public User GetUser(string name)
{
// Codigo para obtener un usuario desde la bd
}
public void RemoveUser(User user)
{
// Codigo para borrar un usuario
}
}
public class ActiveDirectoryProvider : ISecurityProvider
{
public User GetUser(string name)
{
// Codigo para obtener un usuario desde AD
}
public void RemoveUser(User user)
{
// Active directory no permite el borrado de usuario, asi
//que no se implementa.
throw new NotImplementedException();
}
}
public class UserController : Controller
{
private ISecurityProvider securityProvider;
public ActionResult RemoveUser(string name)
{
User user = securityProvider.GetUser(name);
if (securityProvider is DatabaseProvider) // rompe LSP
securityProvider.RemoveUser(user);
//retornar....
}
}
En el ejemplo DatabaseProvider y ActiveDirectoryProvider implementan ISecurityProvider, pero si nos fijamos bien ActiveDIrectoryProvider no permite el borrado de usuarios, por lo tanto no implementaría el método RemoveUser. Lo cual quiere decir que cuando la clase UserController use una instancia de ActiveDirectoryProvider y el método RemoveUser se encontrará con una excepción; como se ve claramente las instancias de un subtipo no son intercambiables por su padre ya que pueden ocurrir comportamientos inesperados al usar una de ellas.
De aquí se deduce que a la hora de usar clases bases o interfaces como ancestro común se tenga un cuidado importante sobre qué van a implementar sus subtipos. El principio de Liskov nos dice que los subtipos de un ancestro común deben poder reemplazar a su padre sin inconvenientes.
Resumen: “cada clase derivada debería proveer no más que el padre y no menos que él”.
Interface Segregation Principle
Este principio es simple y nos dice que un cliente no debería ser obligado a “mirar” interfaces que no necesita y que los componentes no deberían ser obligados a implementar interfaces que no planean usar. Esto nos quiere decir que los contratos (interfaces) que construyamos deberían ser lógicamente cohesivos (una vez mas un principio universal dando vueltas), en lugar de interfaces con muchas responsabilidades se prefieren varias interfaces con pequeñas responsabilidades. En otras palabras: interfaces “generosas” son un mal bicho.
Vamos a tomar un ejemplo prestado:
public interface IDoor
{
void Lock();
void Unlock();
Boolean IsDoorOpen { get; }
Int32 OpenTimeout { get; set; }
event EventHandler DoorOpenForTooLong;
}
En síntesis, la interface IDoor representa la abstracción de una puerta con comportamiento regular (Lock y UnLock) y con la posibilidad también de disparar una alarma (DoorOpenForToLoong). Se nota que desde el vamos, la interface acoge mas de una responsabilidad y que puede haber clases que la implementen que no requieren el manejo de alarmas.
Veamos ahora cómo quedaría la versión aplicando el principio de Segregación de Interfaces:
public interface IDoor
{
void Lock();
void Unlock();
Boolean IsDoorOpen { get; }
}
public interface ITimedDoor
{
Int32 OpenTimeout { get; set; }
event EventHandler DoorOpenForTooLong;
}
public class RegularDoor : IDoor
{
public void Lock()
{
throw new NotImplementedException();
}
public void Unlock()
{
throw new NotImplementedException();
}
public bool IsDoorOpen
{
get { throw new NotImplementedException(); }
}
}
public class TimedDoor : IDoor, ITimedDoor
{
public void Lock()
{
throw new NotImplementedException();
}
public void Unlock()
{
throw new NotImplementedException();
}
public bool IsDoorOpen
{
get { throw new NotImplementedException(); }
}
public int OpenTimeout
{
get;
set;
}
public event EventHandler DoorOpenForTooLong;
}
Aquí hemos dividido una interface “generosa” en dos interfaces con responsabilidades cohesivas. De eso se trata la segregación de interfaces: crear contratos con alta cohesión.
Dependency Inversion Principle
Este principio nos dice simplemente que deberíamos programar contra interfaces y no contra implementaciones concretas. Se debería abstraer en interfaces las partes que consideramos sujetas al cambio. En otras palabras, componentes que dependen de otros deberían interactuar a través de abstracciones (interfaces) en lugar de clases especificas.
La ventaja principal del principio es que programando contra interfaces los diferentes componentes pueden desarrollarse y cambiarse independientemente de quien los usa (porque el cliente emplea su “contrato”). La otra ventaja es que obtenemos código mas testeable.
Tomaré un ejemplo prestado una vez más:
interface ISearchProvider
{
List<string> FindAll();
}
public class SearchController : Controller
{
//Se programo contra una interfaz y no contra una
//implementacion
private ISearchProvider searchProvider;
public SearchController(ISearchProvider provider)
{
this.searchProvider = provider;
}
}
public class ProductRepository : ISearchProvider
{
public List<string> FindAll()
{
throw new NotImplementedException();
}
}
La clase SearchController usa un objeto que implementara la interface ISearchProvider. Es decir, no especificó qué clase empleará con el contrato ISearchProvider, sino que indico cuál es esa interface y nada más.
Para profundizar muy buena esta presentación: http://ircmaxell.github.io/DontBeStupid-Presentation/
Estos son todos los principios SOLID, pero existe otro principio unificador que de alguna forma aglutina todos estos para darle un sentido más preciso: INVERSION DE CONTROL….en otro post.
No hay comentarios.:
Publicar un comentario