Translate

Mostrando las entradas con la etiqueta Java. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Java. Mostrar todas las entradas

sábado, 14 de diciembre de 2024

LangChain4j: IA Generativa en Java


LangChain4j es un poderoso framework para construir aplicaciones de inteligencia artificial generativa en Java. Inspirado en el popular LangChain para Python y JavaScript, este marco permite integrar modelos de lenguaje como OpenAI GPT, Llama, o Hugging Face con herramientas avanzadas, flujos de trabajo y procesamiento dinámico.

LangChain4j proporciona herramientas para manejar modelos de lenguaje de manera modular y escalable. Algunas de sus características destacadas incluyen:

  • Integración de Modelos de Lenguaje (LLMs): Interactúa fácilmente con GPT-4, GPT-3, o modelos personalizados.
  • Encadenamiento de Operaciones: Diseña flujos complejos combinando varios modelos y herramientas.
  • Contexto Extendido: Utiliza almacenamiento vectorial para manejar contextos largos.
  • Herramientas de Razonamiento y Extracción: Construye aplicaciones como chatbots avanzados, asistentes de búsqueda, o sistemas de recomendación.

Antes de comenzar, agrega la dependencia en tu proyecto Maven o Gradle:


Veamos la dependencia de maven:

<dependency>

    <groupId>com.langchain4j</groupId>

    <artifactId>langchain4j-core</artifactId>

    <version>1.0.0</version>

</dependency>


o gradle:


implementation 'com.langchain4j:langchain4j-core:1.0.0'


Veamos un cliente básico para interactuar con OpenAI GPT:


import com.langchain4j.LangChain;

import com.langchain4j.llm.OpenAiClient;


public class LangChainExample {

    public static void main(String[] args) {

        OpenAiClient client = OpenAiClient.builder()

                .apiKey("tu-api-key")

                .build();


        String response = client.chat("¿Qué es LangChain4j?");

        System.out.println("Respuesta: " + response);

    }

}


Este ejemplo muestra cómo enviar una consulta y recibir una respuesta usando OpenAI GPT.

LangChain4j soporta herramientas como cadenas de procesamiento (`Chains`) para flujos más complejos.


Veamos un ejemplo de un Flujo con Memoria:


import com.langchain4j.chain.ConversationChain;

import com.langchain4j.memory.InMemoryMemory;


public class ConversationExample {

    public static void main(String[] args) {

        ConversationChain conversation = ConversationChain.builder()

                .llm(OpenAiClient.builder().apiKey("tu-api-key").build())

                .memory(new InMemoryMemory())

                .build();


        System.out.println(conversation.chat("Hola, ¿quién eres?"));

        System.out.println(conversation.chat("¿Recuerdas mi nombre?"));

    }

}


Aquí, InMemoryMemory permite que el modelo recuerde las interacciones previas.


LangChain4j admite almacenamiento vectorial, útil para aplicaciones de búsqueda semántica o contextos largos.


import com.langchain4j.vector.PineconeVectorStore;


public class VectorStoreExample {

    public static void main(String[] args) {

        PineconeVectorStore vectorStore = PineconeVectorStore.builder()

                .apiKey("tu-api-key")

                .environment("us-west1-gcp")

                .build();


        vectorStore.add("doc1", "Este es un ejemplo de texto.");

        System.out.println(vectorStore.search("texto relacionado", 1));

    }

}


LangChain4j extiende las capacidades de los modelos de lenguaje para aplicaciones empresariales en Java. Con su enfoque modular, herramientas avanzadas, y soporte para almacenamiento vectorial, se posiciona como una opción clave para proyectos de IA generativa en el ecosistema Java.

Y falto decir que se puede integrar con spring y otros frameworks, pero eso lo voy a dejar para otro post... 

Dejo link: https://docs.langchain4j.dev/


sábado, 9 de noviembre de 2024

GraalVM + sistema operativo = GraalOS


GraalOS es una iniciativa experimental que integra la tecnología de GraalVM directamente en el sistema operativo, permitiendo que las aplicaciones, especialmente las desarrolladas en lenguajes JVM (Java, Scala, Kotlin), se ejecuten de manera más eficiente y directa sobre el hardware. GraalOS busca ser un sistema operativo minimalista y optimizado para ejecutar aplicaciones de alto rendimiento, proporcionando un entorno ideal para microservicios, procesamiento en la nube y aplicaciones en tiempo real.

Las principales características de GraalOS son: 

  1. Soporte Nativo para Lenguajes JVM: GraalOS permite ejecutar código de JVM directamente sobre el sistema operativo sin capas intermedias, ofreciendo un rendimiento nativo para lenguajes como Java, Kotlin y Scala.
  2. Integración con GraalVM: GraalOS está construido sobre la base de GraalVM, lo que permite la compilación AOT (Ahead-of-Time) y el uso de `native-image` para generar binarios nativos que corren eficientemente sobre el hardware.
  3. Ecosistema Multilenguaje: Aunque está optimizado para lenguajes de la JVM, GraalOS también soporta otros lenguajes como JavaScript, Python y R, aprovechando la compatibilidad de GraalVM.
  4. Optimización para Microservicios: GraalOS está diseñado para ejecutarse en contenedores ligeros, ideales para arquitecturas de microservicios y entornos de computación en la nube.

Uno de los puntos fuertes de GraalOS es el uso de la tecnología de compilación Ahead-of-Time (AOT) de GraalVM. La compilación AOT permite que el código de JVM se convierta en código nativo, lo cual mejora significativamente el tiempo de inicio y reduce el uso de memoria.

native-image -jar tu_aplicacion.jar

Este comando convierte un archivo JAR en un binario nativo, optimizado y listo para ejecutarse en GraalOS. Los binarios nativos generados pueden arrancar casi instantáneamente y son ideales para aplicaciones que requieren respuesta en tiempo real.

GraalOS ofrece un entorno perfecto para el despliegue de aplicaciones en la nube gracias a su integración optimizada con GraalVM. Además, permite manejar aplicaciones en tiempo real gracias a su bajo tiempo de respuesta y consumo de recursos. Su diseño minimalista y eficiente hace que sea una opción atractiva para desarrolladores que busquen optimizar costos y rendimiento en entornos de microservicios o serverless.

Aunque GraalOS es experimental, se puede probar en entornos de contenedores o como un sistema operativo en máquinas virtuales para evaluar su rendimiento en aplicaciones específicas. Para comenzar, puedes instalar GraalOS en una máquina virtual y luego utilizar GraalVM para compilar y ejecutar aplicaciones.


apt update && apt install graalos


GraalOS representa un avance en la forma en que interactuamos con el hardware a nivel de sistema operativo para ejecutar aplicaciones nativas. Aunque en sus primeras etapas, su integración con GraalVM abre la puerta a nuevas oportunidades en la ejecución de aplicaciones de alto rendimiento y microservicios en la nube.

Con una promesa de rendimiento optimizado, tiempos de respuesta ultrarrápidos y soporte multilenguaje, GraalOS podría transformar la forma en que desarrollamos e implementamos aplicaciones nativas.

