Categoría: Programación

  • Programación Reactiva y Sistemas Reactivos – Transformando el Desarrollo de Software

    Programación Reactiva y Sistemas Reactivos – Transformando el Desarrollo de Software

    Este enfoque no solo redefine la forma en que concebimos la interacción entre componentes, sino que también desencadena una revolución en la capacidad de respuesta, la resiliencia y la elasticidad de nuestras aplicaciones.

    Imaginen un equipo de jugadores colaborando para alcanzar un objetivo común, donde cada componente desempeña un papel crucial. Este escenario, similar a la dinámica de un Sistema Reactivo, ilustra cómo los elementos individuales pueden operar de forma independiente o en sincronía para lograr un resultado conjunto. Pero, ¿qué distingue a un Sistema Reactivo? Es la clave de su funcionamiento: la interacción dinámica entre componentes, que puede ser tan fluida como individual o tan armoniosa como colectiva.

    En este blog, exploraremos los sistemas reactivos y la programación reactiva. Desentrañaremos los conceptos esenciales, desde las herramientas y metodologías de diseño hasta los principios de implementación que constituyen la columna vertebral de estos sistemas. Además, desafiaremos la confusión común entre los términos “Sistema Reactivo” y “Programación Reactiva”, destacando sus diferencias clave y sus roles cruciales en el desarrollo de software.

    Prepárense para sumergirse en el universo de la reactividad, donde la capacidad de respuesta, la resiliencia y la elasticidad son mucho más que simples características; son los cimientos de un enfoque revolucionario en la creación de software para entornos distribuidos. ¡Bienvenidos a la nueva era de la programación reactiva y sistemas reactivos!

    Sistemas reactivos

    Un sistema reactivo se refiere a un conjunto de técnicas de diseño y principios utilizados en arquitecturas distribuidas. Se compone de herramientas, metodologías de diseño y procedimientos de implementación. Donde los componentes trabajan juntos para lograr un objetivo común. Lo que diferencia a un sistema reactivo a un sistema por defecto es la interacción entre los componentes, pudiendo operar de manera individual o en armonía para lograr un resultado conjunto. Recuerda el post de microservicios.

    En el contexto de diseño y desarrollo de software, a menudo se usan indistintamente los términos “Sistema Reactivo” y “Programación Reactiva”, aunque no son exactamente lo mismo. Un Sistema Reactivo está asociado con la comunicación de procesos distribuidos a través de la red, mientras que la Programación Reactiva es generalmente basada en eventos y manejada localmente.

    Un Sistema Reactivo se caracteriza por ser “responsivo” (responder a solicitudes en tiempo razonable), “resiliente” (mantener la respuesta incluso en caso de fallos) y “elástico” (capaz de adaptarse a cargas variables). Además, utiliza la comunicación de mensajes asincrónica entre componentes para lograr una interacción suelta, aislamiento de responsabilidades y transparencia en la ubicación.

    La clave que distingue a un sistema reactivo es su enfoque en el procesamiento de flujos, utilizando un mecanismo de paso de mensajes asincrónico y no bloqueante entre componentes. Se centra en el procesamiento de flujos para lograr una mayor capacidad de respuesta, resiliencia y elasticidad en entornos distribuidos.

    programación reactiva

    Es una práctica alineada con el flujo de datos y la propagación de cambios, y puede ser utilizada para construir un sistema reactivo. Se centra en manejar de manera efectiva flujos de datos asíncronos, permitiendo que los cambios en los datos se propaguen automáticamente a través del modelo de ejecución subyacente.

    Se enfoca en gestionar flujos de datos como eventos de teclado, cambios en campos HTML, solicitudes HTTP, actualizaciones de datos, entre otros. Cuando ocurren cambios en un componente, una biblioteca o framework reactivo propaga automáticamente esos cambios a otros componentes, manteniendo una reacción dinámica en el flujo de datos.

    A diferencia de la programación imperativa, donde los hilos se comunican de manera sincrónica, Reactive Programming no requiere esperar y puede realizar otras tareas mientras espera que un recurso esté disponible. Esto reduce el riesgo de que el sistema se bloquee, mejora la utilización eficiente de recursos y mantiene la capacidad de respuesta del sistema. Uno de los lenguajes que mas facilita el trabajo con hilos es GO, te dejo un post donde hacemos una  introducción.

    En el contexto de Reactive Programming, la asincronía implica que el procesamiento de mensajes o eventos ocurre en algún momento futuro. Esta naturaleza asíncrona y no bloqueante es especialmente útil en entornos de aplicaciones donde los recursos son compartidos, ya que no es necesario detener la ejecución mientras un recurso está ocupado en otro lugar.

    En los próximos post nos enfocaremos en la programación reactiva, para ver sus principios y llegar a implementarla, así que suscríbete, deja tus comentarios y dudas.


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • Explorando las Estructuras de Datos en Python: Tuplas y Diccionarios

    Explorando las Estructuras de Datos en Python: Tuplas y Diccionarios

    Continuando nuestra ruta de estructuras de datos en Python, el segundo tipo de estructura de datos que se puede utilizar para almacenar una colección es una tupla.

    Tuplas

    A diferencia de las listas, las tuplas son estructuras de datos inmutables (de solo lectura). Las tuplas consisten en varios elementos rodeados por paréntesis ().

    Al igual que las listas, los elementos dentro de una tupla pueden ser de diferentes tipos. También permiten que sus elementos sean tipos de datos complejos. Por lo tanto, puede haber tuplas anidadas.

    vocales=('a','e','i','o','u')
    print(vocales[1])
    # 'e'
    
    print(vocales[2:]}")
    # ('i','o','u')
    
    #Tupla anidada
    mi_typla_anidada=('a',1,(3,True),8)

    Como vemos, podemos aplicar el mismo slicing que hicimos para las listas.

    Se recomienda usar estructuras de datos inmutables (como las tuplas) en lugar de estructuras de datos mutables (como las listas) siempre que sea posible, debido a cuestiones de rendimiento. Especialmente al tratar con grandes conjuntos de datos, las estructuras de datos inmutables son considerablemente más rápidas que las mutables. Cuando se pasa una estructura de datos a una función como inmutable, no es necesario crear una copia, ya que la función no puede modificarla. Esto se llama transparencia referencial y mejora el rendimiento. Aunque la capacidad de cambiar elementos de datos en listas tiene un costo, se debe analizar cuidadosamente si es realmente necesario.

    Diccionarios

    Almacenar datos como pares clave-valor es crucial, especialmente en algoritmos distribuidos. En Python, esta colección se conoce como un diccionario.

    Seleccionar una clave adecuada es esencial para identificar datos de manera eficiente durante el procesamiento. Las claves deben ser de tipos hashables(valor único y constante durante su vida útil) para garantizar su unicidad y rapidez en las búsquedas. Ejemplos de tipos hashables incluyen números enteros, cadenas de texto y tuplas.

    Los valores pueden ser de cualquier tipo, incluso listas o diccionarios anidados. Para crear un diccionario simple, solo se requiere encerrar los pares clave-valor en llaves {}.

    personaje ={
      "nombre": "Juanito",
      "apellido": "Escarcha",
      "elemento": "Agua"
    }

    Para obtener un valor asociado a una clave, se utiliza la función get o simplemente utilizar la clave como índice. Veamos como:

    personaje.get('nombre')
    
    personaje['apellido']

    Para actualizar un valor asociado a una clave lo usamos de manera similar al anterior ejemplo y “seteamos” su valor:

    personaje['elemento']="Fuego"
    print(personaje)
    
    # {'nombre': 'Juanito', 'apellido': 'Escarcha', 'elemento': 'Fuego'}

    Cuando iteramos un diccionario en Python, a menudo necesitamos tanto las claves como los valores. Fíjate en el siguiente ejemplo:

    for k,v in personaje.items():
        print(k,'->',v)
    
    # nombre -> Juanito
    # apellido -> Escarcha
    # elemento-> Fuego

    Para eliminar un elemento de un diccionario, utilizaremos la función del.

    del personaje['elemento']
    print(personaje)
    
    # {'nombre': 'Juanito', 'apellido': 'Escarcha'}

    Sets

    Estrechamente relacionado con un diccionario está un conjunto, que se define como una colección desordenada de elementos distintos que pueden ser de diferentes tipos. Una de las formas de definir un conjunto es encerrar los valores entre llaves { }. Si un valor esta duplicado en este conjunto lo omitirá.

    colores={'blanco','negro'}

    Por ahora hemos llegado al final, pero aun nos falta estructuras por revisar, así que suscríbete y comparte este blog con tus compañeros de programación y amigos interesados en Python.

    ¡Gracias por ser parte de nuestra comunidad! ¡Esperamos con entusiasmo tus comentarios y contribuciones!


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • Explorando las Estructuras de Datos en Python: LISTAS

    Explorando las Estructuras de Datos en Python: LISTAS

    Las estructuras de datos en memoria desempeñan un papel crucial al almacenar datos temporales durante la ejecución. La elección de las estructuras de datos adecuadas es fundamental para lograr implementaciones eficientes. Algunas clases de algoritmos son recursivas o iterativas en lógica y requieren estructuras de datos especialmente diseñadas para ellas. Por ejemplo, un algoritmo recursivo puede implementarse de manera más sencilla, exhibiendo un mejor rendimiento, si se utilizan estructuras de datos anidadas. En este post, exploraremos las estructuras de datos en el contexto de los algoritmos, centrándonos en las que ofrece Python. Sin embargo, los conceptos presentados aquí son aplicables a otros lenguajes como Java y C++.

    Python y sus Tipos de Datos Integrados

    En cualquier lenguaje, las estructuras de datos son fundamentales para almacenar y manipular datos complejos. En Python, las estructuras de datos son contenedores de almacenamiento que permiten gestionar, organizar y buscar datos de manera eficiente. Estas estructuras se utilizan para almacenar grupos de elementos de datos denominados colecciones que deben ser almacenados y procesados conjuntamente.

    Listas

    Una secuencia ordenada, posibilidad de anidamiento y mutabilidad de elementos

    Una lista es una estructura de datos que permite almacenar una secuencia de elementos que pueden ser modificados. La característica principal de las listas es que los elementos no necesitan ser del mismo tipo. Puedes definir una lista encerrando los elementos entre corchetes [] y separándolos por comas. Por ejemplo, el siguiente código crea una lista con cuatro elementos de tipos diferentes:

    mi_lista = [19, 'Python', 1.76, True]

    Indexación de Listas: La posición de un elemento es determinística en una lista, el índice se puede utilizar para obtener un elemento de una posición específica, ten en cuenta que Python es un lenguaje de indexación desde cero.

    print(mi_lista[2])
    # [1.76]

    Slicing de listas: Obtener un subconjunto de los elementos de una lista especificando un rango de índices se llama slicing.

    Al realizar un slicing de una lista, el rango se indica de la siguiente manera: el primer número (inclusivo) y el segundo número (exclusivo). También contamos con índices negativos, los cuales cuentan desde el final de la lista.

    print(mi_lista[0:2])
    # [19, 'Python']
    
    print(mi_lista[2:])
    # [1.76, True]
    
    print(mi_lista[:3])
    # [19, 'Python', 1.76]
    
    print(mi_lista[:-1])
    # [19, 'Python', 1.76]
    
    print(mi_lista[-2:-1])
    # [1.76]

    Anidamiento: Ya que un elemento de una lista puede ser de cualquier tipo de dato podemos anidar listas. Para algoritmos iterativos y recursivos, esto proporciona capacidades importantes.

    mi_lista = [19, 'Python', [12, 'a', 'b'], 1.76, True]

    Iteración: Python permite iterar sobre cada elemento de una lista utilizando un bucle for. Si quieres profundizar tengo un post sobre estructuras de programación.

    lenguajes = ['Python', 'Java', 'GO', 'COW']
    for lenguaje in lenguajes:
        print(lenguaje)
    
    # Python
    # Java
    # GO
    # COW

    Añadir elementos con append(): Cuando deseas insertar un nuevo elemento al final de una lista, utilizas el método append(). Si la lista ya está en su capacidad máxima, Python extiende la asignación de memoria.

    lenguajes.append('C#')
    print(lenguajes)
    
    # ['Python', 'Java', 'GO', 'COW', 'C#']

    Eliminar elementos con pop(): Para extraer un elemento de la lista, se llama este método que extrae el elemento especificado (o el último elemento si no se proporciona un índice). Los elementos situados después del elemento extraído se reposicionan para mantener la continuidad de la memoria:

    lenguajes.pop()
    print(lenguajes)
    
    # ['Python', 'Java', 'GO', 'COW']

    El método range(): Se utiliza para autocompletar secuencias de números en una lista. Su uso es simple, podemos utilizarla especificando la cantidad de elementos que queremos en la lista, por defecto comienza desde cero e incrementa de uno en uno, pero esto es parametrizable.

    lista = range(5)
    print(lista)
    
    # [0, 1, 2, 3, 4]
    
    lista2 = range(3,20,2)
    print(lista2)
    
    # [3, 5, 7, 9, 11, 13, 15, 17, 19]

    Las listas son compañeras esenciales para cualquier desarrollador, facilitando la manipulación y gestión de datos, pero la aventura no termina aquí. ¡Prepárense para nuestro próximo destino: las tuplas! Las tuplas son otro concepto emocionante que exploraremos en nuestro próximo post.

    ¿Tienes preguntas, comentarios o dudas sobre las listas en Python? ¡Déjalos en la sección de comentarios! Estamos aquí para ayudarte y profundizar en estos temas juntos. ¡Hasta la próxima, y no olviden suscribirse para no perderse ninguna entrega de nuestro viaje por el mundo de Python! Happy coding! 🚀🐍


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • Comandos mas utilizados de Git

    Comandos mas utilizados de Git

    Estos son solo algunos ejemplos y hay muchos más comandos y opciones disponibles en Git. Puedes obtener más información sobre cualquier comando específico utilizando git help [comando] o consultando la documentación oficial de Git.

    git init

    git init: Inicia un nuevo repositorio Git.

    git init
    git clone

    git clone [URL]: Clona un repositorio existente, ten en cuenta si usas SSH.

    git clone https://github.com/jhontona/DownDownConnectPro.git
    git add

    git add [archivo]: Agrega cambios al área de preparación (staging). Puedes usar . para indicar que agregue todos los archivos desde la raíz.

    git add .
    git commit

    git commit -m “Mensaje”: Guarda los cambios en el repositorio con un mensaje descriptivo.

    git commit -m "Describo los cambios a subir"
    git status

    git status: Muestra el estado de los archivos en el repositorio. Cuales están pendientes o listos para agregar.

    git status
    git pull

    git pull: Obtiene cambios del repositorio remoto. Opcionalmente puedes indicar el origen y la rama.

    git pull origin master
    git push

    git push: Sube los cambios locales al repositorio remoto. Opcionalmente puedes indicar el origen y la rama.

    git push origin master
    git branch

    git branch: Lista las ramas locales.

    git branch
    git checkout

    git checkout [rama]: Cambia a otra rama. Especificar -b hace que se cree una nueva rama.

    git checkout -b desarrollo
    git merge

    git merge [rama]: Fusiona los cambios de otra rama en la rama actual.

    git merge desarrollo
    git log

    git log: Muestra el historial de commits.

    git log
    git remote

    git remote: Muestra las URL de los repositorios remotos configurados.

    git remote -v
    git fetch

    git fetch: Obtiene cambios del repositorio remoto sin fusionarlos.

    git fetch --all

    Es esencial comprender que cualquier acción incorrecta o malinterpretación de las siguientes opciones puede resultar en la pérdida de horas de trabajo y esfuerzo invertidos en la codificación. Para evitar cualquier inconveniente, se recomienda encarecidamente revisar y comprender los comandos y parámetros asociados antes de proceder.

    La integridad y seguridad de tu código son fundamentales, y el conocimiento preciso de las funciones y herramientas que utilizas es clave para preservar tu trabajo. ¡Recuerda, un pequeño error puede tener grandes consecuencias!

    git reset

    git reset: Deshace los cambios en el área de preparación.

    git reset --soft
    git revert

    git revert [commit]: Crea un nuevo commit que deshace los cambios de un commit anterior.

    git revert HEAD~2

    Recuerda que esta es una guía básica, así que faltan por aprende aun mas comandos y parámetros. Sin embargo es un excelente punto de partida y se listaron los comando mas usados en el día a día de un programador. Si quieres ahondar en uno de los comandos o te surge alguna duda comenta y comparte lo que sabes. Hasta la próxima!


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • Desmitificando los Algoritmos: Diseño de un algoritmo

    Desmitificando los Algoritmos: Diseño de un algoritmo

    Bienvenidos de regreso en esta travesía hacia el corazón de los algoritmos, donde las soluciones matemáticas encuentran su aplicación en problemas del mundo real. Esta es la continuación de un post anterior, donde nos introducíamos a los conceptos base de algoritmos. En este articulo veremos las técnicas y recursos para diseñar un algoritmo.

    Estrategias de Diseño de Algoritmos

    Cuando nos embarcamos en el diseño de algoritmos, nos enfrentamos a tres consideraciones fundamentales para perfeccionar nuestras creaciones:

    •  ¿Este algoritmo produce el resultado esperado?
    • ¿Es esta la manera más óptima de obtener estos resultados?
    • ¿Cómo se desempeñará el algoritmo con conjuntos de datos más grandes?

    Comprender la complejidad del problema es crucial antes de diseñar la solución. Clasificamos los algoritmos según las siguientes categorías, orientadas a las características del problema:

    Algoritmos Intensivos en Datos

    Diseñados para manejar grandes cantidades de datos con requisitos de procesamiento relativamente simples. Ejemplos incluyen algoritmos de compresión aplicados a archivos enormes. En estos casos, el tamaño de los datos supera la memoria del motor de procesamiento, y se debe desarrollar un diseño de procesamiento iterativo para manejar eficientemente los datos.

    base de datos

    Algoritmos Intensivos en cálculos

    Requieren un procesamiento considerable pero no implican grandes cantidades de datos. Un ejemplo simple sería un algoritmo para encontrar un número primo muy grande. La clave para maximizar el rendimiento de estos algoritmos radica en dividirlos en fases que puedan paralelizarse.

    Algoritmos Intensivos tanto en Datos como en Cálculos

    Abordan grandes cantidades de datos y tienen considerables requisitos de procesamiento. Ejemplos notables son los algoritmos para realizar análisis de sentimientos en transmisiones de video en vivo. Estos algoritmos, los más exigentes en recursos, necesitan un diseño cuidadoso y una asignación inteligente de recursos disponibles

    La dimensión de los datos

    Al abordar las dimensiones de volumen, velocidad y variedad (los 3Vs), nos adentramos en la esencia misma de cómo los algoritmos enfrentan desafíos del mundo real.

    • Volumen: El Tamaño Esperado de los Datos El volumen representa el tamaño esperado de los datos que el algoritmo procesará. Desde conjuntos modestos hasta datos masivos, entender el volumen es esencial para diseñar algoritmos que se adapten a la escala requerida.
    • Velocidad: La Tasa de Generación de Nuevos Datos La velocidad mide la tasa esperada de generación de nuevos datos al utilizar el algoritmo. Desde procesos por lotes hasta procesos en tiempo real, la velocidad de los datos varía. Imagina un proceso en tiempo real, como la recopilación de feeds de video en vivo; aquí, la velocidad alcanza su máxima complejidad.
    • Variedad: La Diversidad de Tipos de Datos La variedad cuantifica cuántos tipos diferentes de datos se espera que maneje el algoritmo. Desde datos estructurados hasta no estructurados, la variedad desafía al diseño del algoritmo a abordar diferentes formatos y contextos de datos.

    Por ejemplo, en la dimensión de velocidad, tenemos el procesamiento por lotes como el más simple, seguido del procesamiento periódico y luego el procesamiento casi en tiempo real. Finalmente, tenemos el procesamiento en tiempo real, que es el más complejo de manejar en el contexto de la velocidad de los datos.

    Al caracterizar la dimensión informática, se analizan las necesidades de procesamiento del problema en cuestión. Estas necesidades determinan el diseño más eficiente para un algoritmo. Por ejemplo, los algoritmos complejos suelen requerir una gran potencia de procesamiento, lo que puede hacer necesario contar con una arquitectura paralela de varios nodos. En el caso de algoritmos modernos de aprendizaje profundo, que implican un procesamiento numérico considerable, puede ser necesario utilizar la potencia de unidades de procesamiento gráfico (GPUs) o unidades de procesamiento tensorial (TUPs).

    En futuros post veremos como calcular esta complejidad para poder abordar la creación de algoritmos.  No olvides dejar tus comentarios e inquietudes y nos vemos en el próximo post.


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • En las entrañas de la Arquitectura de Microservicios en Desarrollo de Software

    En las entrañas de la Arquitectura de Microservicios en Desarrollo de Software

    En el anterior post nos introdujimos a los microservicios, en este post exploraremos a fondo los beneficios que los microservicios ofrecen, nos sumergiremos en los desafíos que plantea esta arquitectura innovadora y desvelaremos las claves para alcanzar el éxito en su implementación. Desde la agilidad en el desarrollo hasta la gestión efectiva de la complejidad, abordaremos temas cruciales como la optimización de recursos, la resiliencia en situaciones de fallo y las mejores prácticas para una implementación exitosa de microservicios.

    Beneficios de los microservicios

    1. Mayor velocidad en desarrollo: La arquitectura de microservicios acelera todos los procesos de desarrollo al permitir una compilación y tiempo de construcción más rápidos. Esto es clave para la agilidad en el desarrollo.

    2. Despliegues rápidos y tamaño reducido: Al desplegar cada parte del sistema de manera independiente, el tamaño de implementación se reduce significativamente. Esto se traduce en despliegues individuales que toman una fracción del tiempo comparado con aplicaciones monolíticas.

    3. Programación de despliegues personalizada: La arquitectura de microservicios resuelve el problema de seguir un calendario de despliegue estándar. Cada servicio puede ser desplegado de manera independiente, siguiendo su propio cronograma.

    4. Monitoreo fino y personalizado: Servicios críticos pueden ser monitoreados de manera más detallada que otros, permitiendo un control más preciso y chequeos adicionales. Permite identificar servicios claves y cuellos de botella.

    5. Pruebas automatizadas independientes: Cada microservicio puede realizar pruebas automatizadas configurables como parte del proceso de construcción y despliegue. Esto reduce el tiempo necesario para realizar pruebas en toda la aplicación.

    6. Soporte para varios lenguajes de programación: Ya no es necesario ejecutar una aplicación como un solo ejecutable. Esto permite implementar diferentes partes del sistema utilizando tecnologías diversas, adaptándose a cada problema de manera óptima. Esto lo logramos a partir de definir interfaces de comunicación.

    7. Escalabilidad más sencilla y económica: Los microservicios son más fáciles y a menudo más baratos de escalar horizontalmente. Cada parte del sistema puede escalarse de manera independiente, evitando los requisitos pesados de recursos de las aplicaciones monolíticas. Aunque cuidado, por que al seguir escalando nuestra aplicación se puede volver incluso mas costosa de mantener ya que necesitamos orquestadores y pagar por mantenibilidad de cada servicio, pero ampliaremos esto en la siguiente sección de contras.

    8. Flexibilidad en el hardware: La división de la aplicación a menudo significa reducir los requisitos de hardware para la mayoría de las partes del sistema, brindando más opciones al elegir hardware o proveedores de servicios en la nube.

    9. Aislamiento efectivo de fallos: El desacoplamiento de servicios proporciona un mecanismo de seguridad eficiente para prevenir problemas mayores en caso de fallas parciales del sistema.

    10. Facilidad de entendimiento: Los servicios son más fáciles de entender y mantener debido a tamaños de código más pequeños. Además, suelen estar asociados a procesos completos lo que puede hacer que contrastemos la funcionalidad con la experiencia de un especialista en el proceso.

    11. Optimización de costos: Ejecutar la mayoría de los componentes de la aplicación en instancias de menor costo en comparación con instancias monolíticas de alto recurso puede resultar en ahorros significativos ya que podemos potenciar los servicios críticos a demanda.

    12. Desarrollo distribuido: La eliminación del acoplamiento entre componentes contribuye a lograr más independencia en el desarrollo de código, beneficiando a equipos distribuidos.

    13. Facilidad de refactorización: Es mucho más fácil realizar refactorizaciones en microservicios debido al menor alcance de cambios y procesos de lanzamiento y prueba independientes.

    14. Libertad tecnológica: Cambiar a nuevas tecnologías es más fácil con la arquitectura de microservicios, ya que cada servicio es más pequeño y estructuralmente independiente.

    15. Toma de decisiones independiente: Los desarrolladores son libres de elegir lenguajes, bibliotecas y herramientas que mejor se adapten a sus necesidades, fomentando la toma de decisiones distribuida.

    Desafíos de la Arquitectura de Microservicios

    Veamos de cerca algunas de las posibles complicaciones y limitaciones que podríamos enfrentar al adoptar esta innovadora arquitectura.

    1. Mayor uso de recursos: Al tener múltiples componentes en lugar de compartir el mismo espacio de proceso, la necesidad de comunicación entre ellos aumenta, generando mayor carga en la red. Esto se traduce en más tráfico, latencia y uso de E/S. Además, la carga total de CPU y RAM también es mayor debido a la ejecución independiente de cada componente. Todo esto repercute finalmente en los costos de implementación y mantenibilidad.

    2. Dificultades en la depuración: Diagnosticar y depurar suele ser más difícil con varios servicios. Por ejemplo, si varios servicios procesan una solicitud que falla, un desarrollador debe acceder a los registros de varios servicios para entender la causa de la falla.

    3. Necesidad de pruebas de integración complejas: La separación de un sistema implica la construcción de un conjunto extenso de pruebas de integración y otros controles automatizados que monitorizan la compatibilidad y disponibilidad de cada componente.

    4. Consistencia en transacciones: La dispersión de datos en las aplicaciones de microservicios dificulta la realización de cambios transaccionales y atómicos en el sistema.

    5. Divergencia en versiones y librerías: Los servicios pueden utilizar diferentes versiones de bibliotecas, algunas de las cuales pueden ser incompatibles u obsoletas. Esto complica las actualizaciones del sistema y la solución de problemas, incluyendo correcciones de vulnerabilidades de software.

    6. Deuda técnica en un sistema distribuido: La deuda técnica en el software se refiere a compromisos o decisiones que los desarrolladores toman durante el proceso de desarrollo de software que, a corto plazo, aceleran la entrega de un producto pero generan problemas a largo plazo. Estos compromisos pueden incluir la implementación de soluciones rápidas y poco robustas, la omisión de pruebas exhaustivas, el uso de código desactualizado o el incumplimiento de estándares de codificación. Abordar la deuda técnica es más complicado en un sistema distribuido donde cada componente es propiedad de un equipo diferente y suele convertirse en una bola de nieve hasta que los problemas son mas que evidentes.

    7. Observabilidad: Gestionar múltiples aplicaciones implica desafíos adicionales en la recopilación y uso de eventos del sistema, como registros, trazas y métricas. Los desarrolladores deben asegurarse de que todos estos datos estén disponibles para análisis, incluyendo información contextual necesaria para depurar problemas entre los servicios.

    8. Duplicación de funcionalidades: En entornos de desarrollo altamente distribuidos, no es raro tener múltiples componentes realizando roles similares en el sistema. Es crucial establecer límites claros y decidir de antemano qué roles específicos se asignan a cada componente.

    9. Propiedad y responsabilidad: La propiedad se vuelve crucial cuando varios equipos mantienen y desarrollan componentes independientes. Es fundamental definir contratos de propiedad claros para abordar solicitudes de desarrollo, problemas de seguridad y mantenimiento.

    Como hemos ilustrado, el modelo de microservicios tiene sus desafíos, pero ser conscientes de estos problemas y abordarlos de manera proactiva es clave para el éxito.

    Claves para el éxito en el desarrollo ¿Cómo implementar Microservicios?

    Hemos explorado sus beneficios, desafíos comunes y ahora es momento de resumir las claves esenciales para aprovechar al máximo el modelo de microservicios en el desarrollo de aplicaciones.

    1. Introducción gradual: Evita implementar la arquitectura de microservicios demasiado pronto si el producto aún está poco definido o podría experimentar cambios significativos. Comienza con una aplicación monolítica y divídela a medida que se definan claramente las capacidades comerciales y los límites, reduciendo así la carga de trabajo y estableciendo interfaces adecuadas entre los componentes. También podrías definir servicios muy generales y claramente identificables.

    2. Modelo de negocio único: La decisión de adoptar microservicios debe basarse en diversos factores, como el tamaño y distribución del equipo, así como la geografía. Un equipo local pequeño puede sentirse cómodo trabajando con una aplicación monolítica, mientras que un equipo distribuido geográficamente podría beneficiarse enormemente al dividir la aplicación en múltiples microservicios para lograr mayor flexibilidad. Identifica procesos y como funcionan estos a nivel de negocio, puede que tu aplicación se parezca a otra pero no significa que debes copiar al detalle.

    3. Planificación para escenarios de falla: Dado que en la arquitectura de microservicios hay numerosas interacciones entre los componentes a través de llamadas remotas y eventos, se incrementa la posibilidad de diversas fallas. Construye el sistema considerando todos los posibles escenarios de falla y las diferentes formas de abordarlos.

    4. Abrazar la automatización: Con componentes más independientes, se requieren verificaciones más estrictas para lograr una integración estable entre los servicios. La inversión en una automatización sólida es crucial para lograr un alto grado de confiabilidad y asegurar que todos los cambios sean seguros para ser implementados.

    5. Modelo de negocio vs jerarquía: Aunque es común dividir la aplicación en servicios basados en la estructura organizativa, donde cada equipo es responsable de su propio servicio, esto solo funciona bien si la estructura organizativa se alinea perfectamente con las capacidades de negocio de los microservicios. En lugar de seguir un modelo servicio-por-equipo, define dominios y procesos de negocio claros alrededor de los cuales se estructura el código.

    6. Enfoque en pruebas de integración: Asegúrate de tener pruebas integrales para las integraciones entre tus microservicios, realizándose automáticamente.

    7. Mantener la compatibilidad hacia atrás: Recuerda mantener tus cambios compatibles hacia atrás para garantizar que las nuevas modificaciones sean seguras para implementar. Lo mas aconsejable es construir sobre las definiciones que ya tienes para que se mantenga la retrocompatibilidad.

    A medida que desvelamos los secretos detrás de esta arquitectura de desarrollo, recordemos que el futuro tecnológico nos espera con infinitas posibilidades. Así que, ¡prepárense para abrazar la era de los microservicios y conquistar nuevos horizontes en el desarrollo de software! No olvides dejar tu comentario y dudas ¡Hasta el próximo encuentro!


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • Descubriendo el Mundo de los Microservicios en Desarrollo de Software

    Descubriendo el Mundo de los Microservicios en Desarrollo de Software

    La arquitectura de microservicios ha emergido como una piedra angular para muchas empresas en todo el mundo. Este enfoque revolucionario ha transformado la manera en que concebimos y construimos aplicaciones, permitiendo una mayor flexibilidad y escalabilidad. En este artículo, exploraremos a fondo qué es exactamente la arquitectura de microservicios y cómo ha llegado a ser la elección predeterminada para el desarrollo de software en numerosas compañías.

    ¿Qué es un Microservicio?

    El término servicio se refiere a un conjunto de funciones o tareas que un programa o aplicación proporciona para cumplir con una o varias funciones específicas. Estos servicios son a menudo parte de una arquitectura más amplia y se diseñan para realizar tareas específicas de manera eficiente y modular. Aquí hay algunas características clave de los servicios en programación:

    1. Funcionalidad específica: Un servicio generalmente se centra en una funcionalidad específica o conjunto de funciones relacionadas. Por ejemplo, un servicio puede encargarse de gestionar la autenticación de usuarios, procesar pagos, o proporcionar acceso a bases de datos.

    2. Interfaz definida: Los servicios suelen tener una interfaz claramente definida que especifica cómo se pueden acceder y utilizar. Esto se puede lograr mediante API (Interfaz de Programación de Aplicaciones) o mediante protocolos de comunicación específicos.

    3. Independencia: Los servicios son diseñados para ser independientes y modularizados. Esto significa que pueden ser desarrollados, probados y actualizados de manera independiente, lo que facilita la gestión y mantenimiento del sistema completo.

    4. Comunicación: Los servicios a menudo se comunican entre sí o con otras partes del sistema. La comunicación puede ocurrir a través de diversos mecanismos, como llamadas a API, mensajes, o servicios web.

    5. Reusabilidad: Dado que los servicios están diseñados para realizar funciones específicas, pueden ser reutilizados en diferentes partes de una aplicación o incluso en diferentes aplicaciones. Esto promueve la eficiencia en el desarrollo y la mantenibilidad del código.

    6. Escalabilidad: La arquitectura basada en servicios facilita la escalabilidad de una aplicación, ya que los servicios pueden ser escalados de manera independiente según las necesidades de carga de trabajo específicas.

    7. Tecnologías: Los servicios pueden implementarse utilizando diversas tecnologías, como microservicios, servicios web, funciones en la nube, entre otros, dependiendo de los requisitos del proyecto.

    Ahora, si hablamos específicamente de microservicios estamos hablando de un servicio aun mas especializado, es decir que al momento de planificar nuestra arquitectura debemos procurar identificar los procesos para generar sus abstracciones de manera clara pero simple; pongamos un ejemplo para entenderlo mejor: Podemos tener un servicio que se encargue de la gestión de usuarios,  en donde tenemos procesos como registro, autenticación, obtener perfil y actualizar datos, ahora esto se podría separar en dos microservicios, por una parte podríamos tener un microservicio encargado de lo que tiene que ver con el registro y autenticación y el otro de los datos de sesión o perfil; claro que esto no es así tan fácil, esto requiere un análisis concienzudo, entender los procesos y que encaje con nuestra arquitectura empresarial. Cualquier servicio puede ser tratado como microservicio siempre y cuando tenga cohesión lógica y estructural.

    ¿Qué es una arquitectura de Microservicios?

    Muy fácil, es un enfoque de diseño de software que organiza una aplicación como un conjunto de servicios pequeños e independientes. Cada servicio se centra en una tarea específica y puede ser desarrollado, implementado y escalado de manera independiente. Estos servicios se comunican entre sí a través de interfaces bien definidas.

    Aquí hay algunas características clave de la arquitectura de microservicios:

    1. Descomposición en servicios: En lugar de construir una aplicación monolítica, la arquitectura de microservicios divide la funcionalidad en servicios más pequeños, cada uno ejecutando un proceso independiente. Cada servicio se enfoca en realizar una tarea específica dentro del dominio de la aplicación.

    2. Independencia y escalabilidad: Cada servicio es independiente y puede ser desarrollado, implementado y escalado de manera autónoma. Esto permite a los equipos de desarrollo trabajar de manera más eficiente y facilita la escalabilidad, ya que solo los servicios que necesitan más recursos pueden ser escalados.

    3. Comunicación a través de APIs: Los microservicios se comunican entre sí a través de APIs bien definidas. Esto significa que cada servicio expone una interfaz clara que especifica cómo otros servicios pueden interactuar con él. La comunicación puede realizarse de manera síncrona o asíncrona, según los requisitos.

    4. Despliegue independiente: Los microservicios pueden ser desplegados de forma independiente, lo que facilita la implementación continua y la actualización de servicios sin afectar toda la aplicación. Esto mejora la agilidad del desarrollo y reduce el tiempo de inactividad.

    5. Resiliencia y tolerancia a fallos: La arquitectura de microservicios fomenta la resiliencia al diseñar servicios que son autónomos y pueden manejar fallas de manera aislada. Si un servicio falla, no debería afectar a los demás servicios en la aplicación.

    6. Poliglota: Cada servicio puede ser desarrollado en el lenguaje de programación más adecuado para su tarea específica. Esto permite a los equipos utilizar las tecnologías más apropiadas para cada servicio, según sus necesidades.

    7. Gestión de datos descentralizada: Cada servicio puede tener su propia base de datos, y la gestión de datos se realiza de manera descentralizada. Esto evita la dependencia de una única base de datos monolítica.

    8. Orientación al dominio de negocio: Los microservicios se organizan en torno a las capacidades del negocio, lo que facilita la comprensión y mantenimiento del sistema. Sobre como entender y trabajar el dominio del negocio te deje unos post de arquitectura empresarial.

    Ejemplo en Archimate de una arquitectura de microservicios

    Las empresas a nivel mundial han adoptado tan ampliamente el modelo de arquitectura de microservicios que casi se ha convertido en la forma estándar de desarrollo de software. Estas compañías cuentan con decenas, cientos e incluso miles de microservicios a su disposición.

    En pocas palabras, la arquitectura de microservicios organiza una aplicación como una colección de servicios, llamados microservicios, cada uno de los cuales es responsable de una parte específica de la lógica de la aplicación, generalmente definida por una capacidad comercial particular.

    Tomemos como ejemplo una aplicación de mercado en línea. La aplicación puede tener múltiples funciones, como búsqueda, carrito de compras, pagos, historial de pedidos y muchas más. Cada función puede ser tan diferente que el código puede (y en algunos casos, debería) ser completamente independiente del resto de la aplicación. En este ejemplo, la búsqueda y los pagos técnicamente no tienen nada en común. En el modelo de arquitectura de microservicios, cada componente sería un servicio independiente que desempeña su propio papel en el sistema.

    Desafíos… Separar la Aplicación en Servicios:

    Organizar cada parte de la aplicación como un servicio separado no es necesariamente un requisito. Al igual que con cualquier modelo de arquitectura o aspecto del desarrollo de software, los ingenieros deben ser cuidadosos al elegir un enfoque o solución particular, realizando un análisis inicial y comprendiendo la solución en las condiciones dadas.

    El dilema monolítico

    Para entender la motivación detrás de la arquitectura de microservicios, primero, veamos el enfoque opuesto: las aplicaciones monolíticas o “monolitos”. Estas aplicaciones son construidas y ejecutadas como un solo programa, con una arquitectura simple que no implica dividir la aplicación en partes independientes.

    Ventajas de los Monolitos:

    • Código Compacto: Al no dividir la aplicación, el código base es más pequeño, evitando complejidades asociadas a la comunicación entre componentes.

    • Flexibilidad en la Lógica de la Aplicación: Permite cambios estructurales o lógicos fácilmente en las etapas iniciales de desarrollo, donde la agilidad es clave.

    • Alcance Reducido: No todos los servicios necesitan descomposición; servicios simples pueden permanecer integrales.

    ¿Cuándo los monolitos se vuelven problemáticos?

    • Tamaño y Despliegues Lentos: A medida que la aplicación crece, el tiempo de construcción, inicio y despliegue puede volverse excesivamente lento.

    • Imposibilidad de desplegar partes independientemente: La incapacidad de actualizar partes específicas puede ralentizar el desarrollo y lanzamiento.

    • Amplitud de impacto: Errores en una función afectan toda la aplicación, generando posibles problemas significativos.

    • Cuello de botella en escalabilidad vertical: A medida que la aplicación crece, es difícil escalarla aún más debido a limitaciones en CPU y RAM.

    La arquitectura de microservicios surge cuando los monolitos se vuelven demasiado grandes. Los desarrolladores enfrentan problemas como tamaños de aplicación masivos, despliegues lentos y dependencias no deseadas entre componentes. Aquí es donde entran los microservicios.

    En el siguiente post, exploraremos cómo la división de la aplicación en microservicios aborda estos problemas y qué aspectos debes tener en cuenta. ¡Mantente atento para descubrir cómo esta arquitectura puede revolucionar tu enfoque de desarrollo! No olvide dejar tus comentarios e inquietudes. Hasta la próxima.


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • ¿Qué es GO? Descubriendo las Maravillas de Golang, mi experiencia de desarrollo

    ¿Qué es GO? Descubriendo las Maravillas de Golang, mi experiencia de desarrollo

    Cada lenguaje tiene su propio encanto y propósito. Sin embargo, uno que ha ganado popularidad rápidamente en los últimos años es Go, también conocido como Golang. Este proyecto de código abierto ha capturado la atención de desarrolladores por sus características únicas y su enfoque eficiente. En este artículo, nos sumergiremos en el fascinante mundo de Go, explicando por qué deberías considerar aprender este lenguaje de programación, destacando sus características más prominentes y poniendo especial atención en las famosas gorutinas, que para mi fue el gancho que me atrapo.

    Go: Más Allá del Código Abierto

    Go no es simplemente un lenguaje de programación, es un proyecto de código abierto respaldado por algunas de las mentes más brillantes en el mundo de la informática. Su sintaxis, similar a la de C, facilita la transición para aquellos familiarizados con este último. Además, Go emplea tipado estático, proporcionando seguridad y eficiencia en el código.

    ¿Es orientado a objetos?

    En la programación orientada a objetos (POO), la controversia sobre la esencia de la herencia y las clases sigue siendo tema de debate. Algunos expertos sostienen que estos elementos no son fundamentales, sino simplemente herramientas de implementación, por ejemplo Javascript desafían la norma al basarse en prototipos en lugar de clases.

    Por otro lado, Go, el lenguaje de programación desarrollado por Google, adopta un enfoque distinto al permitir un estilo de programación orientado a objetos sin recurrir a clases. En lugar de herencia, Go favorece el patrón de composición, donde las estructuras se utilizan para construir jerarquías sin soporte explícito para subclases. Con conceptos como interfaces de tipo implícito y la flexibilidad de métodos generales, Go redefine la experiencia de la programación orientada a objetos, ofreciendo una alternativa fresca y poderosa a los tradicionales paradigmas de herencia y clases. ¡Explorar estas innovaciones en la POO es todo un viaje; al principio me pareció intimidante pero luego vez la practicidad y logras entender el flujo de trabajo. Así que si esperas clases como tal, aquí no existen, pero se pueden aplicar los principios SOLID sin problema.

    Compilación Cruzada y Eficiencia Nativa

    Uno de los rasgos distintivos de Go es su capacidad de compilación cruzada de manera nativa. Al igual que sus compañeros C y C++, los binarios de Go son portables y se pueden ejecutar en diferentes plataformas sin necesidad de recompilación. Esto no solo facilita la distribución del software, sino que también contribuye a la eficiencia del desarrollo. En otras palabras, implica compilar un programa en el entorno de desarrollo que puede diferir del entorno en el que se ejecutará el programa final, imaginémoslo como generar aplicaciones portables.

    La compilación cruzada es útil en situaciones donde el hardware de destino no es accesible desde el entorno de desarrollo, o cuando se busca optimizar el rendimiento del código para una arquitectura específica. Este enfoque es común en el desarrollo de sistemas embebidos, dispositivos móviles y otros escenarios donde la portabilidad del código es esencial. En mi caso lo he probado para optimizar microservicios, algo que me ha parecido una locura al utilizar la gorutinas, vamos a ver que son.

    Gorutinas: La Magia de la concurrencia en Go

    Una de las razones fundamentales para sumergirse en el mundo de Go es su enfoque único en la concurrencia a través de las gorutinas. Estas son unidades ligeras de ejecución que permiten realizar tareas simultáneas de manera eficiente. A diferencia de otros lenguajes, Go facilita la creación y gestión de múltiples hilos de ejecución sin complicaciones, lo que resulta en un código más rápido y menos propenso a bloqueos. La verdad suena simple y es que es así, Go lo implementa de una manera genial, además de proveernos herramientas para trabajar de manera asíncrona y evitar bloqueos de lectura o escritura.

    Orientado a la Eficiencia en Sistemas Múltiples

    Go está especialmente diseñado para aprovechar sistemas con múltiples procesadores y procesamiento en red. Su capacidad para gestionar eficientemente la concurrencia lo convierte en una opción destacada para aplicaciones distribuidas y servicios en la nube. Si buscas un lenguaje que se adapte a la velocidad y la escalabilidad, Go es tu elección.

    Dinamismo y Flexibilidad: Duck Typing en Go

    Aunque Go utiliza tipado estático, sorprendentemente admite la tipificación dinámica de datos, en lugar de basarse en la herencia o en la implementación de una interfaz específica, el Duck Typing se centra en el comportamiento de un objeto. Si un objeto puede realizar las acciones requeridas, es considerado adecuado, sin importar su tipo. Esto proporciona flexibilidad en el desarrollo, permitiendo adaptarse a cambios en tiempo de ejecución sin sacrificar la seguridad del tipo.

    Delegación, Polimorfismo y Elegancia en la Programación

    Go ofrece una implementación única de la programación orientada a objetos. Aunque carece de herencia de tipos, permite la delegación a través de valores embebidos y el polimorfismo mediante interfaces. La definición de tipos se realiza de manera clara y concisa, fomentando un código elegante y fácil de entender.

    En resumen, aprender Go no solo significa adquirir habilidades en un nuevo lenguaje de programación, sino sumergirse en una experiencia de desarrollo única. Con su enfoque en la eficiencia, la concurrencia sencilla y la portabilidad nativa, Go se ha ganado su lugar entre los lenguajes de programación más destacados. Y ahora solo me queda invitarte a que me acompañes a sumergirnos en este lenguaje, por que créeme que es super genial optimizar cada segundo de respuesta. Es una barbaridad. Espero estés atento al blog por que subiré mas contenido de GO y claro deja tus dudas, y cometarios.


    ¡Conviértete en un experto tecnológico! 🚀 Suscríbete a nuestro newsletter y recibe las últimas noticias, análisis y tendencias directamente en tu bandeja de entrada. No te pierdas las actualizaciones que harán que tu experiencia tecnológica sea aún más emocionante. ¡Únete a nuestra comunidad hoy! 📧✨

  • Spring Boot – Configuración

    Spring Boot – Configuración

    Vimos anteriormente que se puede hacer la inyección de dependencias por medio de archivos xml y usando anotaciones; sin embargo también podemos usar clases de configuración para instanciar objetos de manera fácil y limpia.

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
     
    @Configuration
    public class AppConfig {
         
        @Bean("student")
        public Student getStudent(Service service) {
            return new StudentImpl(service);
        }
         
        @Bean("service")
        public Service getService() {
            return new ServiceImpl();
        }
    }

    La ventaja de este método es que permite liberar a las clases de negocio de anotaciones, dejándolas limpias.

    La clave es marcar la clase con el decorador @Configuration, es cual es procesado por el Spring IoC, que en este caso crea las instancias marcadas con @Bean.

    Veamos un ejemplo muy básico:

    1. Creamos una interfaz con un método de prueba:
      public interface Service {
          String hello();
      }
    2. Creamos la implementación del servicio:
      public class ServiceImpl implements Service {
          @Override
          public String hello() {
              return "Hola";
          }
      }
    3. Creamos el archivo de configuración:
      @Configuration
      public class Config {
          @Bean("service")
          public Service getService() {
              Service myService = new ServiceImpl();
              System.out.println(myService.hello());
              return myService;
          }
      }

      Lo que hacemos es instanciar la implementación e imprimir en consola el String que viene del método hello, lo que resulta que al ejecutar la aplicación veamos un “hola” en consola.

        .   ____          _            __ _ _
       /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
       \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
        '  |____| .__|_| |_|_| |_\__, | / / / /
       =========|_|==============|___/=/_/_/_/
       :: Spring Boot ::                (v2.7.0)
      
      Hola

    Al ser una clase Java obtenemos las ventajas propias del lenguaje, como asegurarnos del tipado correcto, comprobación en tiempo de compilación y algo mas amigable para la lectura.

    También si se desea, podemos usar las anotaciones en conjunto con con las clases de configuración.

  • Spring Boot – Anotaciones

    Spring Boot – Anotaciones

    Segunda parte del curso Spring Boot

    En el anterior post hablamos de los conceptos básicos, y este es uno de los principales, pero merece un espacio propio.

    Tocamos temas sobre inyección de dependencias y beans, sin embargo no hablamos del ciclo de vida de estos componentes, por lo que vamos a empezar por ahí.

    Teníamos nuestro archivo XML para inicializar los beans y dijimos que podemos inicializar varios; en muchos casos estos beans dependen de otros, es decir que debemos inyectar las dependencias y comportamiento. Esto lo logramos usando la interfaz de BeanPostProcessor que nos permite crear objetos y asignar comportamientos antes y después de crear los beans con los metodospostProcessBeforeInitialization y postProcessAfterInitialization respectivamente. Lo que hace Spring es justamente en los anteriores métodos por medio de Reflexion instanciar los objetos necesario y asignar el comportamiento deseado. A partir de Spring 3.5 se implemento la autodetección de beans, es decir que ya no es necesario usar un XML para inyectar cada uno de nuestros beans. Esto se logra a través de las anotaciones.

    ¿Qué es una anotación?

    Una anotación es un metadato que se incrusta en el código, se caracterizan por iniciar con el carácter @, este no cambia el comportamiento del código pero si puede indicarle al compilador acciones especificas de lectura o ejecución, por ejemplo, Java por defecto tiene varias anotaciones, una de las mas conocidas es @Override que se utiliza para asegurar que un método heredado se sobrescribe. Así como ésta hay muchas mas y además se pueden crear anotaciones propias.

    Spring se vale de anotaciones para indicarte la la interfaz BeanPostProcessor como instanciar los beans. En el siguiente ejemplo busca la anotación @MyAnnotation:

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Method[] methods = bean.getClass().getDeclareMethods();
        for (Method get Bean: methods) {
            if (method.getAnnotation(MyAnnotation.class) != null) {
                Class[] args = method.getParameterTypes();
                try {
                    Object obj = context.getBean(args[0]);
                    method.invoke(bean, obj);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    Spring provee unas anotaciones por defecto, vamos a ver las principales.

    Anotaciones y componentes

    @Component

    Esta anotación le indica a Spring Boot que debe tratar la clase con el decorador como un bean, es decir que el propio motor se encargará de hacer la inyección.

    import org.springframework.stereotype.Component;
    
    @Component
    public class MyClass {

    @Repository, @Service y @Controller son especializaciones de @Component.

    @Repository

    Se utiliza para la capa de persistencia, en otras palabras, clases que interactúan con la base de datos. Tiene la particularidad de que tiene un control de errores mas especifico para interactuar con la sintaxis de base de datos, causando un mejor manejo de estos objetos.

    @Service

    Este se utiliza en la capa de servicio, es decir que se espera que sea lógica de negocio.

    @Controller

    Esta anotación nos enlaza con la capa de presentación, allí solemos colocar las urls que se exponen vía web.

    @Controller
    @RequestMapping("students")
    public class StudentController {
    
        @GetMapping("/{id}", produces = "application/json")
        public @ResponseBody Student getStudent(@PathVariable int id) {
            return studentService.getStudentById(id);
        }
    }

    Esta anotación se suele usar mucho en los servicios web junto con la anotación @ResponseBody que permite mapear nuestra respuesta a un objeto JSON.

    Ya que este tipo de comportamiento es el mas común, desde la versión 4.0 de Spring se creo una nueva anotación que los incluye a ambos:

    @RestController

    @RestController
    @RequestMapping("students")
    public class StudentController {
    
        @GetMapping("/{id}", produces = "application/json")
        public Student getStudent(@PathVariable int id) {
            return studentService.getStudentById(id);
        }
    }

    @Autowired

    Esta anotación nos sirve para indicarle que vamos a inyectar un bean dentro de otro, es decir como una propiedad o también lo podemos usar en los métodos set; esto quiere decir que podemos darle el control al framework para que cree la instancia de los componentes, esto es importante ya que solo podemos utilizar esta anotación para inicializar componentes definidos en el contexto de Spring, es decir, que tengan el decorador @Component o alguna de sus especializaciones.

    En el siguiente ejemplo se muestra la instanciación de un servicio como propiedad de otro bean(controlador):

    @Service
    public class StudentService {
    @RestController
    @RequestMapping("students")
    public class StudentController {
    
        @Autowired
        private StudentService studentService;
    
        @GetMapping("/{id}", produces = "application/json")
        public Student getStudent(@PathVariable int id) {
            return studentService.getStudentById(id);
        }
    }

    Con esta anotación nos surge un problema que vamos a ilustrar a continuación:

    @Service
    public class StudentService implements PersonService {
    @Service
    public class TeacherService implements PersonService {

    La idea es que queremos inicializar una instancia de la interfaz PersonService, en este caso hay dos posibles orígenes y @Autowired no sabría cual implementar, para solucionar esto podemos usar la anotación @Qualifier de la siguiente manera:

    @Service
    @Qualifier("student")
    public class StudentService implements PersonService {
    @Service
    @Qualifier("teacher")
    public class TeacherService implements PersonService {
    @RestController
    @RequestMapping("registration")
    public class RegistrationController {
    
        @Autowired
        @Qualifier("student")
        private PersonService personService;
    
        @GetMapping("/info/{id}", produces = "application/json")
        public Person getDataPerson(@PathVariable int id) {
            return personService.getBasicDataById(id);
        }
    }

    De esta manera podemos indicarle específicamente que tipo implementar. En el anterior ejemplo obtenemos la instancia del servicio StudentService.

    @Value

    Esta anotación nos sirve para inyectar valores en los campos de un constructor o método. En el siguiente ejemplo se obtiene un valor del archivo .properties y se le asigna a una variable. Podemos ver el @Value asignado a diferentes tipos de datos e incluso establecemos un valor por defecto.

    @Service
    @Qualifier("teacher")
    public class TeacherService implements PersonService {
    
        @Value("${university.class.code:default}")
        private String code;
    
        @Value("${university.class.subjects}")
    private String[] subjects;

    @Scope

    El Scope o ámbito indica el alcance de una instancia, en Spring tenemos por defecto la instancia de los beans como singleton (se crea una única instancia del beans, es decir que las siguientes instancias del mismo bean hacen referencia al único creado); como se mencionó por defecto funciona así, aunque si queremos especificarlo se hace de la siguiente manera:

    @Bean
    @Scope("singleton")
    public University university() {
        return new University();
    }

    En el anterior post también mencionamos que se pueden establecer los beans por medio de xml, para ese caso también podemos definir el scope.

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="personService" class="com.jhontona.service.PersonServiceImpl" scope="singleton" />
        <!--También podemos usar singleton="true" -->
    </beans>

    También podemos definir el alcance como prototype, el cual crea una instancia nueva en cada llamado.

    @Bean
    @Scope("prototype")
    public Teacher teacher() {
        return new Teacher();
    }

    Los anteriores scopes se pueden usar en cualquier aplicación de Spring, pero además tenemos scopes específicos para el ámbito web(Web Aware).

    El scope de Request se utiliza para crear una instancia nueva de bean por cada petición http, es decir que su uso mas común es con un @RestController aunque también lo podemos definir para un @Bean.

    @Bean
    @Scope(value = WebApplicationContext.SCOPE_REQUEST)
    public ExampleBean exampleBean() {
        return new ExampleBean();
    }
    @RestController
    @RequestScope
    public class ExampleController {
    
        @GetMapping("/test")
        public String sayHello() {
                return "Hello";
        }
    
    }

    El scope de Session se utiliza para crear una instancia nueva de bean por cada sesión http

    @Bean
    @Scope(value = WebApplicationContext.SCOPE_SESSION)
    public ExampleBean exampleBean() {
        return new ExampleBean();
    }
    @RestController
    @SessionScope
    public class ExampleController {
    
        @GetMapping("/test")
        public String sayHello() {
                return "Hello";
        }
    
    }

    El scope de Global Session se utiliza para crear una instancia nueva de bean por cada sesión http, pero este va orientado a Portlets, es decir aplicaciones que trabajan con módulos reutilizables de este tipo; las características principales de este tipo de aplicación es que dependen de un contenedor especializado en controlar estos módulos, además de que cada pagina puede estar constituida por múltiples portlets y cada una de estos tiene un request y response independiente.

    @Bean
    @Scope(value = WebApplicationContext.SCOPE_GLOBAL_SESSION)
    public ExampleBean exampleBean() {
        return new ExampleBean();
    }

    El scope de Application se utiliza para crear una instancia nueva de bean por cada ServletContext, que por lo general en una aplicación es único(aunque podemos crear mas), comportándose como un singleton.

    @Bean
    @Scope(value = WebApplicationContext.SCOPE_APPLICATION)
    public ExampleBean exampleBean() {
        return new ExampleBean();
    }
    @RestController
    @ApplicationScope
    public class ExampleController {
    
        @GetMapping("/test")
        public String sayHello() {
                return "Hello";
        }
    
    }

    El scope de WebSocket se utiliza para crear una instancia nueva de bean por cada websocket.

    @Bean
    @Scope(scopeName = "websocket")
    public ExampleBean exampleBean() {
        return new ExampleBean();
    }

    Tenemos un montón de anotaciones, pero las que hemos visto son las de uso mas frecuente o nos ayudan a entender los comportamientos por defecto que toma una aplicación en Spring (si se quiere profundizar mas en estas anotaciones le recomiendo este recurso).

    Anotaciones Javax

    Con las anotaciones propias de Spring tenemos para adaptarnos a una gran variedad de situaciones, sin embargo podemos trabajar con anotaciones propias del lenguaje o provistas por paquetes externos, por ejemplo, Javax Annotation API tiene unas anotaciones interesantes que se integran con Spring.

    @PostConstruct and @PreDestroy nos sirve para llamar un método después de la instanciación del bean o antes de su destrucción, es decir que funciona como un callback.

    import javax.annotation.*;
    
    public class ExampleController {
    
        public String sayHello() {
                return "Hello";
        }
    
        @PostConstruct
        public void init(){
            System.out.println("El Bean se acaba de inicializar.");
        }
       
        @PreDestroy
        public void destroy(){
            System.out.println("El Bean se va a destruir.");
        }
    
    }

    Contamos con otras etiquetas como @Resource e @Inject que se comporta de manera muy similar al @Autowired por lo que teniendo alternativas en Spring es mas practico. Simplemente debemos saber que podemos crear o insertar anotaciones desde otros paquetes.

    Hasta aquí llega este post, espero tus preguntas, comentarios y que compartas este contenido.

¡Hola a todos los entusiastas de la tecnología! Quería informarles que en mi blog utilizamos cookies para mejorar la experiencia de usuario. Estas pequeñas herramientas nos ayudan a personalizar el contenido y ofrecer funciones específicas. Al continuar explorando el sitio, aceptas nuestro uso de cookies. Puedes obtener más información sobre cómo las utilizamos en nuestra política de privacidad. ¡Gracias por ser parte de esta comunidad tecnológica! 🍪    Más información
Privacidad