sábado, 18 de mayo de 2019

Paradigmas y tipos de lenguajes informáticos (2 de 3)

Segunda parte de las clasificaciones de los lenguajes informáticos, esta vez vamos separarlos por lenguajes interpretados y compilados y también según el tipado.

Lenguajes interpretados o compilados



Lenguajes compilados


Los lenguajes compilados son aquellos en los que el código fuente pasa por una seria de transformaciones hasta que se genera un código máquina (dicho proceso, se llama compilación). El código maquina es el "producto final", el producto que se va a distribuir, y ejecutar.

Características

  • Están compilados para una arquitectura hardware en particular y para unos sistemas operativos en concreto, esto hace que, si bien su velocidad sea la más óptima posible, sean dependiente de dichas arquitecturas, es decir si queremos ejecutar nuestro software en diversas plataformas, debemos compilarlo varias veces.
    Aunque pudiera parecer "sencillo" compilarlo varias veces, una para cada plataforma, descubríamos ciertamente que no lo es. Entre los problemas a encontrar están los siguientes:
  • Los procesadores de 32 y 64 bits, tienen distintos tamaños para sus tipos de variables, por ejemplo un entero en el compilador de 32 bites pude medir 2 byte y en el de 64bits, puede medir 4, lo cual provoca caer fácilmente en problemas de memoria si dependemos del tamaño de las variables. ara tomar decisiones. Lo anterior también puede ocurrir entre distintas versiones de un mismo lenguaje en varios compiladores.
  • Las librerías y utilerías de una plataforma, no tienen por qué estar en otra, con lo que nos obligaría a reestructurar completamente nuestro código, según tengamos dependencias de estas.

Es ciertamente una forma de no compartir nuestro código fuente sin necesidad, pero debemos considerar que existen herramientas de descompilación, que nos pueden dar una idea aproximada de cómo era nuestro código fuente originalmente.

Quizás el mas típico representante de los lenguajes compilados es C.

Lenguajes interpretados


Ciertamente no se puede ejecutar, en ningún caso, un código fuente directamente, es decir la transformación de código a fuente a código máquina, siempre se tiene que dar, pero en este caso se da cada vez que se ejecuta nuestro sistema, puesto que el producto final es el código fuente. El código fuente es lo que distribuiremos y es lo que se configurara en las maquinas destino.

Los lenguajes interpretados son bastante populares en la actualidad, lo que ha impulsado que haya multitud de librerías y frameworks disponibles en internet, además de mecanismos para consumirlos de forma sencilla y directa.

Debido a que la distribución de código fuente es indispensable, son lenguajes propicios tanto para el software libre, así como para aplicaciones de servidor.

Las principales características que encontramos son:

  • Homogeneidad ente distintas plataformas: Debido a que el código fuente es portable, este se ejecuta de igual forma (sin modificaciones), entre los distintos escenarios posibles.
  • Menor necesidad de versiones concretas de sistemas operativos, o componentes externos en particular. Prácticamente son sistemas que se enlazan dinámicamente a dependencias, y generalmente es necesario usar el nombre del componente a usar, sin tener ningún tipo de liga compleja basado en compatibilidad binaria, o semejantes.
  • Debido a que le código, por necesidad, está disponible, siempre es posible hacer un diagnóstico basado en este.
  • Facilidad de despliegue, debido a la sencillez para resolver dependencias, casi siempre consiste en copiar los archivos requeridos a la ruta indicada.
  • Clásicamente la ejecución es más lenta que en los sistemas compilados, pero los lenguajes interpretados modernos tienen mecanismos para garantizar que esta lentitud es solo apreciable la primera vez que se ejecuta el sistema, disminuyendo el tiempo drásticamente en ejecuciones posteriores.

Algunos ejemplos son:

  • PHP
  • Ruby
  • Python


Lenguajes de compilación intermedia


Algunos lenguajes caen en un punto intermedio, son lenguajes compilados, siendo el producto final un ejecutable en código máquina, pero dicho código maquina no es de una plataforma en particular, sino de una máquina que no existe físicamente. Es lo que se conoce como una máquina virtual.