Dejo like : 

https://blogs.oracle.com/java/post/introducing-graalos

https://graal.cloud/

jueves, 31 de octubre de 2024

Le pregunte a la IA : ¿Qué lenguaje tiene las mejores perspectivas de futuro: Python, Java o JavaScript?

Le pregunte a la IA : ¿Qué lenguaje tiene las mejores perspectivas de futuro: Python, Java o JavaScript? y como me gusto mucho su respuesta se las comparto: 

Para evaluar el futuro de Python, Java, y JavaScript, consideremos cómo se están usando, su versatilidad y las tendencias que los impulsan en áreas clave:


Python

   - Ventajas: Python es ampliamente valorado por su legibilidad y simplicidad, lo cual lo hace popular tanto entre principiantes como en sectores avanzados como la ciencia de datos, la inteligencia artificial (IA), y el análisis de datos. Su ecosistema ofrece un gran soporte en estas áreas, con bibliotecas como TensorFlow, Pandas, y NumPy.

   - Perspectivas de Futuro: Su relevancia en IA y ciencia de datos seguirá creciendo, dado que es el lenguaje de referencia en estos campos. Aunque tiene limitaciones de rendimiento comparado con lenguajes de bajo nivel, las mejoras continuas en sus bibliotecas y en plataformas de soporte (como el uso de PyPy y la integración con código en C++) pueden mitigar estas limitaciones. Sin embargo, en áreas que demandan alto rendimiento en tiempo real, como juegos y aplicaciones de alta concurrencia, su uso podría ser limitado.


Java

   - Ventajas: Java es conocido por su estabilidad y robustez, especialmente en entornos empresariales y sistemas a gran escala. La JVM (Java Virtual Machine) permite que las aplicaciones en Java se ejecuten de manera confiable en múltiples plataformas. Java también es ampliamente utilizado en aplicaciones de Android y sigue siendo un estándar en muchas empresas.

   - Perspectivas de Futuro: La madurez y confiabilidad de Java aseguran su presencia en grandes empresas y aplicaciones financieras, que demandan sistemas sólidos y escalables. Recientemente, con mejoras de rendimiento en las versiones más nuevas y características de programación moderna (como pattern matching y records), Java sigue evolucionando y atrayendo a una nueva generación de desarrolladores. Su uso en aplicaciones web modernas y su compatibilidad con entornos de nube son elementos clave que aseguran su vigencia en el mercado.


JavaScript

   - Ventajas: JavaScript es el lenguaje dominante en el desarrollo web, y su versatilidad ha crecido enormemente con el ecosistema Node.js, lo cual permite su uso tanto en el frontend como en el backend. Su capacidad para construir aplicaciones de una sola página (SPA), junto con frameworks como React, Vue, y Angular, le ha dado un rol esencial en la web moderna.

   - Perspectivas de Futuro: La demanda de aplicaciones web interactivas y rápidas respalda el crecimiento de JavaScript. Además, su capacidad de integrarse con **WebAssembly (Wasm)** ha ampliado su uso en aplicaciones de alto rendimiento en el navegador. Node.js sigue siendo relevante para el backend, y con la evolución continua del lenguaje (por ejemplo, ES2022), JavaScript se mantiene competitivo. 


- Python: dominará en ciencia de datos, IA y aplicaciones académicas; su simplicidad asegura su uso entre nuevos programadores.

- Java: mantiene su posición en el desarrollo empresarial y de sistemas complejos; su futuro está asegurado en estos sectores.

- JavaScript: seguirá siendo fundamental en el desarrollo web, y su interoperabilidad con WebAssembly amplía sus horizontes.


En general, la elección del lenguaje con las "mejores perspectivas" depende del campo específico. Para IA y datos, Python; para sistemas empresariales, Java; y para la web y aplicaciones de usuario, JavaScript. Cada uno tiene un rol fuerte y perspectivas sólidas en sus respectivos dominios.


Que piensan? 

miércoles, 25 de septiembre de 2024

Apache Geode: Almacenamiento de Datos Distribuido en Tiempo Real


En el mundo actual, donde las aplicaciones demandan respuestas inmediatas y alta disponibilidad de datos, es crucial contar con herramientas que permitan gestionar datos en tiempo real de manera distribuida. Apache Geode es una de esas soluciones que permiten construir aplicaciones escalables y altamente disponibles.

Apache Geode es un sistema de almacenamiento de datos en memoria distribuido que ofrece capacidades de procesamiento y almacenamiento en tiempo real. Originalmente desarrollado por GemStone bajo el nombre de GemFire, pasó a ser un proyecto de código abierto bajo el paraguas de la Apache Software Foundation. 

Apache Geode se caracteriza por su capacidad para almacenar datos en memoria distribuidos entre varios nodos, permitiendo así un acceso rápido y eficiente a grandes volúmenes de datos. Ofrece baja latencia, alta disponibilidad, y consistencia, lo que lo convierte en una opción ideal para aplicaciones críticas que requieren acceso en tiempo real a los datos.

Apache Geode distribuye los datos entre múltiples nodos (o servidores), formando un clúster donde cada nodo puede almacenar una parte de los datos. De esta forma, el sistema es capaz de escalar horizontalmente a medida que aumenta la demanda. Además, Geode puede replicar los datos entre los nodos para garantizar redundancia y alta disponibilidad.

La arquitectura de Apache Geode permite particionar y replicar datos de manera eficiente. Esto significa que cada partición de datos se almacena en un nodo del clúster, y estas particiones pueden replicarse en otros nodos para evitar pérdida de información en caso de fallos.

Además, Geode ofrece consistencia fuerte, lo que significa que los datos son siempre consistentes entre las réplicas, lo cual es fundamental en entornos de alta disponibilidad.

Características Principales de Apache Geode:

  1. Almacenamiento en Memoria: Apache Geode utiliza la memoria principal de los servidores para almacenar los datos, lo que reduce drásticamente la latencia de acceso en comparación con bases de datos tradicionales basadas en disco.
  2. Distribución y Replicación de Datos: Los datos en Geode se distribuyen entre varios nodos y pueden replicarse para garantizar redundancia y alta disponibilidad.
  3. Alta Disponibilidad y Tolerancia a Fallos: Al replicar los datos en diferentes nodos del clúster, Geode garantiza que los datos estarán disponibles incluso si uno o varios nodos fallan.
  4. Consistencia: Apache Geode asegura consistencia fuerte, es decir, cualquier cambio en los datos es inmediatamente visible en todos los nodos que almacenan copias del mismo dato.
  5. Procesamiento en Tiempo Real: Permite realizar consultas y operaciones sobre los datos en tiempo real, manteniendo la latencia baja incluso en sistemas con altos volúmenes de transacciones.
  6. Soporte para APIs de Java y Spring: Geode está profundamente integrado con Java y tiene un fuerte soporte para el ecosistema Spring, lo que facilita su integración en aplicaciones Java empresariales.
  7. Persistencia: Aunque su principal almacenamiento es en memoria, Geode permite configurar persistencia en disco para asegurar que los datos no se pierdan tras un reinicio o fallo catastrófico.

