lunes, 4 de febrero de 2019

Regla N°18 de la Ingeniería de Software: Mejor herencia que sentencias condicionales (evita el código espagueti)


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

Ejemplo de SuperBASIC (No estructurado)

  
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:

Ejemplo de SuperBASIC (Eestructurado)

  
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.

Convertiremos la estructura de if, en un switch, que se lee mas fácilmente:




  
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.

1 comentario:

  1. Caí en este blog por este articulo y me he leído todos los demás.

    Este 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.

    ResponderEliminar