Los principios SOLID son un conjunto de principios que ayudan a desarrollar aplicaciones de una manera «correcta» desde el punto de vista de diseño e implementación.
SOLID es un conjunto de acrónimos que representan a todos los principios «buenos» de programación orientada a objetos. Si bien, estos acrónimos están directamente relacionados con los siguientes principios SOLID y de los cuales estaremos hablando en una serie de blogs a los cuales llamaré: SOLIeDificando .Net
S -> Single Responsibility
O -> Open Closed
L -> Liskov Substitution
I -> Interface Segregation
D -> Dependency Inversion
Ahora pasaremos a ver más en detalles de cómo funciona el primer principio:
1. Single Responsibility:
Cada clase debería hacer una única cosa, cada método debería hacer una única cosa. Ahora, si eres como yo y le encuentras el punto a todas las i’s, podrías preguntar algo como: «¿Pero qué pasa cuando en un método tengo que hacer varias cosas para conseguir una?».
Bueno, para esto usaré de ejemplo el periódico. Un periódico, tiene un resumen importante con los títulos de las noticias más interesantes en la portada. Además tiene una introducción a la noticia y la página donde se encuentra. Luego la noticia en sí normalmente comienza con una frase resumen, que luego es explicada en la noticia con ejemplos, frases y algunas veces (ni tiene nada que ver con el título).
Ahora volvamos al código. Para conseguir el principio Single Responsibility, necesitamos determinar niveles de abstracción. Estos niveles de abstracción vendrán dados en número según uno entienda usando su «sentido común», es como la «pizca de sal» de las recetas. Depende también de cómo de compleja es la función y cuántas cosas haga (aunque en su abstracción hace una sola cosa.)
El número ideal de líneas de una función es 4 líneas, aunque a veces me he encontrado por ahí funciones con varios miles, y es ahí donde me llevo la mano a la cabeza y suspiro, mis compañeros me miran y yo sonrío como si nada. Cuatro líneas es el número ideal, sin embargo, en la práctica hay métodos que inevitablemente son «grandes». Ejemplo de esto es cuando vamos hacer un «swith case», esta instrucción nos fuerza a tener a veces 5, 10, 20 líneas o más en un método. Algunos autores recomiendan poner estos métodos cuyo nivel de abstracción es prácticamente cero, en lugares aislados de la aplicación donde no sean visibles y solo tengan que «tocarse» en casos muy concretos para cambiar la estructura del swith case.
Esto de los niveles de abstracción es un poco subjetivo, pero para dar una idea mejor voy a dar mi propia definición semi-formal de niveles de abstracción y voy a desarrollar un ejemplo que nos dará herramientas para cultivar y desarrollar el hábito de programar con este principio:
Mi propia definición de abstracción:
- Cada línea de una función debe tener el «mismo» nivel de abstracción que las demás líneas de la misma función.
- El nivel de abstracción de una función es el mismo que el de sus líneas.
- Una función de determinada abstracción llama a otras funciones con igual o menos abstracción que ella.
Por ejemplo, supongamos que estamos programando un servicio para obtener entidades.
public EstructuraEspecial ObtenerEntidades()
{
throw new NotImplementedException();
}
Análisis TopDown: Mi hábito para separar y responsabilizar:
A mí personalmente me gusta usar un análisis Top Down en niveles de abstracción. Primero pienso: ¿Cómo puedo describir el funcionamiento de esta función de una manera muy abstracta, y luego con cada función que vaya creando voy bajando el nivel de abstracción hasta llegar al punto en que hay una funcionalidad única en la función y su nivel de abstracción es «cero»?
El siguiente diseño, aunque un poco alejado de la vida real, se propone de manera educativa:
Bien, supongamos que queremos obtener una entidades y que estas están encriptadas en base de datos, que tienen un formato no deseable para quien la quiere (esto puede ser bool y queremos un Sí o un No…), y que necesitamos construir unas estructuras especiales utilizando la información de ellas.
Esto se traduce como las siguientes acciones: obtener entidades de base de datos, desencriptar, convertir a dtos, y por último construir esas estructuras especiales asociadas a los datos.
Notemos sin embargo que nuestra función no hace varias cosas. Desde un punto de vista abstracto, nuestra función obtiene las entidades auxiliándose de otras funciones para pasar por tres procesos:
- Obtener entidades de base de datos
- Convertir de base de datos a DTO
- Construir estructura especial.
Es decir, nuestra función no solo realiza sino que describe el proceso de obtener entidades, sin tener nada que ver con la implementación de esos pasos del proceso. Su función es unificar esos pasos menos abstractos, pero aún así abstractos en sí mismos. Debido a esto se puede decir que tiene una sola responsabilidad: Realizar las llamadas a las funciones que realizan pasos más específicas del proceso de obtener entidades.
public EstructuraEspecial EstructuraEspecialGetAllEntities()
{
List<EntidadDb > entitiesDb = ObtenerEntidadesDeBaseDatos();
List<EntidadDto > entitiesDto = ConvertirDeBdADto(entitiesDb);
EstructuraEspecial result = ConstruirEstructuraEspecial(entitiesDto);
return result;
}
Pero me podrías preguntar: ¿Qué ha sido de la parte de desencriptar?, bueno no siempre toda la funcionalidad que uno dice verbalmente tiene el mismo nivel de abstracción, así mismo esta aplicación imaginaria tiene los campos de las entidades de la base de datos encriptados y son de tipo byte[], con lo cual parece lógico que cuando se conviertan a Dto estos sean desencriptados. Por tanto, desde este punto de vista, la funcionalidad de desencriptar, forma parte de la conversión a Dto. Esto lo hago para ilustrar que el árbol de abstracción no tiene que tener el mismo nivel en todas sus ramas y que puede que hayan algunas ramas más profundas que otras. De cualquier forma como veremos más adelante el desencriptar no debe formar parte intrínseca de la conversión, sino que tiene que ser usada por esta última y estar aislada en otro «sitio» desde donde como funcionalidad pueda ser usada por otros.
En nuestro método que construye la estructura especial se delega en otra clase que hace el trabajo.
private EstructuraEspecial ConstruirEstructuraEspecial(List< EntidadDto> entidadesDto)
{
EstructuraEspecial estructura= new EstructuraEspecial();
foreach (var entidadDto in entidadesDto)
{
estructura.AñadirElemento(entidadDto );
}
return structure;
}
También para evitar complejidades innecesarias en esta explicación permitimos que la clase EstructuraEspecial se forme internamente sin dejarnos ver qué es.
Desarrollemos ahora el método ConvertirDeBdADto:
private List <EntidadDto> ConvertirDeBdADto( List<EntidadDb > entidadesDb)
{
List<EntidadDescifrada> entidadesDescifradas = DescifrarEntidades(entidadesDb);
List<EntidadDto> entidadeDto = MapearEntidades(entidadesDescifradas);
return entidadeDto;
}
private List<EntidadDto> MapearEntidades(List< EntidadDescifrada> entidadesDescifradas)
{
Mapeador mapeador = new Mapeador();
List<EntidadDto> entidadeDto = mapeador.Mapear(entidadesDescifradas);
return entidadeDto;
}
private List <EntidadDescifrada> DescifrarEntidades( List<EntidadDb > entidadesDb)
{
Cifrador cifrador = new Cifrador();
List<EntidadDescifrada > entidadesDescifradas = cifrador.Descifrar(entidadesDb);
return entidadesDescifradas;
}
Ahora, vemos cómo las funciones MapearEntidades y DescifrarEntidades tienen un nivel de abstracción bastante similar. Ambas delegan la responsabilidad en una entidad externa que se dedica a hacer esa cosa en específico que ellas quieren.
De esta forma es como personalmente hago la separación de responsabilidades e implemento a su vez funciones más modulares y a su vez creo clases específicas para cada acción. Por ejemplo, el objeto Mapeador, solo se encarga de mapear entidades, internamente podría llamar a funciones de Reflection, atributos de las clases que mapea, etc. Pero eso está encapsulado y estaría en una función poco abstracta que a su vez forma parte de un camino de funciones con distintos niveles de abstracción.
Todavía queda mucho de este tema de lo que quisiera hablarles, pero el tiempo exige que termine pronto para no aburrir en exceso. En próximos posts estaré modificando este código para mostrar cómo encajarían los demás principios. Algunos serán más divertidos pues estaré integrando tecnologías de .Net con estos últimos.
Más información:
Jorge Casals, estudió Cibernética en Cuba. Es un fiel seguidor de la Programación Orientada a Objetos, los principios SOLID y los Patrones de Diseño. Actualmente, trabaja como Analista-Programador en Sogeti.
Para saber más sobre Sogeti: www.sogeti.es/soluciones/soluciones-microsoft/
Gracias por la introducción Jorge. Que sencillo es de entender cuando se lee y que difícil es hacer entender a algunos desarrolladores la importancia de estos principios.
Me gustaMe gusta