Y donde podemos utilizar Geode: 

  1. Aplicaciones Financieras: En sistemas de trading y banca, donde la baja latencia y la consistencia de datos son cruciales, Geode se utiliza para garantizar acceso rápido a los datos en tiempo real.
  2. eCommerce: Plataformas de comercio electrónico, donde es necesario manejar grandes cantidades de usuarios concurrentes y transacciones, pueden beneficiarse de la capacidad de escalado y alta disponibilidad de Geode.
  3. Sistemas de Telecomunicaciones: Las redes de telecomunicaciones requieren acceso constante a la información del usuario y deben procesar grandes volúmenes de datos en tiempo real, algo que Apache Geode maneja eficientemente.
  4. Monitorización en Tiempo Real: Para sistemas de monitoreo y análisis en tiempo real, Geode permite el procesamiento y la toma de decisiones rápidas basadas en datos en memoria, sin la necesidad de acceder a discos lentos.

Cuando comparamos a Apache Geode con otras soluciones de almacenamiento en memoria como Redis o Hazelcast, la diferencia radica en el soporte más amplio de Apache Geode para modelos de datos más complejos y la integración nativa con Spring, lo que facilita su adopción en entornos Java empresariales.

Por otro lado, en comparación con bases de datos tradicionales como MySQL o PostgreSQL, Geode ofrece una arquitectura distribuida en memoria, lo que reduce significativamente la latencia de acceso y permite una mayor escalabilidad.

Apache Geode es una herramienta poderosa para desarrollar aplicaciones críticas que requieren almacenamiento distribuido en memoria y acceso en tiempo real a los datos. Su integración con tecnologías como Java y Spring, junto con su capacidad de escalabilidad y alta disponibilidad, lo convierten en una opción excelente para sectores como finanzas, telecomunicaciones, y comercio electrónico.

Dejo link; https://geode.apache.org/

viernes, 20 de septiembre de 2024

Programación Reactiva con Spring


La programación reactiva es un paradigma orientado a manejar flujos de datos de manera asíncrona, lo cual resulta ideal para aplicaciones con alta carga de tráfico, como sistemas de microservicios. Con Spring, la programación reactiva se facilita mediante el módulo Spring WebFlux.

Spring WebFlux es el módulo reactivo de Spring que permite construir aplicaciones no bloqueantes y asíncronas. Está basado en el patrón Reactor, que sigue el estándar de Reactive Streams y utiliza Mono y Flux como las abstracciones principales para manejar uno o varios elementos, respectivamente.

  • Mono: Representa 0 o 1 elemento.
  • Flux: Representa 0 o N elementos.
  • Backpressure: Mecanismo para controlar la velocidad de emisión de los eventos.

Primero, crea un proyecto de Spring Boot en Spring Initializr. Asegúrate de agregar las dependencias de Spring Reactive Web.


<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-webflux</artifactId>

</dependency>


A continuación, implementamos un controlador que responde a peticiones HTTP de manera reactiva usando `Flux` y `Mono`.


import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;


import java.time.Duration;


@RestController

public class ReactiveController {


    // Endpoint que devuelve un solo valor (Mono)

    @GetMapping("/mono/{id}")

    public Mono<String> getMono(@PathVariable String id) {

        return Mono.just("Valor recibido: " + id);

    }


    // Endpoint que devuelve un flujo continuo de datos (Flux)

    @GetMapping("/flux")

    public Flux<Integer> getFlux() {

        return Flux.range(1, 10)

                   .delayElements(Duration.ofSeconds(1));

    }

}


Una vez que hayas implementado el controlador, ejecuta la aplicación de Spring Boot. Puedes acceder a los endpoints:

  • /mono/{id}: Devuelve un único valor. Ejemplo: http://localhost:8080/mono/5
  • /flux: Devuelve un flujo de datos continuo que se emite cada segundo. Ejemplo: http://localhost:8080/flux
Como ventajas podemos ver: 
  • Eficiencia y Escalabilidad: Ideal para manejar aplicaciones que requieren alta concurrencia y asíncronas.
  • Control del Flujo: Con Reactive Streams puedes controlar el ritmo de procesamiento y evitar la sobrecarga del sistema.
  • Menos Bloqueo: No hay hilos bloqueados mientras se esperan respuestas, lo que mejora el rendimiento en sistemas de alta demanda.


viernes, 23 de agosto de 2024

lunes, 19 de agosto de 2024

Se encuentran abiertas las inscripciones para los cursos Gugler!!!

¡Tengo grandes noticias! Estoy emocionado de anunciar que ya están abiertas las inscripciones para los tan esperados cursos Gugler. Si estás buscando avanzar en tu carrera, aprender nuevas habilidades, o simplemente profundizar tus conocimientos en áreas tecnológicas, ¡estos cursos son para ti!







Inscripciones abiertas del segundo cuatrimestre 2024. Inscripciones.gugler.com.ar

lunes, 12 de agosto de 2024

Procedimientos Almacenados y Funciones en H2


H2 es una base de datos relacional escrita en Java que es especialmente popular por su ligereza y facilidad de uso. Aunque es muy común utilizar H2 como una base de datos en memoria para pruebas, también es lo suficientemente potente para manejar operaciones más avanzadas, como procedimientos almacenados y funciones. 

Un procedimiento almacenado es un conjunto de instrucciones SQL que pueden ser almacenadas en la base de datos y ejecutadas cuando sea necesario. Los procedimientos almacenados son útiles porque permiten encapsular lógica compleja en un solo lugar, lo que facilita su mantenimiento y reutilización.

Las funciones son similares a los procedimientos almacenados, pero con algunas diferencias clave. Mientras que un procedimiento almacenado puede ejecutar una serie de comandos SQL y no necesariamente retornar un valor, una función siempre retorna un valor y está diseñada para ser utilizada en expresiones SQL.

En H2, los procedimientos almacenados se crean utilizando la palabra clave `CREATE ALIAS`. Veamos un ejemplo sencillo de cómo crear un procedimiento almacenado que inserta un nuevo registro en una tabla:


CREATE ALIAS insert_user AS $$

void insert_user(Connection conn, String username, String email) throws SQLException {

    PreparedStatement prep = conn.prepareStatement("INSERT INTO users(username, email) VALUES(?, ?)");

    prep.setString(1, username);

    prep.setString(2, email);

    prep.execute();

    prep.close();

}

$$;


Una vez que el procedimiento almacenado ha sido creado, puedes llamarlo usando `CALL`:


CALL insert_user('JohnDoe', 'john.doe@example.com');


Si necesitas que tu procedimiento devuelva resultados, puedes modificarlo ligeramente:


CREATE ALIAS get_user_by_id AS $$

ResultSet get_user_by_id(Connection conn, int id) throws SQLException {

    PreparedStatement prep = conn.prepareStatement("SELECT * FROM users WHERE id = ?");

    prep.setInt(1, id);

    return prep.executeQuery();

}