Este código intermedio debe ser interpretado por cada plataforma destino para poder ejecutarse. ¿Cuáles la ventaja que tenemos entonces con este tipo de lenguajes?


  • Sistemas Multiplataforma
  • Homogeneidad
  • Rapidez aceptable

Al programar para una máquina virtual, nuestro código maquina no está ligado a ninguna plataforma en particular, no tiene dependencias particulares de hardware. El tamaño en memoria de las variables básicas, y su estructura esta definida y siempre es la misma (independientemente si el procesado es de 32 o 64 bits). Adicionalmente el código intermedio de la máquina virtual esta tan próxima a los modelos tradicionales de máquinas físicas que la traducción es bastante rápida. Así tenemos los mejor de los lenguajes compilados y los lenguaje interpretados.

Lenguajes de este tipo son por ejemplo C# y Java



Según el "Tipado"


Todas la variables en memoria de un programa, tiene evidentemente un tipo, es decir son cadenas de texto, numero enteros, decimales, fechas, o cualquier otro tipo de estructura o tipo.

Ahora bien, las variables son espacios de memoria, una secuencia de bytes, lo que le da identidad realmente es lo que "creemos" que hay en ese espacio de memoria. El "creemos", hace referencia al tipo de variable que esta apuntado a ese espacio de memoria, si el tipo de variable que está apuntándolo es un entero, supondremos que el espacio de memoria al que apunto es un entero, si es una cadena supondremos que es una cadena.

El como el lenguaje permite gestionar el "tipo" de las variables es lo que llamaremos su "tipeado", según mas restricciones tenga, mayor y más fuerte será el tipeado, una tipeado mayor nos garantizara que solo variables de un tipo apunten a espacios de memoria donde este tipo, por ejemplo garantiza que si nuestra variable es un entero, solo pueda apuntar a un espacio de memoria que haya un entero.


Lenguajes estáticos y dinámicos



  • Los lenguajes estático son aquellos en los que una vez definido el tipo de una variable, dicha variable siempre será del mismo tipo, no pueden apuntar en un momento a un entero y al siguiente a un cadena de texto por ejemplo, esto es propio de los lenguajes compilados.
  • Los lenguajes dinámicos, son aquellos en que el tipo al que puede apuntar una variable puede cambiar, por ejemplo en este caso en un momento puede apuntar a un entero y al siguiente a una cadena de texto, este comportamiento es típico de los lenguajes interpretados.

Hay que tener en cuenta un punto, algunos lenguajes orientados a objetos (sobre todo los modernos), tienen un clase antecesora común para todas las demás clases, generalmente se llama Object. Un objeto de tipo Object, pudiera apuntar a cualquier variable, en cualquier momento. Esto nos puede hacer pensar que el lenguaje es dinámico, pero no es cierto, simplemente están involucrados mecanismos de herencia pero un objeto de tipo Object, apunta a un Object, para usarlo como un entero, o una cadena de texto, obligatoriamente debemos hacer un cast.

Lenguajes de tipado débil


Es cuando se conoce de que tipo es una variable, pero es posible hacer que dicha variable apunte a un espacio de memoria, donde no este un valor de dicho tipo, esto puede generar un error o no en tiempo de ejecución (aunque lo más problema es que por lo menos genere un comportamiento extraño y no deseado).

Lenguajes de tipeado fuerte


Los lenguajes de tipeado fuerte, son aquellos que tienen un estricto control sobre el tipo de variable y el tipo de contenido a la que están apuntando, si están apuntado a un tipo incorrecto el programa no compilaría, si están en tiempo de ejecución se da la circunstancia que una variable apuntara a un tipo incorrecto, el programa de detendría generando un error (en vez de los tipados débiles, que continuaría haciendo cosas "raras", hasta que el fallo fuera demasiado grande).

JavaScript, tipado débil y dinámico



Analicemos el siguiente código en JavaScript, para ver cómo se comportan las variables (que son de tipado débil y dinámicas). El código es simple suma:

