El primer ordenador que tuve fue un Sinclair QL, era una especie de evolución del
famoso ZX Spectrum. En su tiempo fue un ordenador personal poderoso, aunque no tan
popular como su antecesor. Desde que iniciaba, como intérprete de comandos y lenguaje
principal tenía una versión de BASIC, llamada SuperBASIC. Recuerdo
como pasaba días enteros haciendo código en BASIC, para posteriormente imprimirlos
en una impresora matricial, tratando de encontrar el bug perdido que no conseguía
hallar y buscando alguna forma de recodar que se supone que debieran hacer los programas,
a veces era más sencillo tirar todo el código y volver a hacerlo de nuevo. Si bien
antes tenía el entusiasmo y el tiempo para hacer un código de cero por no entenderlo,
ahora solo me queda el entusiasmo y muy poco tiempo. Aunque estaba muy orgulloso
de los mis primeros códigos, he de reconocer, al pasar del tiempo, que eran horribles,
tan difíciles de entender, como de modificar.
El código Espagueti es aquel que tiene un control de flujo demasiado complicado
para entenderlo claramente, además de que son prácticamente imposibles de modificar
por miedo a que mientras cambiamos algo se estropee otra cosa. El código esta enredado
tal como un plato de espagueti, se llega a esta situación (en la actualidad), a
base de agregar sentencias if encadenas, grandes y complejas. Ya de por sí,
tener dos if encadenados aumenta la complejidad del código y la dificultad
de mantenerlo.
Primitivamente el código espagueti era causado por los saltos de línea en el código,
las famosas instrucciones goto, que ya no son comunes, básicamente por que
los lenguajes modernos no la incluyen. La instrucción goto cambia la línea
actual de un programa a otra línea, continuando la ejecución desde el punto señalado.
Veamos un ejemplo de esto en BASIC primitivo (No estructurado):
Ejemplo sacado e la Wikipedia
https://es.wikipedia.org/wiki/BASIC
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | 10 INPUT "Cuál es su nombre:" ; NN$ 20 PRINT "Bienvenido al 'asterisquero' " ;NN$ 25 PRINT 30 INPUT "con cuántos asteriscos inicia [Cero sale]:" ; N 40 IF N<=0 THEN GOTO 200 50 AS$= "" 60 FOR I=1 TO N 70 AS$=AS$+ "*" 80 NEXT I 90 PRINT "AQUI ESTAN:" ; AS$ 100 INPUT "Desea más asteriscos:" ;SN$S 110 IF SN$= "" THEN GOTO 100 120 IF SN$<> "S" AND N$<> "s" THEN GOTO 200 130 INPUT "CUANTAS VECES DESEA REPETIRLOS [Cero sale]:" ; VECES 140 IF VECES<=0 THEN GOTO 200 150 FOR I=1 TO VECES 160 PRINT AS$; 170 NEXT I 180 PRINT 185 REM A repetir todo el ciclo (comentario) 190 GOTO 25 200 END |
10 INPUT "Cuál es su nombre:"; NN$ 20 PRINT "Bienvenido al 'asterisquero' ";NN$ 25 PRINT 30 INPUT "con cuántos asteriscos inicia [Cero sale]:"; N 40 IF N<=0 THEN GOTO 200 50 AS$="" 60 FOR I=1 TO N 70 AS$=AS$+"*" 80 NEXT I 90 PRINT "AQUI ESTAN:"; AS$ 100 INPUT "Desea más asteriscos:";SN$S 110 IF SN$="" THEN GOTO 100 120 IF SN$<>"S" AND N$<>"s" THEN GOTO 200 130 INPUT "CUANTAS VECES DESEA REPETIRLOS [Cero sale]:"; VECES 140 IF VECES<=0 THEN GOTO 200 150 FOR I=1 TO VECES 160 PRINT AS$; 170 NEXT I 180 PRINT 185 REM A repetir todo el ciclo (comentario) 190 GOTO 25 200 END
Al principio lo que más destaca es la dificulta para leerlo y comprenderlo. Es mas
hay que leer todo como un bloque y de arriba abajo. Básicamente la salida del programa
es la siguiente:
Cuál es su nombre: Jose Luis
Bienvenido al 'asterisquero' Jose Luis
con cuántos asteriscos inicia [Cero sale]: 5
AQUI ESTAN:*****
Desea más asteriscos: S
CUANTAS VECES DESEA REPETIRLOS [Cero sale]: 5
*************************
Analizándolo, vemos que no tiene ningún tipo de estructura reconocible, simplemente
el programa comienza en la línea 10 y llegado un momento, según condiciones, mueve
el flujo a la línea corresponda, por ejemplo en la línea 40, si N<=0 salta a
la línea 200. El problema de estos saltos es básicamente que no sabemos dónde vamos
a acabar sin leer todo el bloque, los saltos pueden ser hacia delante, y hacia atrás
en el código (si ningún tipo de restricción), incluso si nos viéramos en la necesidad
de agregar nuevas líneas debiéramos tener cuidado en donde las insertamos, pues
no sabemos desde donde se van a llamar en nuestro código, y si le va pegar a lo
ya establecido. En definitiva es muy difícil tener el control de la secuencia de
ejecución una vez que ya se ha establecido.
El mismo código en un BASIC que permite programación estructura es el siguiente:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | iTrue = -1 'Flag en Verdadero INPUT "¿Cuál es su nombre" ; NombreUsuario$ PRINT "Bievenido al 'asterisquero'," ; NombreUsuario$ DO PRINT "" INPUT "¿Con cuántos asteriscos inicia [Cero sale]:" ; NroAsteriscos IF NroAsteriscos<=0 THEN EXIT DO Asteriscos$ = "" FOR I=1 TO NroAsteriscos Asteriscos$=Asteriscos$ + "*" NEXT I PRINT "AQUI ESTAN: " ; Asteriscos$ DO INPUT "Desea más asteriscos:" ;SN$ LOOP UNTIL SN$<> "" IF SN$<> "S" AND SN$<> "s" THEN EXIT DO 'Salida INPUT "CUANTAS VECES DESEA REPETIRLOS [Cero sale]:" ;iVeces IF iVeces<=0 THEN EXIT DO 'Salida FOR I = 1 TO iVeces PRINT Asteriscos$; NEXT I PRINT LOOP WHILE iTrue END |
iTrue = -1 'Flag en Verdadero INPUT "¿Cuál es su nombre"; NombreUsuario$ PRINT "Bievenido al 'asterisquero',"; NombreUsuario$ DO PRINT "" INPUT "¿Con cuántos asteriscos inicia [Cero sale]:"; NroAsteriscos IF NroAsteriscos<=0 THEN EXIT DO Asteriscos$ = "" FOR I=1 TO NroAsteriscos Asteriscos$=Asteriscos$ + "*" NEXT I PRINT "AQUI ESTAN: "; Asteriscos$ DO INPUT "Desea más asteriscos:";SN$ LOOP UNTIL SN$<>"" IF SN$<>"S" AND SN$<>"s" THEN EXIT DO 'Salida INPUT "CUANTAS VECES DESEA REPETIRLOS [Cero sale]:";iVeces IF iVeces<=0 THEN EXIT DO 'Salida FOR I = 1 TO iVeces PRINT Asteriscos$; NEXT I PRINT LOOP WHILE iTrue END
Si nos fijamos, aunque sea por el indentado, es mucho más fácil de leer, también
que no tenga números de líneas facilita la tarea.
A simple vista es un bucle DO..WHILE, con varios bucles FOR..NEXT y DO…UNTIL, internos.
Incluso en este caso el código sigue algo “espaguetizado”, debido a la repetición
de código (que dificulta su mantenimiento y compresión), por ejemplo este código
se repite, casi por igual:
1 2 3 4 | FOR I=1 TO NroAsteriscos Asteriscos$=Asteriscos$ + "*" NEXT I PRINT "AQUI ESTAN: " ; Asteriscos$ |
FOR I=1 TO NroAsteriscos Asteriscos$=Asteriscos$ + "*" NEXT I PRINT "AQUI ESTAN: "; Asteriscos$
Y
1 2 3 | FOR I = 1 TO iVeces PRINT Asteriscos$; NEXT I |
FOR I = 1 TO iVeces PRINT Asteriscos$; NEXT I
Con todo, hubo gran mejora en el primer código y en el Segundo.
Como comentábamos en la actualidad es muy difícil que un lenguaje moderno tenga
una instrucción goto valida. El código espagueti se consigue cuando la combinación
de condicionales, y estructura de bloques, se anida, se complica y se extiende demasiado.
Para evitar que nuestro programa contenga código espagueti debemos evitar usar instrucciones
condicionales, cuando estas pueden evitarse usando mecanismos de herencia, "Mejor
herencia que condicionales".
En los siguientes párrafos estaremos viendo un ejemplo de la "desespaguetización"
de un código creado en C#, para Visual Studio.
Se puede descargar el código desde GitHub en:
Un ejemplo actual de código espagueti
Imaginemos el siguiente
ejemplo, queremos crear una API que guarda un mensaje (una simple cadena de texto
que nos informe de algún suceso). Las opciones para guardar un mensaje son:
-
En base de datos
-
En un archivo de texto plano
-
En el log de eventos de Windows.
En un primer acercamiento ponernos definir una clase de nombre GestorMensajes, con
un métodos que se llame Guardar, que recibirá dos parámetros, uno es el cómo va
a guardar, dos el mensaje a guardar.
El código de la clase es el siguiente:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="modo">Modo para guardar</param> /// <param name="mensaje">Mensaje a guardar</param> public void Guardar(ModoGuardado modo, string mensaje) { if (modo == ModoGuardado.ArchivoPlano) //Guardo la información en el archivo de texto { string directorioArchivo = AppDomain.CurrentDomain.BaseDirectory; string rutaArchivo = Path.Combine(directorioArchivo, "bitacora.txt" ); File.AppendAllText(rutaArchivo, $ "{mensaje}{Environment.NewLine}" ); } if (modo == ModoGuardado.BaseDatos) //Guardo la información en la base de datos { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Si no existe la base de datos (archivo mdf) lo creo string directorioBaseDatos = AppDomain.CurrentDomain.BaseDirectory; AppDomain.CurrentDomain.SetData( "DataDirectory" , directorioBaseDatos); string rutaBaseDatos = Path.Combine(directorioBaseDatos, @"EjemploCodigoEspagueti.mdf" ); context.Database.CreateIfNotExists(); //Agrego el mensaje en la base de datos context.Bitacora.Add( new Bitacora { mensaje = mensaje }); context.SaveChanges(); } } else if (modo == ModoGuardado.VisorEventos) //Lo guardo en el Visor de eventos de Windows { //Si no existe en memoria lo creo string source = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; string log = "Application" ; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, log); } //Guardo el mensaje. System.Diagnostics.EventLog appLog = new System.Diagnostics.EventLog(); appLog.Source = source; appLog.WriteEntry(mensaje); } else { throw new InvalidOperationException($ "No se reconoce el tipo de guardado {modo}" ); } } |
/// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="modo">Modo para guardar</param> /// <param name="mensaje">Mensaje a guardar</param> public void Guardar(ModoGuardado modo, string mensaje) { if (modo == ModoGuardado.ArchivoPlano) //Guardo la información en el archivo de texto { string directorioArchivo = AppDomain.CurrentDomain.BaseDirectory; string rutaArchivo = Path.Combine(directorioArchivo, "bitacora.txt"); File.AppendAllText(rutaArchivo, $"{mensaje}{Environment.NewLine}"); } if (modo == ModoGuardado.BaseDatos) //Guardo la información en la base de datos { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Si no existe la base de datos (archivo mdf) lo creo string directorioBaseDatos = AppDomain.CurrentDomain.BaseDirectory; AppDomain.CurrentDomain.SetData("DataDirectory", directorioBaseDatos); string rutaBaseDatos = Path.Combine(directorioBaseDatos, @"EjemploCodigoEspagueti.mdf"); context.Database.CreateIfNotExists(); //Agrego el mensaje en la base de datos context.Bitacora.Add(new Bitacora { mensaje = mensaje }); context.SaveChanges(); } } else if (modo == ModoGuardado.VisorEventos) //Lo guardo en el Visor de eventos de Windows { //Si no existe en memoria lo creo string source = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; string log = "Application"; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, log); } //Guardo el mensaje. System.Diagnostics.EventLog appLog = new System.Diagnostics.EventLog(); appLog.Source = source; appLog.WriteEntry(mensaje); } else { throw new InvalidOperationException($"No se reconoce el tipo de guardado {modo}"); } }
Este es un buen ejemplo de código espagueti, vemos como todo esta resuelvo en un
único y exclusivo método, allí mediante una serie de if encadenados
seleccionamos como tenemos que guardar el mensaje.
¿Cuál es el principal problema del código anterior?, la dificultad del mantenimiento
que ofrece el método, que mezcla muchos conceptos agrupados en muy poco espacio,
y su falta de escalabilidad, si tuviéramos que agregar una nueva forma de guardar
mensajes, debiéramos agregar un nuevo, ify aumentar el tamaño y complejidad
del código. Este solo es solo un métodos, pero imaginemos que tenemos un método
para consultar mensajes (según se haya guardado) y otro para eliminarnos, nuestra
complejidad crecería enormemente.
Simplificación del código espagueti
Mejoremos el código anterior. Lo primero que podemos hacer es separa el método en
funciones más pequeñas que solo realizan una tarea, así tendremos una para guardar
archivos planos, otra para base de datos y otra para el visor de eventos, adicionalmente
separaremos cualquier función necesaria para configurar los destinos del mensaje.
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 | /// <summary> /// Gestor de mensajes, su misión es guardar mensajes de bitacora en diversos medios. /// </summary> public class GestorMensajes { /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="modo">Modo para guardar</param> /// <param name="mensaje">Mensaje a guardar</param> public void Guardar(ModoGuardado modo, string mensaje) { switch (modo) { case ModoGuardado.ArchivoPlano: //Guardo la información en el archivo de texto GuardarArchivoPlano(mensaje); break ; case ModoGuardado.BaseDatos: //Guardo la información en la base de datos GuardarBaseDatos(mensaje); break ; case ModoGuardado.VisorEventos: //Lo guardo en el Visor de eventos de Windows GuardarVisorEventos(mensaje); break ; default : throw new InvalidOperationException($ "No se reconoce el tipo de guardado {modo}" ); } } /// <summary> /// Guarda el mensaje dentro de un archivo plano /// </summary> /// <param name="mensaje">Mensaje a guardar</param> private void GuardarArchivoPlano( string mensaje) { string directorioArchivo = AppDomain.CurrentDomain.BaseDirectory; string rutaArchivo = Path.Combine(directorioArchivo, "bitacora.txt" ); File.AppendAllText(rutaArchivo, $ "{mensaje}{Environment.NewLine}" ); } /// <summary> /// Configura la base de datos, creandola si no existe /// </summary> private void ConfigurarBaseDatos() { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Si no existe la base de datos (archivo mdf) lo creo string directorioBaseDatos = AppDomain.CurrentDomain.BaseDirectory; AppDomain.CurrentDomain.SetData( "DataDirectory" , directorioBaseDatos); string rutaBaseDatos = Path.Combine(directorioBaseDatos, @"EjemploCodigoEspagueti.mdf" ); context.Database.CreateIfNotExists(); } } /// <summary> /// Guarda el mensaje dentro de una base de datos /// </summary> /// <param name="mensaje">Mensaje a guardar</param> private void GuardarBaseDatos( string mensaje) { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { ConfigurarBaseDatos(); //Agrego el mensaje en la base de datos context.Bitacora.Add( new Bitacora { mensaje = mensaje }); context.SaveChanges(); } } /// <summary> /// Crea la fuente para el visor de eventos. /// </summary> private void ConfigurarVisorEventos() { //Si no existe en memoria lo creo string source = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; string log = "Application" ; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, log); } } /// <summary> /// Guarda el mensaje en el visor de eventos de windows /// </summary> /// <param name="mensaje">Mensaje a guardar</param> private void GuardarVisorEventos( string mensaje) { ConfigurarVisorEventos(); string source = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; //Guardo el mensaje. System.Diagnostics.EventLog appLog = new System.Diagnostics.EventLog(); appLog.Source = source; appLog.WriteEntry(mensaje); } } |
/// <summary> /// Gestor de mensajes, su misión es guardar mensajes de bitacora en diversos medios. /// </summary> public class GestorMensajes { /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="modo">Modo para guardar</param> /// <param name="mensaje">Mensaje a guardar</param> public void Guardar(ModoGuardado modo, string mensaje) { switch (modo) { case ModoGuardado.ArchivoPlano: //Guardo la información en el archivo de texto GuardarArchivoPlano(mensaje); break; case ModoGuardado.BaseDatos: //Guardo la información en la base de datos GuardarBaseDatos(mensaje); break; case ModoGuardado.VisorEventos: //Lo guardo en el Visor de eventos de Windows GuardarVisorEventos(mensaje); break; default: throw new InvalidOperationException($"No se reconoce el tipo de guardado {modo}"); } } /// <summary> /// Guarda el mensaje dentro de un archivo plano /// </summary> /// <param name="mensaje">Mensaje a guardar</param> private void GuardarArchivoPlano(string mensaje) { string directorioArchivo = AppDomain.CurrentDomain.BaseDirectory; string rutaArchivo = Path.Combine(directorioArchivo, "bitacora.txt"); File.AppendAllText(rutaArchivo, $"{mensaje}{Environment.NewLine}"); } /// <summary> /// Configura la base de datos, creandola si no existe /// </summary> private void ConfigurarBaseDatos() { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Si no existe la base de datos (archivo mdf) lo creo string directorioBaseDatos = AppDomain.CurrentDomain.BaseDirectory; AppDomain.CurrentDomain.SetData("DataDirectory", directorioBaseDatos); string rutaBaseDatos = Path.Combine(directorioBaseDatos, @"EjemploCodigoEspagueti.mdf"); context.Database.CreateIfNotExists(); } } /// <summary> /// Guarda el mensaje dentro de una base de datos /// </summary> /// <param name="mensaje">Mensaje a guardar</param> private void GuardarBaseDatos(string mensaje) { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { ConfigurarBaseDatos(); //Agrego el mensaje en la base de datos context.Bitacora.Add(new Bitacora { mensaje = mensaje }); context.SaveChanges(); } } /// <summary> /// Crea la fuente para el visor de eventos. /// </summary> private void ConfigurarVisorEventos() { //Si no existe en memoria lo creo string source = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; string log = "Application"; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, log); } } /// <summary> /// Guarda el mensaje en el visor de eventos de windows /// </summary> /// <param name="mensaje">Mensaje a guardar</param> private void GuardarVisorEventos(string mensaje) { ConfigurarVisorEventos(); string source = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; //Guardo el mensaje. System.Diagnostics.EventLog appLog = new System.Diagnostics.EventLog(); appLog.Source = source; appLog.WriteEntry(mensaje); } }
Las ventajas de esto es que hemos hecho más sencillo el código (aunque hemos necesitado
mas código), cada función realiza solo una tarea y son fáciles de modificar. El
problema es lo terriblemente poco cohesionado que esta nuestra clase. La cohesión
hace referencia a que los elementos que tienen que ver entre sí deben estar juntos
y los que no, deben estar separados y ser independientes. En esta clase poco tiene
que ver que se guarde un dato en una base de datos, con que se guarde en un archivo.
Al disminuir la cohesión, aumentamos el acoplamiento (y viceversa), y todo eso impacta
en el manteniendo y escalabilidad de un código.
A la tercera va la vencida (Orientación a objetos)
Cambiaremos la solución a un enfoque basado en objetos, con esto esperamos conseguir:
-
Eliminar el cogido espagueti al evitar usar condicionales de ningún tipo.
-
Aumentar la cohesión (cada clase solo tendrá métodos relacionados entre sí).
-
Se disminuirá el acoplamiento, cada clase podrá ser cambiada e incluso modificada sin afecta al resto del código.
Para ello la clase GestorMensajes, pasara a ser una clase abstracta con un método,
también abstracto llamado "Guardar". Es abstracta por que no tiene sentido instanciarla
por si misma (siempre se guardar de alguna "forma" y dicha forma no ni tiene que
estar especificada directamente en la clase GestorMensajes).
01 02 03 04 05 06 07 08 09 10 11 | /// <summary> /// Gestor de mensajes, su misión es guardar mensajes de bitacora en diversos medios. /// </summary> public abstract class GestorMensajes : IGestorMensajes { /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="mensaje">Mensaje a guardar</param> public abstract void Guardar( string mensaje); } |
/// <summary> /// Gestor de mensajes, su misión es guardar mensajes de bitacora en diversos medios. /// </summary> public abstract class GestorMensajes : IGestorMensajes { /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="mensaje">Mensaje a guardar</param> public abstract void Guardar(string mensaje); }
De la clase GestorMensajes heredaran tres clases; una para guardar en un archivo,
otra para base de datos y una final para el Visor de Eventos de Windows. Estas clase
solo implementaran las funciones relativas al guardado de su tipo.
Nótese que cada clase, GestorMensajesArchivoPlano, GestorMensajesBaseDatos y GestorMensajes
desconocen la existencia de las otras clases, son completamente independientes y
pueden modificarse de la manera que nos plazca sin afectar al resto en alguna forma.
Un ejemplo seria la clase de guardar en base de datos:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | /// <summary> /// Gestor de mensajes, su misión es guardar mensajes de bitacora en diversos medios. /// </summary> public class GestorMensajesBaseDatos : GestorMensajes { /// <summary> /// Contructor por defecto /// </summary> public GestorMensajesBaseDatos() { //Inicializa la base de datos ConfigurarBaseDatos(); } /// <summary> /// Configura la base de datos, creandola si no existe /// </summary> private void ConfigurarBaseDatos() { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Si no existe la base de datos (archivo mdf) lo creo string directorioBaseDatos = AppDomain.CurrentDomain.BaseDirectory; AppDomain.CurrentDomain.SetData( "DataDirectory" , directorioBaseDatos); string rutaBaseDatos = Path.Combine(directorioBaseDatos, @"EjemploCodigoEspagueti.mdf" ); context.Database.CreateIfNotExists(); } } /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="mensaje">Mensaje a guardar</param> public override void Guardar( string mensaje) { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Agrego el mensaje en la base de datos context.Bitacora.Add( new Bitacora { mensaje = mensaje }); context.SaveChanges(); } } } |
/// <summary> /// Gestor de mensajes, su misión es guardar mensajes de bitacora en diversos medios. /// </summary> public class GestorMensajesBaseDatos : GestorMensajes { /// <summary> /// Contructor por defecto /// </summary> public GestorMensajesBaseDatos() { //Inicializa la base de datos ConfigurarBaseDatos(); } /// <summary> /// Configura la base de datos, creandola si no existe /// </summary> private void ConfigurarBaseDatos() { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Si no existe la base de datos (archivo mdf) lo creo string directorioBaseDatos = AppDomain.CurrentDomain.BaseDirectory; AppDomain.CurrentDomain.SetData("DataDirectory", directorioBaseDatos); string rutaBaseDatos = Path.Combine(directorioBaseDatos, @"EjemploCodigoEspagueti.mdf"); context.Database.CreateIfNotExists(); } } /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="mensaje">Mensaje a guardar</param> public override void Guardar(string mensaje) { using (EjemploCodigoEspaguetiEntities context = new EjemploCodigoEspaguetiEntities()) { //Agrego el mensaje en la base de datos context.Bitacora.Add(new Bitacora { mensaje = mensaje }); context.SaveChanges(); } } }
Adicional a esto declaramos una interfaz de nombre IGestorMensajes, que contendrá
un solo método llamado Guardar. Nuestro sistema trabajara solo con la interfaz y
no con la clase abstracta. El cuándo usar interfaz o una clase abstracta (o con
los dos) en un tema que a veces se torna complicado, pero trabajar con interfaces,
que son implementadas por clases abstractas nos da mucha muchas ventajas; por ejemplo
al usar nuestro código la interfaz, se puede enfocar solo en los asuntos de negocio
que debiera hacer, lo cual reduce el acoplamiento, al usar la clase abstracta, se
define los flujos que deben seguir dicha clase y sus hijas, sin entrar en detalles
concretos de implementación. Más información en "¿En qué se diferencia las interfaces de las clases abstractas?".
01 02 03 04 05 06 07 08 09 10 11 | /// <summary> /// Interfaz de GestorMensaje /// </summary> public interface IGestorMensajes { /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="mensaje">Mensaje a guardar</param> void Guardar( string mensaje); } |
/// <summary> /// Interfaz de GestorMensaje /// </summary> public interface IGestorMensajes { /// <summary> /// Guarda el mensaje segun el metodo que se indica como parametro /// </summary> /// <param name="mensaje">Mensaje a guardar</param> void Guardar(string mensaje); }
Nuestro diagrama quedaría así:
Salta a la vista que según nuestra necesidad debemos usar una clase o otra, lo cual
podría se engorroso, sobre todo si se lo dejamos al sistema consumidor.
Para solucionar el problema vamos a delegar la creación de la clase adecuada a otra
clase, usando el patrón Factory. Nuestra clase erigirá el método adecuado de guardado
de mensajes según una configuración externa (en el archivo app.config asociado a
la aplicación).
Esta es la configuración:
1 2 3 4 5 | < DesdeLasHorasExtras.EjemploCodigoEspagueti3.Properties.Settings > < setting name = "GestorMensajes" serializeAs = "String" > < value >EjemploCodigoEspagueti3.Mensajes.GestorMensajesArchivoPlano</ value > </ setting > </ DesdeLasHorasExtras.EjemploCodigoEspagueti3.Properties.Settings > |
<DesdeLasHorasExtras.EjemploCodigoEspagueti3.Properties.Settings> <setting name="GestorMensajes" serializeAs="String"> <value>EjemploCodigoEspagueti3.Mensajes.GestorMensajesArchivoPlano</value> </setting> </DesdeLasHorasExtras.EjemploCodigoEspagueti3.Properties.Settings>
Esa es la clase:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | /// < summary > /// Clase que se encarga de crear el objeto de tipo GestorMensajes /// </ summary > public static class GestorMensajesFactory { /// < summary > /// Crea una clase de Gestor Mensajes, segun los indicado en el App.config /// </ summary > /// < returns ></ returns > public static IGestorMensajes Crear() { return (IGestorMensajes)Assembly.GetExecutingAssembly().CreateInstance(Settings.Default.GestorMensajes); } } |
/// <summary> /// Clase que se encarga de crear el objeto de tipo GestorMensajes /// </summary> public static class GestorMensajesFactory { /// <summary> /// Crea una clase de Gestor Mensajes, segun los indicado en el App.config /// </summary> /// <returns></returns> public static IGestorMensajes Crear() { return (IGestorMensajes)Assembly.GetExecutingAssembly().CreateInstance(Settings.Default.GestorMensajes); } }
Como vemos mediante reflexión, se crea la clase de guardado de mensajes adecuada,
nuestro consumidor lo puede usar de la siguiente forma:
//Instancio la clase gestor de mensajes, es la encargada de guardar los mensajes
IGestorMensajes gestor = GestorMensajesFactory.Crear();
//Guardo un mensaje de cada tipo.
gestor.Guardar("Mensaje de ejemplo.");
Como vemos, y a aunque a primea vista puede parece los contrario, se simplifico
el código usando objetos, se crearon varias clases más pequeñas, pero con una sola
funcionalidad, se aumento la escalabilidad del sistema, y su cohesión. Para entender
por qué esto es importante, conviene revisar la regla "Va a cambiar", la regla "Va a fallar" y la regla "Se va a mantener".
Por último hacer notar que es un ejemplo sencillo, pero si se quiere revisar un
buen código que aplica los mismos principios, y con una funcionalidad semejante,
recomiendo echar un vistazo al proyecto https://nlog-project.org/,
diseñador para guardar logs en distintas fuentes y distintos criterios.
Caí en este blog por este articulo y me he leído todos los demás.
ResponderEliminarEste blog me parece de lo mejor que he visto para un programador o aficionado que es mas bien lo que soy. Al final nos limitamos aprender las estructuras básicas y cuando oímos hablar de abstracción, herencia, poliformismo.... miramos a otro lado.
Tengo que dar las gracias por este blog, no solo he tomado conciencia del deficiente código creado por mi, sino que he descubierto que hay un mundo mas allá. Gracias ha este blog he descubierto una literatura sobre la programación fantástica.
Sigue así me han encantado tus artículos.