$$;


Luego puedes utilizar el procedimiento en una consulta:


CALL get_user_by_id(1);


Al igual que los procedimientos, las funciones en H2 se crean utilizando `CREATE ALIAS`, pero la diferencia es que las funciones siempre retornan un valor. Aquí tienes un ejemplo de una función que calcula el IVA de un precio dado:


CREATE ALIAS calculate_vat AS $$

double calculate_vat(double price) {

    return price * 0.21;

}

$$;


Una vez creada, puedes utilizar la función en tus consultas SQL:


SELECT calculate_vat(100) AS vat_amount;


Esto te devolverá el valor con el IVA aplicado.


H2 ofrece un soporte robusto para procedimientos almacenados y funciones, lo que lo hace muy útil incluso en aplicaciones más allá del simple almacenamiento de datos en memoria. Con la capacidad de encapsular lógica SQL compleja en procedimientos y funciones, puedes mantener tu código más limpio y organizado. 


jueves, 25 de julio de 2024

Como crear un compilador que tome un lenguaje personalizado y lo traduzca a bytecode


Crear un compilador que tome un lenguaje personalizado y lo traduzca a bytecode es una tarea compleja pero fascinante. Veamos cómo podríamos abordar este proyecto, usando herramientas y conceptos clave en el desarrollo de compiladores. Vamos a dividirlo en varias etapas:

1. Definir el Lenguaje

   Antes de empezar, necesitas definir tu lenguaje. Esto incluye:

  •    Sintaxis: La estructura del lenguaje, las reglas gramaticales, etc.
  •    Semántica: El significado de las construcciones del lenguaje.

   Por ejemplo, podríamos definir un lenguaje simple con variables, operadores y estructuras de control.


2. Crear una Gramática

Usa ANTLR o una herramienta similar para definir la gramática de tu lenguaje. Esto implica escribir un archivo `.g4` (o el formato correspondiente) que describa la sintaxis de tu lenguaje.

   Ejemplo de una gramática simple en ANTLR (`SimpleLang.g4`):


   grammar SimpleLang;


   program: statement+;

   statement: assignment | expression ';';

   assignment: ID '=' expression ';';

   expression: ID | NUMBER | '(' expression ')' | expression '+' expression | expression '-' expression;

   ID: [a-zA-Z_][a-zA-Z_0-9]*;

   NUMBER: [0-9]+;

   WS: [ \t\r\n]+ -> skip;

   

3. Generar el Lexer y el Parser

Usa ANTLR para generar el lexer y el parser a partir de tu gramática.


   antlr4 SimpleLang.g4


4. Crear un Árbol de Sintaxis Abstracto (AST)

El parser generará un árbol de sintaxis. Sin embargo, es útil convertir esto en un Árbol de Sintaxis Abstracto (AST) que simplifica el manejo de la estructura del código.


5. Transformar el AST a Bytecode

   Para convertir el AST a bytecode, necesitas:

  • Definir un formato de bytecode: Dependiendo de la máquina virtual (como la JVM para Java) o un formato personalizado.
  • Generar bytecode: Escribir código que recorra el AST y genere el bytecode correspondiente.

Aquí hay un ejemplo simple de cómo podrías generar bytecode para una calculadora en Java:


   public class BytecodeGenerator extends SimpleLangBaseVisitor<Void> {

       private final StringBuilder bytecode = new StringBuilder();


       @Override

       public Void visitAssignment(SimpleLangParser.AssignmentContext ctx) {

           String id = ctx.ID().getText();

           visit(ctx.expression());

           bytecode.append("STORE ").append(id).append("\n");

           return null;

       }


       @Override

       public Void visitExpression(SimpleLangParser.ExpressionContext ctx) {

           if (ctx.ID() != null) {

               bytecode.append("LOAD ").append(ctx.ID().getText()).append("\n");

           } else if (ctx.NUMBER() != null) {

               bytecode.append("PUSH ").append(ctx.NUMBER().getText()).append("\n");

           } else if (ctx.op != null) {

               visit(ctx.expression(0));

               visit(ctx.expression(1));

               switch (ctx.op.getType()) {

                   case SimpleLangParser.PLUS:

                       bytecode.append("ADD\n");

                       break;

                   case SimpleLangParser.MINUS:

                       bytecode.append("SUB\n");

                       break;

               }

           }

           return null;

       }


       public String getBytecode() {

           return bytecode.toString();

       }

   }


6. Compilar el Bytecode

Si estás utilizando una máquina virtual existente como la JVM, deberás generar bytecode en el formato adecuado. Si estás creando tu propia máquina virtual, tendrás que diseñar un mecanismo para ejecutar el bytecode.


7. Probar y Depurar

Pruebar el compilador con varios programas de ejemplo para asegurarte de que se comporta como se espera. Depura cualquier problema que encuentres en el proceso de generación de bytecode.


Este es un enfoque básico y simplificado para ilustrar cómo podrías comenzar a construir un compilador. En un compilador real, deberás gestionar muchas más cosas, como el manejo de errores, la optimización del bytecode y la implementación de una máquina virtual completa si no estás utilizando una existente.

jueves, 11 de julio de 2024

Como podemos manejar las referencias nulas?


El error más frecuente en Java es NullPointerException y me imagino que en otros lenguajes alguno similar...  Para abordar esto, se han introducido estructuras y operadores que ayudan a manejar la ausencia de valores de manera más segura y explícita. 

Por ejemplo en Java se introdujo la clase `Optional` en la versión 8 para manejar valores potencialmente nulos de una manera más segura. `Optional` es un contenedor que puede o no contener un valor no nulo.

import java.util.Optional;


public class OptionalExample {

    public static void main(String[] args) {

        Optional<String> optional = Optional.of("Hello, World!");

        

        // Verificar si hay un valor presente

        if (optional.isPresent()) {

            System.out.println(optional.get());

        }

        

        // Uso del método ifPresent

        optional.ifPresent(System.out::println);

        

        // Proveer un valor predeterminado

        String value = optional.orElse("Default Value");

        System.out.println(value);

        

        // Proveer un valor predeterminado usando un Supplier

        value = optional.orElseGet(() -> "Default Value from Supplier");

        System.out.println(value);

    }

}


Scala utiliza la clase `Option` para representar un valor opcional. `Option` tiene dos subclases: `Some` y `None`, lo que proporciona una forma elegante y funcional de manejar valores que pueden estar ausentes. Esta idea es similar a la monada `Maybe` en Haskell.


object OptionExample extends App {

  val someValue: Option[String] = Some("Hello, World!")

  val noneValue: Option[String] = None


  // Uso de getOrElse

  println(someValue.getOrElse("Default Value"))

  println(noneValue.getOrElse("Default Value"))


  // Uso del patrón de coincidencia (Pattern Matching)

  someValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }


  noneValue match {

    case Some(value) => println(value)

    case None => println("No value")

  }

}


Scala "copio" esta forma de Haskell. Haskell utiliza el tipo de datos `Maybe` para manejar valores opcionales `Maybe` puede ser `Just` un valor o `Nothing`.