Ejemplo JavaScript
 
1
2
3
4
5
6
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
 
 
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
La salida será 3.

Ahora bien, por error podemos asignar el valor "hola" a la variable "a" y volver a realizar la suma, esto no generara ningún error (ni al compilar, ni la ejecutar), pero si una circunstancia inesperada

Ejemplo JavaScript con errores
 
01
02
03
04
05
06
07
08
09
10
11
12
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
//Ahora bien imaginemos que asignamos el valor "hola" a "a"
//Nos permite hacerlo sin problemas
a="hola "
c= a + b;
// En este caso el resultado sera "hola 2"
console.log(c)
 
 
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
//Ahora bien imaginemos que asignamos el valor "hola" a "a"
//Nos permite hacerlo sin problemas
a="hola "
c= a + b;
// En este caso el resultado sera "hola 2"
console.log(c)
La salida será hola 2 lo cual posiblemente no tenga ningún sentido.

C, tipado débil y estático




El siguiente código en C, muestras un ejemplo de un lenguaje de tipado débil y declaración de variables estáticas, esto es que forzosamente debo declarar el tipo de las variables, pero estas pueden a apuntar a espacios de memoria, que no tuvieran ese tipo en particular.

El código del siguiente ejemplo es válido, compila y no generar errores al ejecutarse, sin embargo, estoy convirtiendo enteros a floats, y string, sin que posiblemente me dé cuenta de ello, por lo que pudiera tomarse como un error de dedo y los resultados son "extraños" (nótese que nunca se interrumpe el programa).

Ejemplo C con código incorrecto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
int main()
{
    int a=120;
    //Imprimo a como un numero, es valor es 50;
    printf ("El valor de a es '%d' \n",a);
     
    //imprimo a como un float se imprime un valor
    //pero no es 50,y no hay ningun fallo
    printf ("El valor de a es '%f'\n",a);
     
    //Lego el valor de a del teclado como cadena de texto
    printf ("Introducta el valor de a: ");
    scanf("%s",&a);
    printf ("El valor de a es '%d' \n",a);
     
}
 
 
int main()
{
    int a=120;
    //Imprimo a como un numero, es valor es 50;
    printf ("El valor de a es '%d' \n",a);
    
    //imprimo a como un float se imprime un valor
    //pero no es 50,y no hay ningun fallo
    printf ("El valor de a es '%f'\n",a);
    
    //Lego el valor de a del teclado como cadena de texto
    printf ("Introducta el valor de a: ");
    scanf("%s",&a);
    printf ("El valor de a es '%d' \n",a);
    
}
La salida del código es:
El valor de a es '120'
El valor de a es '0.000000'
Introduzca el valor de a: hola
El valor de a es '1634496360'

A pesar que no tiene sentido, el programa funciona y continua su ejecución, sin percibir nada extraño.

C#, tipado fuerte y estático




C# es un lenguaje de fuerte tipeado y estático, es decir las variables tienen un tipo específico y solo pueden apuntar a un valor de dicho tipo.

Veamos el siguiente ejemplo_

Ejemplo C# con código incorrecto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=(string) a;
        }
    }
}
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=(string) a;
        }
    }
}
Genera el siguiente error:

(16:22) Cannot convert type 'int' to 'string'

Esto es porque no se puede convertir un entero a una cadena de texto, un variable de tipo string, no puede apuntar a un valor de tipo entero.

Para hacerlo posible, habría que convertir explícitamente el valor entero a una cadena de texto, básicamente esta conversión consiste en crear una nueva dirección de memoria donde este una cadena de texto y no un entero (con lo que tendríamos dos direcciones de memoria, la del entero y la cadena de texto).

Ejemplo C# correcto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=a.ToString();
        }
    }
}
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=a.ToString();
        }
    }
}
A veces pensamos que los lenguajes en los que especificamos explícitamente el tipo de la variable son estáticos y los que no dinámicos, pero esto no es siempre cierto, veamos el siguiente código:

Ejemplo C# incorrecto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var a=20;
            a="hola";
        }
    }
}
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var a=20;
            a="hola";
        }
    }
}