main :: IO ()

main = do

    let someValue = Just "Hello, World!"

    let noneValue = Nothing


    -- Uso de fromMaybe

    putStrLn (fromMaybe "Default Value" someValue)

    putStrLn (fromMaybe "Default Value" noneValue)


    -- Uso del patrón de coincidencia (Pattern Matching)

    case someValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


    case noneValue of

        Just value -> putStrLn value

        Nothing -> putStrLn "No value"


Kotlin es similar a Scala en muchos aspectos pero no en este. Kotlin introduce el operador `?` para facilitar la gestión de valores nulos. Este operador se utiliza para declarar tipos de datos que pueden ser nulos y para realizar operaciones seguras contra nulos.


fun main() {

    var nullableString: String? = "Hello, World!"


    // Uso del operador ?. para llamadas seguras

    println(nullableString?.length)


    // Uso del operador ?: para proporcionar un valor predeterminado

    val length = nullableString?.length ?: 0

    println(length)


    nullableString = null


    // Uso de let para ejecutar código solo si el valor no es nulo

    nullableString?.let {

        println(it)

    }

}


C# ha incluido varias características para manejar valores nulos, como el operador `?`, que facilita el manejo seguro de tipos que pueden ser nulos.


using System;


class Program

{

    static void Main()

    {

        string? nullableString = "Hello, World!";

        

        // Uso del operador ?. para llamadas seguras

        Console.WriteLine(nullableString?.Length);


        // Uso del operador ?? para proporcionar un valor predeterminado

        int length = nullableString?.Length ?? 0;

        Console.WriteLine(length);


        nullableString = null;


        // Uso de pattern matching para verificar nulos

        if (nullableString is string nonNullString)

        {

            Console.WriteLine(nonNullString);

        }

    }

}


Rust maneja la ausencia de valores y los errores de una manera robusta utilizando los tipos `Option` y `Result`. `Option` puede ser `Some` o `None`, mientras que `Result` puede ser `Ok` o `Err`.


fn main() {

    let some_value: Option<String> = Some("Hello, World!".to_string());

    let none_value: Option<String> = None;


    // Uso de unwrap_or

    println!("{}", some_value.unwrap_or("Default Value".to_string()));

    println!("{}", none_value.unwrap_or("Default Value".to_string()));


    // Uso del patrón de coincidencia (Pattern Matching)

    match some_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }


    match none_value {

        Some(value) => println!("{}", value),

        None => println!("No value"),

    }

}


Go no tiene un tipo de datos específico para manejar valores opcionales, pero utiliza la convención de retornar múltiples valores, incluyendo un valor y un `error`. Que la verdad no me gusta, te pasas preguntando todo el tiempo si hay error o si los valores son nulos. 


package main


import (

    "errors"

    "fmt"

)


func getValue() (string, error) {

    return "Hello, World!", nil

}


func getNullableValue() (string, error) {

    return "", errors.New("no value")

}


func main() {

    value, err := getValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", value)

    }


    nullableValue, err := getNullableValue()

    if err != nil {

        fmt.Println("Error:", err)

    } else {

        fmt.Println("Value:", nullableValue)

    }

}


Python utiliza la palabra clave `None` para representar la ausencia de valor. Aunque no tiene una estructura específica como `Optional`, los desarrolladores pueden utilizar condicionales y manejo de excepciones.


def get_value():

    return "Hello, World!"


def get_nullable_value():

    return None


value = get_value()

nullable_value = get_nullable_value()


if value is not None:

    print(value)

else:

    print("Default Value")


if nullable_value is not None:

    print(nullable_value)

else:

    print("Default Value")


Ruby utiliza `nil` para representar la ausencia de valor. Al igual que en Python, no tiene una estructura específica para valores opcionales, pero proporciona métodos para manejar `nil`.


value = "Hello, World!"

nullable_value = nil


# Uso del operador ||

puts value || "Default Value"

puts nullable_value || "Default Value"


# Uso de condicionales

puts value.nil? ? "Default Value" : value

puts nullable_value.nil? ? "Default Value" : nullable_value


C++ utiliza punteros inteligentes (`smart pointers`) para gestionar la memoria y prevenir errores relacionados con punteros nulos. Los punteros inteligentes, como `std::unique_ptr` y `std::shared_ptr`, se encargan de la gestión automática de la memoria.


#include <iostream>

#include <memory>


int main() {

    std::unique_ptr<int> uniquePtr(new int(42));

    if (uniquePtr) {

        std::cout << *uniquePtr << std::endl;

    }


    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);

    if (sharedPtr) {

        std::cout << *sharedPtr << std::endl;

    }


    // Uso de weak_ptr para evitar ciclos de referencia

    std::weak_ptr<int> weakPtr = sharedPtr;

    if (auto lockedPtr = weakPtr.lock()) {

        std::cout << *lockedPtr << std::endl;

    }


    return 0;

}


TypeScript, un superconjunto de JavaScript, permite tipos opcionales y tiene un soporte robusto para manejar valores `null` y `undefined`.


let nullableString: string | null = "Hello, World!";


// Uso del operador ? para llamadas seguras

console.log(nullableString?.length ?? 0);


// Uso de if para asegurar valores no nulos

if (nullableString !== null) {

    console.log(nullableString);

}


TypeScript utiliza tipos opcionales para manejar valores que pueden ser `null` o `undefined`, proporcionando un enfoque seguro para evitar errores comunes relacionados con valores nulos. El operador `?.` permite realizar llamadas seguras, y el operador `??` proporciona valores predeterminados en caso de valores `null` o `undefined`.

En fin, aunque la gestión de valores nulos varía entre lenguajes, la idea subyacente es la misma: proporcionar mecanismos más seguros y expresivos para manejar la ausencia de valores. Ya sea mediante clases contenedoras como `Optional` en Java y `Option` en Scala, tipos de datos como `Maybe` en Haskell, operadores específicos como `?` en Kotlin y C#, punteros inteligentes en C++, o enfoques específicos en Rust, Go, Python y Ruby, estos enfoques ayudan a reducir los errores y a escribir un código más robusto y mantenible.


miércoles, 3 de julio de 2024

Aprende a programar en Java desde cero con Java Magician


Si empiezan en el mundo java, les quiere recomendar este sitio que contiene muy buenos tutoriales en español. Los cuales están escritos de forma divertida.  


Dejo link: https://javamagician.com/

viernes, 21 de junio de 2024

Hola mundo en Vaadin


Vamos a hacer un hola mundo en Vaadin. Yo voy a dar por sobreentendido que sabemos crear un proyecto con spring. Si no lo sabes es tan fácil como ir a https://start.spring.io/ y de ahi vas configurando las cosas y agregamos el framework vaadin. 

Luego creamos un nuevo paquete como por ejemplo : com.example.vaadin.ui. Dentro del paquete, creamos una nueva clase MainView.java


package com.example.vaadin.ui;


import com.vaadin.flow.component.button.Button;

import com.vaadin.flow.component.notification.Notification;

import com.vaadin.flow.component.orderedlayout.VerticalLayout;

import com.vaadin.flow.router.Route;


@Route("")

public class MainView extends VerticalLayout {


    public MainView() {

        Button button = new Button("Haz clic aquí",

                event -> Notification.show("¡Hola, Mundo!"));

        add(button);

    }

}


Y listo!! Vamos a localhost:8080 para chequear nuestro "Hola mundo". 

martes, 18 de junio de 2024

Inyectar condicionalmente un bean con profiles


Tengo 2 beans que implementan una interfaz, por ejemplo un repositorio jpa con una base de datos y otro con archivos o en memoria, ponele... Y quiero algunas veces utilizar un bean y otras otro dependiendo de una configuración, esto se puede resolver de 2 maneras. Se puedes utilizar la anotación @Conditional o @Profile.

Veamos ejemplo utilizando profile: 


public interface MyService {

    void performService();

}


@Service

@Profile("serviceA")

public class ServiceA implements MyService {

    @Override

    public void performService() {

        System.out.println("Service A implementation");

    }

}


@Service

@Profile("serviceB")

public class ServiceB implements MyService {

    @Override

    public void performService() {

        System.out.println("Service B implementation");

    }

}


Tenemos que configurar el perfil activo en application.properties:


# Para usar ServiceA
spring.profiles.active=serviceA

# Para usar ServiceB
#spring.profiles.active=serviceB


Y inyectamos el bean:


@RestController
public class MyController {

    private final MyService myService;

    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }

    @GetMapping("/perform")
    public String perform() {
        myService.performService();
        return "Service performed";
    }
}

Y listo!!

lunes, 20 de mayo de 2024

Lenguajes utilizados en los proyectos apache

Tal vez hace mucho que Apache publico este gráfico pero yo recién lo veo : 



Como se puede ver Java es el lenguaje más utilizado por los proyectos de apache, seguido de python, c++, c, javascript, scala, C#, go, perl, etc ... 




miércoles, 8 de mayo de 2024

Que es la Covarianza, Contravarianza y Invarianza?


Ya sé que estos son temas super basicos pero siempre que hablan de eso me los confundo. Por eso voy a hacer este post para ver si porfin puedo fijar las ideas. 

La varianza y la covarianza son conceptos importantes en el contexto de los tipos genérico. Estos conceptos se refieren a cómo se relacionan los tipos genéricos cuando se consideran subtipos y super tipos. Veamos una explicación de cada uno:

Covarianza: La covarianza se refiere a la relación entre tipos genéricos donde la relación de subtipos se mantiene en la misma dirección que la relación de subtipos de los parámetros de tipo. En otras palabras, si tenemos un tipo T y otro tipo U donde U es un subtipo de T, entonces podemos decir que List<U> es un subtipo de List<T>. Esto significa que podemos asignar una lista de subtipos a una lista de supertipos sin necesidad de conversión explícita. La covarianza se utiliza típicamente en situaciones donde solo se lee de una estructura de datos, como en secuencias o enumeraciones.

Veamos un ejemplo en C#: 

using System;

using System.Collections.Generic;


class Program

{

    static void Main()

    {

        // Covarianza en IEnumerable<T>

        IEnumerable<string> strings = new List<string> { "hello", "world" };

        PrintItems(strings);

    }


    static void PrintItems(IEnumerable<object> items)

    {

        foreach (var item in items)

        {

            Console.WriteLine(item);

        }

    }

}

Veamos un ejemplo en Java: 

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Covarianza en List<? extends Number>
        List<Integer> integers = new ArrayList<>();
        integers.add(1);
        integers.add(2);
        printNumbers(integers);
    }

    static void printNumbers(List<? extends Number> numbers) {
        for (Number number : numbers) {
            System.out.println(number);
        }
    }
}

Y en scala: 

class Main {
  def main(args: Array[String]): Unit = {
    // Covarianza en List[+A]
    val strings: List[String] = List("hello", "world")
    printItems(strings)
  }

  def printItems(items: List[Any]): Unit = {
    items.foreach(println)
  }
}

Contravarianza: La contravarianza es el opuesto de la covarianza. Se refiere a la relación entre tipos genéricos donde la relación de subtipos se invierte en comparación con la relación de subtipos de los parámetros de tipo. En otras palabras, si tenemos un tipo T y otro tipo U donde U es un subtipo de T, entonces podemos decir que Action<T> es un subtipo de Action<U>. Esto significa que podemos asignar una función que acepta supertipos a una variable de función que acepta subtipos. La contravarianza se utiliza típicamente en situaciones donde solo se escribe en una estructura de datos, como en consumidores de datos o comparadores.

Veamos un ejemplo en Java: 


import java.util.function.Consumer;


public class Main {

    public static void main(String[] args) {

        // Contravarianza en Consumer<? super String>

        Consumer<Object> printString = Main::printString;

        printString.accept("hello");

    }


    static void printString(Object s) {

        System.out.println(s);

    }

}


Y en Scala: 

class Main {
  def main(args: Array[String]): Unit = {
    // Contravarianza en Function1[-T, +R]
    val printString: Any => Unit = Main.printString
    printString("hello")
  }
}

object Main {
  def printString(s: String): Unit = {
    println(s)
  }
}

Invarianza: La invarianza es el tercer escenario, donde no hay una relación de subtipos entre tipos genéricos. En otras palabras, List<T> no es ni un subtipo ni un supertipo de List<U> si T y U son tipos diferentes, incluso si U es un subtipo de T o viceversa. En este caso, necesitamos una conversión explícita para asignar entre los dos tipos.

En C#, la covarianza y la contravarianza se expresan a través de modificadores como out y in en declaraciones de tipo genérico. En Java, estos conceptos se implementan a través de la notación <? extends T> para la covarianza y <? super T> para la contravarianza en tipos genéricos. Es importante entender estos conceptos para escribir código genérico seguro y comprensible.

No recuerdo haber utilizado la contravarianza pero no se me hace algo tan común. Y ustedes, han utilizado estas técnicas? 


sábado, 3 de febrero de 2024

gRPC Client


De manera similar al lado del servidor, podemos generar el código del lado del cliente utilizando la definición del servicio. El código del cliente proporciona los mismos métodos que el servidor, que su código de cliente puede invocar; el código del cliente los traduce en llamadas de red de invocación de funciones remotas que van al lado del servidor. Dado que las definiciones de servicios gRPC son independientes del lenguaje, puede generar clientes y servidores para cualquier lenguaje admitido (a través de implementaciones de terceros) de su elección. 

Entonces, veamos un ejemplo en Java. A pesar del lenguaje de programación que utilizamos, los pasos simples involucrados en una implementación del lado del cliente implican configurar una conexión con el servidor remoto, adjuntar el código auxiliar del cliente a esa conexión e invocar el método remoto usando el código auxiliar del cliente.


ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)

.usePlaintext(true)

 .build(); 


// Initialize blocking stub using the channel 