Nos generara el error

(16:15) Cannot implicitly convert type 'string' to 'int'

Aquí usamos la cláusula var, para crear variable y le asignamos el valor "20" esto lo convierte en una variable de tipo entero, y nunca podrá cambiar su tipo, ni apuntar a otro espacio de memoria donde no haya un entero, las sentencia var a=20, equivale a int a=20, la declaración del tipo es estática y en tiempo de compilación.

Ruby, tipado fuerte y dinámico




Es común suponer que si un lenguaje es interpretado debe ser no de tipado débil, pero esto no siempre es así, por ejemplo veamos el siguiente código de Ruby (lenguaje interpretado):

Ejemplo C# incorrecto
 
1
2
3
a=10
b="2"
c=a+b
 
 
a=10
b="2"
c=a+b

Nos generar el error:

String can't be coerced into Integer (repl):3:in `+' (repl):3:in `<main>'

Porque no es posible convertir explícitamente la cadena de texto en un entero, para corregir el código, hacemos las siguientes modificaciones:

Ejemplo C# incorrecto
 
1
2
3
a=10
b="2"
c=a+b.to_i
 
 
a=10
b="2"
c=a+b.to_i
Esta vez nos da el resultado 12.

Como vemos para iniciar una variable no hay ninguna palabra reservada, solo debemos asignarle un valor, pero Ruby si tiene ligado el tipo de la variable a su contenido, de forma que no se pueden convertir directamente, ni usar un tipo como otro, sin sé que se indique.




Si te ha gustado la entrada, ¡Compártela! ;-)

Nota: Puedes encontrar todo el código fuente de este artículo en https://github.com/jbautistamartin/ParadigmasTiposLenguajes

sábado, 30 de marzo de 2019

Paradigmas y tipos de lenguajes informáticos (1 de 3)

La informática es una carrera constante entre el software y el hardware pero no es una carrera competitiva, los avances de hardware, hace que queremos tener más servicios, más funcionalidad, mas automatización… provocan un desarrollo de las inquietudes con respecto al desarrollo de nuevas funcionalidades y la velocidad en la que somos capaces de desarrollarlo. Un cambio de paradigma, nuevas herramientas, y métodos en la construcción de software, provocan una obsolescencia en el hardware, Nuestros sistemas vistosos, amigables e interactivos, son cada vez más lentos, los que hace que necesitamos avanzar en nuevos dispositivos cada vez más potentes y versátiles.


La carrera de los lenguajes de programación no da enormes brincos pero si pasos constantes y rápidos, cada lenguaje es una evolución de los anteriores, una mejora sobre la forma y el proceso, por eso a veces es difícil poder catalogarlos. Cada lenguaje tiene características de los anteriores y se podría situar entre un tipo y otro, solo en casos muy específicos es posible una clasificación clara, en esta entrada (y las siguientes) a ver distintas formas de clasificar un lenguaje.

Según el “nivel”


El nivel, hace referencia a la “distancia” que tienen el lenguaje con respecto a al hardware. Un nivel más bajo implica instrucciones directas al hardware, un nivel más alto implica una abstracción (y desconocimiento) mayor con respecto al hardware en particular que corre nuestro hardware.


Lenguajes de bajo nivel


Los lenguajes de bajo nivel ejecutan instrucciones directamente en el hardware destino, y son extremadamente dependientes del mismo hardware, de forma que un lenguaje para un hardware, suele ser exclusivo para este (o para una familia de hardware en específico si así lo decide el fabricante).

En este punto está el código binario, instrucciones en unos y ceros que ejecuta el procesador directamente, y un poco por encima el lenguaje ensamblado. Hay que recordar que en su forma más primitiva el lenguaje ASM son solos nemotécnicos que son convertidos directamente en código binario. Cada procesador y arquitectura tiene su propio lenguaje ASM.

Ejemplos:

  • Assembly

Lenguajes medios


Son lenguajes que si tienen estructuras claras de control, y nos alejan de conceptos tales como registros y microprocesadores, pero todavía dependemos mucho de conceptos que tiene que ver más con como organiza la computadora la memoria, que con como diséñanos los algoritmos.

Estos lenguajes tienen las siguientes características:

  • Delegan la gestión de la memoria completamente al codificador, él debe reservar la memoria, y liberarla.
  • Además no existe ninguna restricción en cuanto al acceso de memoria, que generalmente es por punteros y libre (lo cual puede provocar que el programa falle).
  • No existe ningún control sobre si estamos asignando un tipo de valor correcto a una variable, ni siquiera si es esta variables se está almacenando en el lugar de memoria adecuado.
  • Generalmente los parámetros se pasan por valor (y no por referencia), para modifica parámetros (tener parámetros de salida) sería necesario pasar direcciones de memoria.

Ejemplos:

  • El ejemplo más representativo es C

Lenguajes de alto nivel


Son lenguajes más independientes de la estructura y características del hardware y mas asociados a la forma de pensar del programador o a las necesidades de negocio a resolver.

Tienen las siguientes características:

  • Una gestión de memoria automática, que suele contar con un recolector de basura.
  • Comprobación automática de tipos y de datos, de forma que es casi imposible compilar si se hay un error de asignación de tipos y en el caso que no se puede compilar el sistema generaría un error que haría que se detuviera (en los lenguajes anteriores el programa seguiría ejecutándose sin generar ninguna advertencia y trabajando con datos corruptos).
  • Tiene sentencias de control de errores claras y sencillas de usar
  • No solo se limita al uso de sentencias condiciones como “if”, sino que tiene estructuras complejas como bucles de diversa índole (for, foreach, while, do until).
  • Generalmente tienen algún mecanismo de reutilización sencilla de código, ya sea a través de los mismos archivos de código fuente (referenciándolos), algún tipo de librería (dll, jar), componentes de algún tipo (COM, ActiveX), o servicios de alguna índole (Servicios SOAP, REST, etc).
  • Lo más importante. Debido a que están más enfocados a facilitar la resolución de las necesidades operativa, se basan más en la implementación de algoritmos sin tener una relación en particular del hardware, lo que hace que sean sencillos de aprender y además, sin que los conozcamos, el análisis de un programa realizado en estos lenguajes es sencillo de comprender.

Ejemplos:

En la actualidad abundan los ejemplos de lenguajes de alto nivel, como los siguientes:

  • C#
  • Java
  • Ruby

Según la generación


En esta clasificación se crean generaciones, en la que se aprecia a través del tiempo, la evolución de los lenguajes. Cada generación es más abstracta (con respecto al hardware) y más sencilla de manejar, con “menos” hacemos “más”.

A quien ocurre un curioso fenómeno, el “conservadurismo” del programador, personas que se afianzaron en una generación (o tecnología) en particular, ensalzando las virtudes de sus lenguajes y rechazando los avances que les sigue porque son “poco óptimos”, a saber "Es mejor programar en ensamblador, porque C es lento”, después “Es mejor programar en C, porque C++ es lento” , “el mejor lenguaje para programar aplicaciones empresariales es COBOL”, posteriormente “Java es el mejor lenguaje de programación empresarial y siempre va a ser vigente”, y esto se va repetir para los lenguajes actuales y los futuros. Hay que comprender que nuestro mercado es cambiante, abarca multitud de actividades humanas y seguramente en el futuro próximo abarque más todavía. Es necesario el cambio y la adaptación, si hay algo mejor, no podemos quedarnos en nuestra zona de confort, necesitamos actualizarnos y adaptarnos, para poder suplir con suficiente rapidez las necesidades de nuestro negocio.

A continuación las diferentes generaciones (las fechas con aproximadas):

Lenguaje de primera generación (años 40 y 50)


Aquí tenemos elementos programables, a través de ceros y unos, código máquina. Las primeras computadoras programables (que no se programaban desplazando elementos hardware), recibían instrucciones a través de códigos en binario, y se programaba directamente hacia el hardware.

Evidentemente cada computadora tenía su propia secuencia de ceros y unos y cada secuencia hacia una cosa diferente, debido a que realmente estas máquinas no podían hacer muchas cosas (Según nuestra perspectiva actual) dicho conjunto de instrucciones era más o menos manejable. Y limitado.


Lenguajes de segunda generación (a partir de los años 50)


La potencia de las computadoras aumento, así como su diversificación, manejar y comprender toda esa secuencia de unos y ceros, se convierte en una tarea colosal, y mantener un programa en una odisea inmensa, llena de dificultades.

Se crea un lenguaje basado en mnemotécnicos llamado ensamblador, cada sentencia corresponde directamente a una secuencia de ceros y uno que comprende el procesador. Así tenemos por un lado un lenguaje basando en sentencias comprensibles por los programadores, y por otro lado fácilmente traducible a código máquina.


Lenguajes de tercera generación (Desde finales de los 50)


Repitamos esto:

“La potencia de las computadoras aumento así como su diversificación, manejar y comprender toda esa secuencia enorme de nemotécnicos, se convierte en una tarea colosal, y mantener un programa en una odisea inmensa llena de dificultades.”

Solo cambiamos algunas palabras, pero el enfoque sigue siendo lo mismo, el hardware puede hacer más, el software lo debe aprovechar.

Aquí tenemos lenguajes ya más alejados de las sentencias propias de una computadora y mucho más cercanos a una forma de resolver problemas más “humana”.


Las características son:

  • Lenguajes más comprensibles, incluso aunque no se conozca el lenguaje en sí, muy parecidos al inglés.
  • Una mayor estructuración del código, se comienza a dividir el código en segmentos que son reusables.
  • Los lenguajes suelen tener un propósito general.
  • Al ser más alejados de las instrucciones de la CPU, es más sencillo crear lenguajes que son más fácilmente traducibles entre plataformas distintas.

Algunos ejemplos son:

  • Pascal
  • C
  • Fortran
  • COBOL
  • C++

Lenguajes de 4 Generación (A partir de los 70, pero con un impacto realmente importante a partir de mediados de los 90)


Se acentúa las necesidades comerciales y diarias con respecto al desarrollo de software y la automatización de cualquier mercado imaginable. La informática llega a nuestros hogares y empresas.

Es necesario crear software, crearlo adaptable y funcional pero sobre todo rápido.


Los lenguajes en esta generación tienen las siguientes características:

  • Frecuentemente, aunque pueden (y suelen) tener la etiqueta multipropósito, si tienen un propósito en específico, como la creación de software empresarial. Cuando este propósito es muy específico, tal que solo sirven para atacar un ámbito en particular, se llaman Domain Specific Languages (DSL)

    Los DSL son por ejemplo SQL (para la generación de consultas), HTML o CSS (para la generación de páginas de internet)… en su momento JavaScript, podría considerarse un DSL (para agregar dinamismo a una página estática), pero ha evolucionado tanto y se puede usar de formas tan variadas, que sería un error colocarlo como DSL.

  • Hay realmente un énfasis especial en el software empresarial, como tal hay también un gran auge en la conexión y uso de bases de datos.
  • El reusó de los componentes es extremadamente común y pueden ser componentes gráficos en muchas ocasiones, complementándose con poderosos IDE. Además suelen estar disponibles por diversos medios y ser fácilmente accesibles (como por MAVEN, gemas de Ruby o NuGet).
  • Se delegan en arquitecturas basadas en servicios, en los que hay una aplicación consumidora y un servidor que procesa las peticiones.
  • Se cuenta con multitud de herramientas que generan código automáticamente.
  • Los lenguajes suelen ser multiplaforma, independientes de la arquitectura de las maquinas donde corren.

Ejemplos

  • La arquitectura .NET (C#, VB.NET).
  • SQL
  • HTML, CSS (Si, aunque te parezca extraño son DSL)




Si te ha gustado la entrada, ¡Compártela! ;-)

Nota: Puedes encontrar todo el código fuente de este artículo en https://github.com/jbautistamartin/ParadigmasTiposLenguajes

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.