ProductInfoGrpc.ProductInfoBlockingStub stub = ProductInfoGrpc.newBlockingStub(channel); 


// Call remote method using the blocking stub 

StringValue productID = stub.addProduct( 

     Product.newBuilder() 

            .setName("Apple iPhone 11") 

            .setDescription("Meet Apple iPhone 11." + "All-new dual-camera system with " + "Ultra Wide and Night mode.") 

   .build());



lunes, 21 de agosto de 2023

Inscripciones Abiertas 2023 a cursos Gugler!!

 

PRENSA GUGLER  LAB 2023

  NOTICIAS ACTUALES

informacion  OFERTA ACADÉMICA


Estimado :

 Se encuentran abiertas las inscripciones para el segundo cuatrimestre del año 2023, para todas las capacitaciones dictadas por el Laboratorio de Investigación Gugler. Podés asegurar tu lugar en el curso y comisión que desees !!!.

Las clases inician:

  • Martes 12/09 , Miércoles 13/09, Jueves 14/09 o Sábado 16/09, según el curso que elegiste.

 

Inscribirteclic aquí

Cursos, Horarios y comisionesclic aquí.

 

 

Dictamos nuestros cursos en la Facultad de Ciencia y Tecnología, perteneciente a la Universidad Autónoma de Entre Ríos. En nuestro portafolio de capacitación encontrarás:

MODALIDAD PRESENCIAL

  • Cursos de Programación:
    Programación en PHP.
     
  • Cursos de Mantenimiento/Reparación:
    Reparación y Mantenimiento de PC.

 MODALIDAD DISTANCIA

  • Cursos de Programación:
    Programación Web Frontend.
    Programación en Java.
informacion   MÁS INFORMACIÓN 

informacion LABORATORIO DE INVESTIGACIÓN GUGLER

Si deseas comunicarte con nosotros, te recordamos que podes hacerlo a través de los siguientes portales, en los cuales encontrarás información sobre Gugler.

TEL: (0343) - 4975066 Interno 119

Sitio Oficial:  www.gugler.com.ar

Campus:  campusvirtual.gugler.com.ar

Sistema de Gestión Cursos:  sgc.gugler.com.ar

Sistema de Gestión Cursos Móvil: Aquí 

Sistema de Documentación:  sgd.gugler.com.ar

Sistema de Validación:  giua.gugler.com.ar

                   Twitter                Facebook

Laboratorio Gugler

partir del 2012, la Facultad de Ciencia y Tecnología nos declaro:  "Laboratorio de Investigación".

 

El laboratorio ha ampliado las incumbencias de Gugler, ya que además de la capacitación, la promoción y difusión del software libre, ahora abarcará actividades como publicaciones y proyectos de investigación, así como también el dictado y participación en conferencias o exposiciones de ámbitos académicos y científicos.

 

Ante cualquier duda comunicarse con nosotros a gugler_contacto@uader.edu.ar

GUGLER PRESS

viernes, 2 de junio de 2023

La potencia de la programación funcional en Java con Vavr

 


En el mundo del desarrollo de software, los lenguajes funcionales están ganando popularidad debido a su enfoque en la inmutabilidad, la programación declarativa y las operaciones funcionales. Java, siendo un lenguaje orientado a objetos por naturaleza, carece de muchas características propias de los lenguajes funcionales. Sin embargo, gracias al framework Vavr, los desarrolladores de Java pueden aprovechar al máximo los conceptos y las ventajas de la programación funcional. 

Vavr es un framework funcional para Java 8 y superior que proporciona una biblioteca de clases y funciones para facilitar la programación funcional en Java. Ofrece una amplia gama de características, incluyendo tipos inmutables, operaciones funcionales, manejo de errores y mucho más. Vavr se basa en el paradigma funcional y promueve el uso de funciones puras, evitando los efectos secundarios y facilitando la escritura de código más limpio y conciso.

Características principales de Vavr:

  • Tipos inmutables: Vavr proporciona una serie de tipos inmutables, como List, Set, Option, Try, etc. Estos tipos aseguran que los objetos no puedan ser modificados una vez creados, lo que ayuda a prevenir errores y garantiza la consistencia del estado.
  • Operaciones funcionales: Vavr ofrece una amplia gama de operaciones funcionales, como map, filter, reduce, flatMap, etc. Estas operaciones permiten manipular y transformar colecciones de datos de manera elegante y expresiva.
  • Patrones de concurrencia: Vavr ofrece una API concisa y segura para trabajar con concurrencia en Java. Proporciona constructores de hilos, promesas y otras estructuras de datos para facilitar la programación concurrente.
  • Manejo de errores: Vavr ofrece una forma más robusta y funcional de manejar errores en Java. El tipo Try captura excepciones y permite manejarlas de manera más elegante y comprensible.
  • Validaciones: Vavr proporciona una API para realizar validaciones de manera concisa y declarativa. Permite combinar múltiples validaciones y proporciona mensajes de error claros y comprensibles.


Entre las ventajas de usar Vavr tenemos :

  • Código más legible y conciso: Las características funcionales de Vavr permiten escribir código más legible y conciso. El uso de funciones puras y tipos inmutables reduce la complejidad y mejora la mantenibilidad del código.
  • Programación segura: Al evitar los efectos secundarios y promover la inmutabilidad, Vavr ayuda a prevenir errores y garantizar la integridad del código.
  • Compatibilidad con Java existente: Vavr se integra perfectamente con el código Java existente, lo que permite aprovechar las ventajas de la programación funcional sin necesidad de cambiar todo el código base.
  • Curvas de aprendizaje suaves: Si estás familiarizado con conceptos de programación funcional, la curva de aprendizaje de Vavr es bastante suave. Vavr proporciona una API coherente y bien documentada que facilita su adopción.

Vavr es un framework funcional poderoso que proporciona a los desarrolladores de Java las herramientas necesarias para aprovechar al máximo los conceptos de programación funcional. Con su amplia gama de características y su fácil integración con el código Java existente, Vavr puede ayudar a mejorar la calidad del código, la legibilidad y la mantenibilidad de las aplicaciones Java. 


Veamos un ejemplo de lo que podemos hacer con Vavr: 


import io.vavr.collection.List;

import io.vavr.control.Option;


public class VavrExample {


    public static void main(String[] args) {

        // Tipos inmutables

        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        List<Integer> squaredNumbers = numbers.map(n -> n * n);

        System.out.println(squaredNumbers); // Imprime: List(1, 4, 9, 16, 25)


        // Operaciones funcionales

        int sum = squaredNumbers.reduce((a, b) -> a + b);

        System.out.println("Suma: " + sum); // Imprime: Suma: 55


        // Option: manejo de valores nulos

        Option<String> name = Option.of("John");

        String upperCaseName = name.map(String::toUpperCase)

                                   .getOrElse("N/A");

        System.out.println(upperCaseName); // Imprime: JOHN


        Option<String> nullName = Option.of(null);

        String nullSafeName = nullName.map(String::toUpperCase)

                                      .getOrElse("N/A");

        System.out.println(nullSafeName); // Imprime: N/A

    }

}

lunes, 22 de mayo de 2023

Por qué son importantes los test de arquitectura.


En el desarrollo de aplicaciones de software, la arquitectura juega un papel fundamental. Define la estructura y el diseño de la aplicación, proporcionando una base sólida sobre la cual se construyen todas las funcionalidades. A medida que las aplicaciones se vuelven más complejas, es esencial garantizar que la arquitectura sea sólida y cumpla con los requisitos deseados. Aquí es donde entran en juego los test de arquitectura.  

¿Qué son los test de arquitectura? Los test de arquitectura son una práctica que busca evaluar la robustez y la calidad de la arquitectura de una aplicación. Se centran en verificar que los componentes clave de la arquitectura funcionen correctamente y cumplan con los requisitos esperados. A diferencia de los test unitarios, que se centran en probar unidades individuales de código, los test de arquitectura se enfocan en la estructura global de la aplicación y en cómo interactúan sus diferentes componentes. 

¿Porque es importante verificar la arquitectura? Principalmente porque a medida que desarrollamos podemos no respetar los principios que definimos desde la concepción de la aplicación. Los test de arquitectura nos permiten mantener esos principios a lo largo del tiempo verificando si se cumplen commit a commit.  

Además, permiten:  

  • Identificación temprana de problemas: Los test de arquitectura permiten identificar problemas potenciales en la fase inicial del desarrollo. Al evaluar la arquitectura antes de implementar todas las funcionalidades, es posible detectar problemas de diseño, cuellos de botella de rendimiento, dependencias no deseadas, entre otros. Esto ayuda a reducir el costo y el esfuerzo necesario para corregir problemas en etapas más avanzadas del desarrollo. 
  • Mejora de la calidad y mantenibilidad: Una arquitectura sólida es fundamental para garantizar la calidad y mantenibilidad a largo plazo de una aplicación. Los test de arquitectura ayudan a validar que la estructura de la aplicación cumpla con principios de diseño y buenas prácticas. Esto facilita la comprensión del código, la reutilización de componentes y la realización de cambios sin afectar negativamente otras partes del sistema. 
  • Alineación con los requisitos: Los test de arquitectura permiten asegurar que la arquitectura esté alineada con los requisitos del negocio y las necesidades de los usuarios. Al realizar pruebas exhaustivas de la arquitectura, se pueden identificar brechas en los requisitos y asegurar que todas las funcionalidades esperadas estén cubiertas. Esto ayuda a evitar costosos cambios posteriores debido a requisitos omitidos o malinterpretados. 
  • Rendimiento y escalabilidad: Las aplicaciones modernas deben ser capaces de manejar grandes volúmenes de datos y un alto número de usuarios concurrentes. Los test de arquitectura pueden ayudar a identificar cuellos de botella de rendimiento y evaluar la escalabilidad de la aplicación. Esto permite tomar decisiones informadas sobre el uso de tecnologías adecuadas, la optimización de consultas y la distribución de la carga de trabajo, asegurando un rendimiento óptimo incluso en condiciones de alta demanda. 
  • Gestión de riesgos: Los test de arquitectura ayudan a mitigar los riesgos asociados con el desarrollo de aplicaciones complejas. Permiten identificar y abordar problemas de seguridad, integridad de datos y fiabilidad del sistema. Al probar y validar la arquitectura en diferentes escenarios y condiciones, se pueden detectar posibles vulnerabilidades o debilidades y tomar medidas para corregirlas antes de que se conviertan en problemas reales. 

¿Y cómo podemos probar nuestra arquitectura fácilmente? Para ello existen varios frameworks y herramientas disponibles para realizar test de arquitectura en las plataformas Java y .NET algunos de los más utilizados son: 

Java: 

  • Arquillian: Es un framework de pruebas de integración que se enfoca en la creación de escenarios de prueba realistas para aplicaciones Java. Proporciona soporte para la ejecución de pruebas en diferentes entornos y contenedores. 
  • JUnit: Aunque principalmente utilizado para test unitarios, JUnit también puede ser utilizado para realizar pruebas de integración y pruebas de arquitectura. Permite la ejecución de pruebas en paralelo y la creación de casos de prueba complejos. 

.NET: 

  • NUnit: Similar a JUnit, NUnit es un framework de pruebas para la plataforma .NET. Ofrece una amplia gama de funcionalidades para realizar pruebas de arquitectura, incluyendo aserciones, configuraciones y la ejecución de pruebas en paralelo. 
  • SpecFlow: Esta herramienta se basa en el concepto de BDD (Behavior-Driven Development) y permite escribir pruebas de aceptación en un lenguaje natural fácilmente comprensible. Es especialmente útil para probar la arquitectura desde la perspectiva del comportamiento esperado del sistema. 

ArchUnit es otro framework relevante para realizar test de arquitectura en aplicaciones Java y .NET. ArchUnit(en java) y NArchiUnit(en .NET) es un framework de código abierto que permite definir y verificar reglas arquitectónicas en el código fuente de una aplicación. Proporciona una forma declarativa de definir restricciones sobre la estructura y el diseño de la arquitectura, como reglas de dependencia entre paquetes, convenciones de nomenclatura, restricciones de visibilidad, entre otros aspectos.  

Al utilizar ArchUnit, los desarrolladores pueden escribir pruebas que verifiquen automáticamente si la arquitectura de la aplicación cumple con las reglas definidas. Esto permite garantizar la coherencia y la integridad de la arquitectura, evitando que se introduzcan violaciones arquitectónicas en el código. Además, ArchUnit ofrece una sintaxis expresiva y flexible que facilita la definición de reglas específicas para cada proyecto.  

Para que los test unitarios sean útiles es importante, correrlos regularmente y para esto herramientas como un servidor de integración continua son fundamentales.  

Los servidores de integración continua desempeñan un papel importante en la ejecución automatizada de los test de arquitectura. Estos servidores, como Jenkins, Bamboo o Azure DevOps, permiten la configuración y ejecución de pipelines de integración continua, que incluyen la compilación, pruebas y despliegue automatizado de la aplicación. Al utilizar un servidor de integración continua, los test de arquitectura se pueden ejecutar de manera regular y sistemática, lo que garantiza que cualquier cambio o actualización en la aplicación se someta a pruebas de arquitectura antes de ser desplegado en producción. 

Conclusión: Los test de arquitectura son fundamentales para garantizar una base sólida y robusta en el desarrollo de aplicaciones de software. Ayudan a identificar problemas tempranamente, mejorar la calidad y mantenibilidad, alinear la arquitectura con los requisitos, garantizar el rendimiento y escalabilidad, y mitigar riesgos. Utilizar frameworks específicos y aprovechar los servidores de integración continua facilita su implementación y automatización. No programar estos test puede tener consecuencias negativas, como problemas de escalabilidad, falta de mantenibilidad, vulnerabilidades de seguridad y dificultad para cumplir requisitos. En resumen, los test de arquitectura son una práctica esencial para asegurar el éxito y la calidad de las aplicaciones.