Translate

miércoles, 19 de marzo de 2025

Funciones Lambda en C++


Las funciones lambda en C++ permiten definir funciones anónimas de manera concisa y flexible. Introducidas en C++11 y mejoradas en versiones posteriores, las lambdas son muy útiles para expresiones cortas y para capturar variables del contexto circundante.

La sintaxis general de una lambda es:


[captura](parámetros) -> tipo_de_retorno { cuerpo };


Veamos un ejemplo:


#include <iostream>


int main() {

    auto suma = [](int a, int b) -> int {

        return a + b;

    };

    

    std::cout << "La suma es: " << suma(3, 4) << std::endl;

    return 0;

}


Las lambdas pueden capturar variables del contexto donde se definen. Su puede captura por Valor


#include <iostream>


int main() {

    int x = 10;

    auto lambda = [x]() { std::cout << "x = " << x << std::endl; };

    lambda();

    return 0;

}


Como x se captura por valor, cualquier cambio dentro de la lambda no afectará la variable original.

Y captura por Referencia:


#include <iostream>


int main() {

    int x = 10;

    auto lambda = [&x]() { x += 5; };

    lambda();

    std::cout << "x después de la lambda: " << x << std::endl;

    return 0;

}


Dado que x se captura por referencia, su modificación dentro de la lambda afecta la variable original.

Se puede capturar todo el entorno automáticamente:


- [=] captura todas las variables por valor.

- [&] captura todas las variables por referencia.


#include <iostream>


int main() {

    int a = 5, b = 10;

    auto lambda = [=]() { std::cout << "a + b = " << (a + b) << std::endl; };

    lambda();

    return 0;

}


Las lambdas pueden almacenarse en std::function para mayor flexibilidad:


#include <iostream>

#include <functional>


int main() {

    std::function<int(int, int)> multiplicar = [](int x, int y) { return x * y; };

    std::cout << "Multiplicación: " << multiplicar(3, 4) << std::endl;

    return 0;

}


Las lambdas son útiles para definir comparaciones personalizadas:


Por ejemplo: 


#include <iostream>

#include <vector>

#include <algorithm>


int main() {

    std::vector<int> nums = {3, 1, 4, 1, 5, 9};

    

    std::sort(nums.begin(), nums.end(), [](int a, int b) {

        return a > b;

    });

    

    for (int n : nums) std::cout << n << " ";

    return 0;

}


Las funciones lambda en C++ permiten definir funciones anónimas de manera flexible y eficiente. Son especialmente útiles en programación funcional, expresiones cortas y manejo de algoritmos en la STL. 

sábado, 15 de marzo de 2025

Clases Selladas y coincidencia de patrones en Java


Con la evolución de Java, dos características han tomado mayor protagonismo: las clases selladas (sealed classes) y el coincidencia de patrones (pattern matching). La combinación de ambas permite escribir código más seguro, expresivo y mantenible.

Las clases selladas permiten restringir qué clases pueden extender o implementar una clase o interfaz. Se declaran con la palabra clave sealed y deben especificar sus subclases con permits.


Por ejemplo:


public sealed class Figura permits Circulo, Poligono {}

public final class Circulo extends Figura {}

public sealed class Poligono extends Figura permits Triangulo {}

public non-sealed class Triangulo extends Poligono {}


Aquí, Figura solo puede ser extendida por Circulo y Poligono, mientras que Triangulo puede ser extendido libremente.

El pattern matching simplifica la lógica condicional al combinar la comprobación de tipos y la conversión en una sola operación.

Por ejemplo:


Object obj = "Hola, mundo!";

if (obj instanceof String s) {

    System.out.println(s.toUpperCase());

}


Si obj es un String, se asigna automáticamente a s, evitando el casting manual.


Tambien lo podemos usar con switch: 


static void procesar(Object obj) {

    switch (obj) {

        case String s -> System.out.println("Es una cadena: " + s.toUpperCase());

        case Integer i -> System.out.println("Es un número: " + (i * 2));

        default -> System.out.println("Tipo no soportado");

    }

}


El switch maneja diferentes tipos de objetos de manera más clara y concisa.

La combinación de ambas características permite que el compilador conozca todas las posibles subclases en tiempo de compilación, facilitando el uso de switch y garantizando que se manejen todos los casos.

Por ejemplo: 


public sealed interface Operacion permits Suma, Resta, Multiplicacion {}


public record Suma(int a, int b) implements Operacion {}

public record Resta(int a, int b) implements Operacion {}

public record Multiplicacion(int a, int b) implements Operacion {}


public int evaluar(Operacion op) {

    return switch (op) {

        case Suma s -> s.a() + s.b();

        case Resta r -> r.a() - r.b();

        case Multiplicacion m -> m.a() * m.b();

    };

}


Aquí, el switch cubre todas las implementaciones posibles de Operacion, asegurando que no haya casos no manejados.

Las clases selladas y el pattern matching en Java permiten escribir código más seguro y expresivo. Al proporcionar control sobre la herencia y simplificar la lógica condicional, facilitan la creación de software más mantenible y alineado con las tendencias modernas del desarrollo en Java.


viernes, 14 de marzo de 2025

Vine: Un Lenguaje Minimalista y Funcional


Vine es un nuevo lenguaje de programación experimental basado en redes de interacción.

Vine es un lenguaje multiparadigma que ofrece una interoperabilidad fluida entre patrones funcionales e imperativos.

Vamos por parte ¿Qué son las redes de interacción? : Las redes de interacción son una representación gráfica de los cálculos en un programa, lo que permite una evaluación más eficiente y altamente paralela. En lugar de ejecutarse como instrucciones secuenciales, las operaciones en Vine pueden reorganizarse dinámicamente para optimizar la ejecución.  

Vine está diseñado para facilitar la programación funcional y la concurrencia. Esto significa que los desarrolladores pueden escribir código más limpio, modular y escalable, sin preocuparse tanto por problemas típicos de la concurrencia, como las condiciones de carrera.  


Los beneficios clave de Vine son:   

  • Ejecución optimizada: Aprovecha mejor los recursos del hardware al paralelizar automáticamente el procesamiento.  
  • Código más claro: Al estar basado en principios funcionales, evita efectos secundarios y hace que el código sea más fácil de entender y mantener.  
  • Escalabilidad: Diseñado para manejar aplicaciones concurrentes sin los bloqueos y problemas de sincronización típicos en otros lenguajes.  


El lenguaje todavía está en fase temprana de desarrollo, pero sus creadores creen que podría ser una alternativa prometedora para la programación funcional y concurrente en el futuro.  

Veamos un ejemplo: 


pub fn main(&io: &IO) {

  while io.prompt("> ") is Some(line) {

    let num = N32::parse(line);

    io.println(match num {

      Some(num) { fib(num).to_string() }

      None { "invalid number" }

    });

  }

}


pub fn fib(n: N32) -> N32 {

  let a = 0;

  let b = 1;

  while n != 0 {

    n -= 1;

    (a, b) = (b, a + b);

  }

  a

}


Muchas de las características de Vine están influenciadas por Rust, y tiene una sintaxis orientada a expresiones, un sistema de tipos y un sistema de módulos similares.

Vine promete ser una herramienta valiosa para aquellos interesados en paradigmas funcionales y en la optimización de aplicaciones concurrentes.


Dejo links: 

https://www.infoq.com/news/2025/03/new-programming-language-vine/

https://vine.dev/

miércoles, 12 de marzo de 2025

Programación Orientada a Objetos en Python parte 3



Seguimos con poo y python. 

En Python, el parámetro `self` se usa en los métodos de una clase para hacer referencia a la instancia actual del objeto. 

Cada vez que se llama a un método de una instancia de una clase, Python pasa automáticamente la instancia como primer argumento. El nombre convencional para este argumento es self, aunque puedes llamarlo de otra manera (aunque no se recomienda porque es una convención establecida).

Veamos un ejemplo:


class Persona:

    def __init__(self, nombre, edad):

        self.nombre = nombre  # `self.nombre` es un atributo de la instancia

        self.edad = edad      # `self.edad` también es un atributo de la instancia


    def saludar(self):

        print(f"Hola, mi nombre es {self.nombre} y tengo {self.edad} años.")


# Crear una instancia de la clase

p1 = Persona("Carlos", 30)


# Llamar al método saludar

p1.saludar()


  • self.nombre = nombre: Guarda el valor del parámetro nombre en el atributo nombre de la instancia.
  • def saludar(self): El método recibe self, lo que le permite acceder a los atributos nombre y edad de la instancia p1.
  • Cuando se llama a p1.saludar(), Python en realidad ejecuta Persona.saludar(p1), pasando la instancia p1 como primer argumento.

self es obligatorio en métodos de instancia porque permite acceder a los atributos y otros métodos de la instancia. Si se omite, se generará un error:


class Prueba:

    def mensaje():

        print("Este método no tiene self.")


p = Prueba()

p.mensaje()  # TypeError: mensaje() takes 0 positional arguments but 1 was given


Aquí, Python intenta pasar la instancia p automáticamente, pero como el método mensaje() no tiene self, se genera un error.


El parámetro self también se usa en métodos especiales como __str__, __repr__, __eq__, etc.:


class Auto:

    def __init__(self, marca):

        self.marca = marca


    def __str__(self):

        return f"Auto de marca {self.marca}"


a = Auto("Toyota")

print(a)  # Auto de marca Toyota


En Python, un constructor es un método especial que se ejecuta automáticamente cuando se crea una nueva instancia de una clase. Se define con el método __init__.

__init__ es el constructor de una clase en Python. Se usa para inicializar los atributos del objeto cuando se crea una instancia.


class Persona:

    def __init__(self, nombre, edad):

        self.nombre = nombre  # Atributo de la instancia

        self.edad = edad      # Atributo de la instancia


    def presentarse(self):

        print(f"Hola, soy {self.nombre} y tengo {self.edad} años.")


# Crear una instancia

p1 = Persona("Carlos", 30)


# Llamar a un método

p1.presentarse()


  • __init__ recibe self, que es la instancia del objeto.
  • self.nombre = nombre almacena el valor en el objeto.
  • Cuando se crea una instancia con Persona("Carlos", 30), Python ejecuta automáticamente __init__, asignando "Carlos" a self.nombre y 30 a self.edad.


Se pueden definir valores predeterminados en los parámetros del constructor:


class Coche:

    def __init__(self, marca="Toyota", modelo="Corolla"):

        self.marca = marca

        self.modelo = modelo


c1 = Coche()  # Usa los valores por defecto

c2 = Coche("Ford", "Focus")  # Pasa valores específicos


print(c1.marca, c1.modelo)  # Toyota Corolla

print(c2.marca, c2.modelo)  # Ford Focus


También es posible definir un constructor sin argumentos (además de `self`):


class Ejemplo:

    def __init__(self):

        print("Se ha creado una nueva instancia.")


obj = Ejemplo()  # Se imprimirá el mensaje


El constructor puede contener lógica para validar datos o modificar valores antes de asignarlos:



class Usuario:

    def __init__(self, nombre, edad):

        self.nombre = nombre

        self.edad = max(0, edad)  # Evita edades negativas


u1 = Usuario("Ana", -5)

print(u1.edad)  # 0


Para poder utilizar objetos debemos definir una variable de referencia y luego instanciar dicho objeto, como vimos anteriormente:

c1 = Coche()  # Usa los valores por defecto

c2 = Coche("Ford", "Focus")  # Pasa valores específicos


Una vez definido un objeto podemos interactuar con él invocando sus atributos (lo cual no es recomendado porque rompemos el principio del encapsulamiento) públicos e interactuando con sus métodos:

c1.marca = "Ford"

c1.modelo = ”Fiesta”

martes, 11 de marzo de 2025

Clases sin Nombre y Métodos Principales sin Nombre en Java 21


Con Java 21, se introducen dos características que simplifican el código para desarrolladores: clases sin nombre y métodos main sin nombre. Estas mejoras buscan reducir la cantidad de código boilerplate en programas simples y facilitar el aprendizaje de Java para nuevos desarrolladores.

Las clases sin nombre permiten escribir código sin necesidad de definir explícitamente una clase contenedora. Esto facilita la escritura de programas pequeños o scripts sin la sobrecarga de definir clases innecesarias.

Antes de Java 21, un programa en Java requería una clase con un main:


public class MiClase {

    public static void main(String[] args) {

        System.out.println("Hola, Java 21!");

    }

}


Con las clases sin nombre, ahora puedes escribir simplemente:


System.out.println("Hola, Java 21!");


En Java 21, no es obligatorio definir explícitamente public static void main(String[] args). Ahora el código dentro del archivo fuente se ejecuta directamente.


System.out.println("Ejecutando sin un método 'main' explícito!");


¿Cómo funciona?

  • La JVM reconoce el archivo como ejecutable sin necesidad de una clase o main.
  • Útil para scripts rápidos o pruebas sencillas.
  • Sigue siendo posible definir clases si se necesita una estructura más compleja.


Las clases sin nombre y los métodos main sin nombre en Java 21 son pasos importantes para hacer el lenguaje más accesible y fácil de usar en escenarios rápidos y educativos. Si bien Java sigue siendo un lenguaje de propósito general con una fuerte orientación hacia aplicaciones empresariales, estas mejoras lo acercan más al paradigma de scripting y programación ligera, pero a mi entender nos alejan de los conceptos puros de programación orientada a objetos. 


lunes, 10 de marzo de 2025

¿Para qué se usa volatile en C?


En C, volatile es un calificador de tipo que se utiliza para indicar al compilador que el valor de una variable puede cambiar de maneras que el compilador no puede predecir. Esto es crucial en situaciones donde:

  • Hardware interactúa con la memoria: En sistemas embebidos, los registros de hardware se mapean a direcciones de memoria. Estos registros pueden cambiar sus valores de forma asíncrona, sin que el código C lo inicie directamente.
  • Manejadores de interrupciones: Los manejadores de interrupciones pueden modificar variables que también se utilizan en el código principal. El compilador no puede saber cuándo ocurrirá una interrupción, por lo que necesita saber que estos valores pueden cambiar de forma inesperada.
  • Variables compartidas entre hilos (en entornos con hilos): Aunque C estándar no define hilos, en sistemas operativos que los soportan, volatile puede ayudar a prevenir ciertas optimizaciones que serían incorrectas en presencia de acceso concurrente a la memoria.

Los compiladores de C son muy buenos para optimizar el código. Sin embargo, estas optimizaciones pueden ser problemáticas cuando el valor de una variable puede cambiar externamente. Sin volatile, el compilador podría:

Almacenar el valor de una variable en un registro y reutilizar ese valor, en lugar de leerlo de la memoria cada vez.

Eliminar lecturas o escrituras de la variable que considera redundantes.

`volatile` le dice al compilador que evite estas optimizaciones, asegurando que:

  • Cada lectura de la variable se realice realmente desde la memoria.
  • Cada escritura de la variable se realice realmente en la memoria.


Veamos un ejemplo: 


volatile unsigned int * puerto_serial = (unsigned int *)0xFFFF0000; // Registro de hardware


int main() {

    unsigned int dato;


    dato = *puerto_serial; // Leer el valor del puerto serial

    // ... procesar el dato ...


    *puerto_serial = 0x0A; // Escribir un valor en el puerto serial


    return 0;

}


En este ejemplo, `puerto_serial` es un puntero a un registro de hardware. El calificador volatile asegura que cada lectura y escritura a esta dirección de memoria se realice realmente, evitando que el compilador optimice estas operaciones.

Por lo tanto podemos concluir que: 

  • volatile no proporciona sincronización de hilos. En entornos multihilo, se necesitan mecanismos de sincronización adicionales (como mutex) para proteger los datos compartidos.
  • volatile solo previene la optimización del compilador con respecto a esa variable. No proporciona atomicidad.
  • El uso excesivo de `volatile` puede reducir el rendimiento, por lo que solo debe usarse cuando sea necesario.


En resumen, volatile es esencial en C cuando se trabaja con hardware, interrupciones o en situaciones donde el valor de una variable puede cambiar de forma impredecible.


Concurrencia en Erlang parte 15


Para empezar, deberíamos establecer una estructura de directorio estándar de Erlang, que se parece a esto:


ebin/

include/

priv/

src/


El directorio ebin/ es donde irán los archivos una vez que se compilen. El directorio include/ se utiliza para almacenar archivos .hrl que se incluirán en otras aplicaciones; los archivos .hrl privados generalmente se guardan dentro del directorio src/. El directorio priv/ se utiliza para ejecutables que podrían tener que interactuar con Erlang, como controladores específicos y demás. En realidad, no utilizaremos ese directorio para este proyecto. Luego, el último es el directorio src/, donde se almacenan todos los archivos .erl.

En los proyectos estándar de Erlang, esta estructura de directorio puede variar un poco. Se puede agregar un directorio conf/ para archivos de configuración específicos, doc/ para la documentación y lib/ para las bibliotecas de terceros necesarias para que se ejecute su aplicación. Los diferentes productos Erlang del mercado suelen utilizar nombres diferentes a estos, pero los cuatro mencionados anteriormente suelen permanecer iguales dado que son parte de las prácticas OTP estándar.

Spring Native: La Evolución de las Aplicaciones Spring en un Mundo de Desempeño Nativo


Spring Native es un proyecto dentro del ecosistema Spring que permite compilar aplicaciones Spring Boot directamente a imágenes nativas utilizando GraalVM. Esto significa que, en lugar de ejecutar la aplicación en la JVM (Java Virtual Machine), el código se compila en un binario nativo específico de la plataforma.

¿Por qué es importante Spring Native? Como sabran, spring es muy bueno pero pero con la inyección de dependencia y el uso de proxies con lleva a un mayor uso de memoria y a un inicio de aplicación lento. Además que con docker tampoco es una ventaja ser multiplataforma. Con que funcione en docker ya esta. Todos estos problemas son resueltos con Graalvm y spring native. 

Entonces podemos nombras los siguientes beneficios:

  • Rendimiento Mejorado: Las aplicaciones nativas tienen tiempos de inicio mucho más rápidos y un consumo de memoria significativamente menor en comparación con las aplicaciones que corren sobre la JVM. Esto es crucial para aplicaciones en contenedores (como Docker) y para arquitecturas de microservicios donde la eficiencia es clave.
  • Despliegue más Eficiente: El código nativo puede simplificar la infraestructura de despliegue, ya que no depende de tener una JVM instalada. Esto puede ser muy útil en entornos donde el espacio y los recursos son limitados.
  • Menor Consumo de Memoria: Las aplicaciones nativas pueden ejecutarse con un consumo de memoria más bajo, lo que hace que sean ideales para entornos de alta escala, como la nube y la ejecución en dispositivos con recursos limitados.
  • Reducción de la Complejidad Operacional: Al generar una imagen nativa, el proceso de despliegue es más directo y menos dependiente de las complejidades de la configuración de la JVM. Esto puede mejorar la mantenibilidad y reducir los errores de configuración.
  • Mejor Compatibilidad con el Ecosistema de Contenedores: Las imágenes nativas de Spring Boot se integran de forma más eficiente con contenedores como Docker, al permitirles ser más livianas y rápidas al arrancar, lo cual es vital en escenarios donde el escalado rápido y la flexibilidad son necesarios.

Cuando usar Spring Native?: 

  • Microservicios: Dado que las aplicaciones nativas tienen tiempos de inicio rápidos y un menor uso de memoria, son perfectas para entornos de microservicios donde el arranque rápido y el escalado eficiente son esenciales.
  • Sistemas Serverless: Al ser aplicaciones de bajo consumo, pueden integrarse bien con arquitecturas serverless, donde el costo de recursos y el tiempo de ejecución son factores clave.
  • IoT y Dispositivos con Recursos Limitados: En aplicaciones que se ejecutan en dispositivos con recursos limitados, como IoT, la eficiencia de Spring Native puede ser un gran beneficio.


Desafíos y Limitaciones:

  • Compatibilidad: No todas las bibliotecas de Spring Boot son totalmente compatibles con GraalVM, y algunas características pueden requerir ajustes adicionales.
  • Tiempo de compilación: La creación de la imagen nativa puede ser lenta, lo que podría no ser ideal para todos los entornos de desarrollo.


¿Cómo Integrar Spring Native en tu Proyecto?

  • Requisitos: Tener un proyecto Spring Boot básico.
  • Configuración de GraalVM: Instalar GraalVM y configurar el entorno de desarrollo para soportar la compilación nativa.
  • Incluir dependencias nativas: Agregar la dependencia de spring-native en el pom.xml o `build.gradle`.
  • Compilación: Usar `mvn  spring-boot:build-image o ./gradlew buildNative para generar la imagen nativa.
  • Despliegue y pruebas: Probar la imagen nativa generada en el entorno de producción.


Spring Native permite a las aplicaciones Spring aprovechar los beneficios del código nativo: tiempos de arranque rápidos, menor uso de memoria y mayor eficiencia en la ejecución. A medida que las arquitecturas modernas se mueven hacia entornos de microservicios y despliegues en la nube, Spring Native se posiciona como una herramienta clave para la optimización de aplicaciones Spring.


viernes, 7 de marzo de 2025

API de Vectores en Java: Alto Rendimiento con Operaciones SIMD


Java ha introducido mejoras significativas en el rendimiento con la API de Vectores, una funcionalidad experimental desde Java 16 y evolucionada en Java 19 y 20. Esta API permite aprovechar las capacidades de SIMD (Single Instruction, Multiple Data), lo que mejora la eficiencia en cálculos intensivos, como gráficos, procesamiento de señales y machine learning.

La API de Vectores en Java permite realizar operaciones con múltiples valores en paralelo utilizando instrucciones SIMD, optimizando así el uso del hardware subyacente. Está diseñada para ejecutarse de manera más eficiente en CPU modernas con conjuntos de instrucciones avanzados como AVX y NEON.

Las características principales son:

  • Operaciones eficientes con vectores: Realiza cálculos numéricos sobre múltiples datos simultáneamente.
  • Compatibilidad con JVM y JIT: Se integra con el compilador Just-In-Time para optimizar el rendimiento.
  • Optimización automática: Se adapta a la arquitectura del procesador para aprovechar el mejor conjunto de instrucciones disponible.
  • API expresiva y segura: Usa un enfoque declarativo y sin riesgo de desbordamiento de memoria.


Veamos un ejemplo: 


import jdk.incubator.vector.FloatVector;

import jdk.incubator.vector.VectorSpecies;


public class VectorExample {

    static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;


    public static void main(String[] args) {

        float[] a = {1.0f, 2.0f, 3.0f, 4.0f};

        float[] b = {5.0f, 6.0f, 7.0f, 8.0f};

        float[] result = new float[a.length];


        var va = FloatVector.fromArray(SPECIES, a, 0);

        var vb = FloatVector.fromArray(SPECIES, b, 0);

        var vc = va.add(vb);

        vc.intoArray(result, 0);


        System.out.println("Resultado: ");

        for (float v : result) {

            System.out.print(v + " ");

        }

    }

}


  • Se define una especie de vector (SPECIES_PREFERRED) que se ajusta a la arquitectura del CPU.
  • Se cargan los datos en vectores FloatVector.
  • Se realiza una operación de suma en paralelo.
  • Se almacenan los resultados en un array de salida.


En Java 19 y 20 se continuo trabajando obteniendo estas mejoras:

  • Mayor estabilidad y optimización en JVM: Se han reducido las sobrecargas en JIT.
  • Soporte extendido para más arquitecturas: Ahora es compatible con ARM y RISC-V.
  • Nuevas operaciones matemáticas: Más funciones avanzadas como cálculos trigonométricos y logarítmicos.


La API de Vectores en Java es una gran incorporación para quienes necesitan alto rendimiento en operaciones matemáticas y científicas. Con el soporte mejorado en arquitecturas modernas y una mayor optimización en la JVM, esta API se perfila como una herramienta clave para el desarrollo de aplicaciones de alto rendimiento.

miércoles, 5 de marzo de 2025

Cómo Spring Implementa AOP


Spring AOP se basa en el Patrón Proxy y la generación dinámica de clases para aplicar aspectos sin modificar el código fuente original.

Spring AOP crea proxies para interceptar llamadas a métodos y aplicar lógica adicional. Dependiendo de la estructura de la clase objetivo, Spring elige entre dos enfoques:

JDK Dynamic Proxies: Si la clase implementa una interfaz, Spring usa java.lang.reflect.Proxy para generar un proxy en tiempo de ejecución.

CGLIB (Code Generation Library): Si la clase no implementa interfaces, se genera una subclase dinámica con CGLIB.


Veamos un ejemplo de un proxy con JDK:


import java.lang.reflect.*;


interface Servicio {

    void ejecutar();

}


class ServicioImpl implements Servicio {

    public void ejecutar() {

        System.out.println("Ejecutando servicio...");

    }

}


class ProxyHandler implements InvocationHandler {

    private final Object target;

    public ProxyHandler(Object target) {

        this.target = target;

    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("[AOP] Antes de ejecutar");

        Object result = method.invoke(target, args);

        System.out.println("[AOP] Después de ejecutar");

        return result;

    }

}


public class Main {

    public static void main(String[] args) {

        Servicio servicio = (Servicio) Proxy.newProxyInstance(

            Servicio.class.getClassLoader(),

            new Class[]{Servicio.class},

            new ProxyHandler(new ServicioImpl()));

        servicio.ejecutar();

    }

}


Si la clase no implementa interfaces, Spring usa CGLIB para generar una subclase dinámica.


import net.sf.cglib.proxy.*;


class Servicio {

    public void ejecutar() {

        System.out.println("Ejecutando servicio...");

    }

}


class Interceptor implements MethodInterceptor {

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        System.out.println("[AOP] Antes de ejecutar");

        Object result = proxy.invokeSuper(obj, args);

        System.out.println("[AOP] Después de ejecutar");

        return result;

    }

}


public class Main {

    public static void main(String[] args) {

        Enhancer enhancer = new Enhancer();

        enhancer.setSuperclass(Servicio.class);

        enhancer.setCallback(new Interceptor());

        Servicio proxy = (Servicio) enhancer.create();

        proxy.ejecutar();

    }

}


Spring AOP aplica aspectos mediante proxies dinámicos. Si la clase tiene una interfaz, usa JDK Dynamic Proxies; si no, usa CGLIB para generar una subclase dinámica. Esto permite agregar lógica sin modificar el código fuente ni el bytecode.



lunes, 3 de marzo de 2025

Programación Orientada a Objetos en Python parte 2


Ya vimos lo complejo que es crear software y como la programación orientada a objetos nos ayuda.

Ahora veamos como se aplican en Python, empecemos con las reglas de Nomenclatura.

En Python existen convenciones de nomenclatura recomendadas por la PEP 8, que es la guía de estilo oficial del lenguaje. Las reglas principales para definir clases, métodos y atributos:

Nombres de Clases  

- Se usa PascalCase (también conocido como **CamelCase** con la primera letra en mayúscula).  

- Cada palabra comienza con mayúscula, sin guiones bajos.  

Por ejemplo: 

  class MiClase:

      pass


Nombres de Métodos y Atributos Publicos  

- Se usa snake_case (minúsculas separadas por guiones bajos).  

Por ejemplo:

  class Persona:

      def obtener_nombre(self):

          return "Juan"


Nombres de Métodos y Atributos Privados

- Se usa un guion bajo inicial (_) para indicar que es un atributo/método de uso interno (convención, no es privado en sentido estricto).  

Por ejemplo:

  class Persona:

      def _calcular_edad(self):

          return 30


Atributos y Métodos Realmente Privados  

- Se usa doble guion bajo (`__`) para evitar colisiones de nombres en clases hijas (_name mangling_).  

Por ejemplo:

  class Persona:

      def __metodo_secreto(self):

          print("Este método es realmente privado")


Atributos y Métodos Estáticos o de Clase  

- Métodos de clase (@classmethod) y atributos de clase siguen la convención snake_case.  

- Métodos estáticos (@staticmethod) también siguen snake_case.

Por ejemplo:

  class Utilidades:

      @classmethod

      def metodo_de_clase(cls):

          pass


      @staticmethod

      def metodo_estatico():

          pass


Constantes

- Se usan mayúsculas con guiones bajos.  

Por ejemplo:

  class Config:

      MAX_INTENTOS = 5


Definiendo clases

Vista ya la parte teórica, vamos a ver como podemos hacer uso de la programación orientada a objetos en Python. Lo primero es crear una clase, para ello usaremos el ejemplo de perro.


# Creando una clase vacía

class Perro:

    pass


Se trata de una clase vacía y sin mucha utilidad práctica, pero es la mínima clase que podemos crear. Nótese el uso del pass que no hace realmente nada, pero daría un error si después de los : no tenemos contenido.

Ahora que tenemos la clase, podemos crear un objeto de la misma. Podemos hacerlo como si de una variable normal se tratase. Nombre de la variable igual a la clase con (). Dentro de los paréntesis irían los parámetros de entrada si los hubiera.


# Creamos un objeto de la clase perro

mi_perro = Perro()


Definiendo atributos

A continuación vamos a añadir algunos atributos a nuestra clase. Antes de nada es importante distinguir que existen dos tipos de atributos:

Atributos de instancia: Pertenecen a la instancia de la clase o al objeto. Son atributos particulares de cada instancia, en nuestro caso de cada perro.

Atributos de clase: Se trata de atributos que pertenecen a la clase, por lo tanto serán comunes para todos los objetos.

Empecemos creando un par de atributos de instancia para nuestro perro, el nombre y la raza. Para ello creamos un método __init__ que será llamado automáticamente cuando creemos un objeto. Se trata del constructor.


class Perro:

    

    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Ahora que hemos definido el método init con dos parámetros de entrada, podemos crear el objeto pasando el valor de los atributos. Usando type() podemos ver como efectivamente el objeto es de la clase Perro.


mi_perro = Perro("Toby", "Bulldog")

print(type(mi_perro))

# Creando perro Toby, Bulldog

# <class '__main__.Perro'>


Seguramente te hayas fijado en el self que se pasa como parámetro de entrada del método. Es una variable que representa la instancia de la clase, y deberá estar siempre ahí.

El uso de __init__ y el doble __ no es una coincidencia. Cuando veas un método con esa forma, significa que está reservado para un uso especial del lenguaje. En este caso sería lo que se conoce como constructor. 

Por último, podemos acceder a los atributos usando el objeto y . (el punto)


print(mi_perro.nombre) # Toby

print(mi_perro.raza)   # Bulldog


Hasta ahora hemos definido atributos de instancia, ya que son atributos que pertenecen a cada perro concreto. Ahora vamos a definir un atributo de clase, que será común para todos los perros. Por ejemplo, la especie de los perros es algo común para todos los objetos Perro.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


Dado que es un atributo de clase, no es necesario crear un objeto para acceder al atributos. Podemos hacer lo siguiente.


print(Perro.especie)

# mamífero


Se puede acceder también al atributo de clase desde el objeto.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.especie

# 'mamífero'


De esta manera, todos los objetos que se creen de la clase perro compartirán ese atributo de clase, ya que pertenecen a la misma.


Definiendo métodos

En realidad cuando usamos __init__ anteriormente ya estábamos definiendo un método, solo que uno especial. A continuación vamos a ver como definir métodos que le den alguna funcionalidad interesante a nuestra clase, siguiendo con el ejemplo de perro.

Vamos a codificar dos métodos, ladrar y caminar. El primero no recibirá ningún parámetro y el segundo recibirá el número de pasos que queremos andar. Como hemos indicado anteriormente self hace referencia a la instancia de la clase. Se puede definir un método con def y el nombre, y entre () los parámetros de entrada que recibe, donde siempre tendrá que estar self el primero.


class Perro:

    # Atributo de clase

    especie = 'mamífero'


    # El método __init__ es llamado al crear el objeto

    def __init__(self, nombre, raza):

        print(f"Creando perro {nombre}, {raza}")


        # Atributos de instancia

        self.nombre = nombre

        self.raza = raza


    def ladra(self):

        print("Guau")


    def camina(self, pasos):

        print(f"Caminando {pasos} pasos")


Por lo tanto si creamos un objeto mi_perro, podremos hacer uso de sus métodos llamándolos con . y el nombre del método. Como si de una función se tratase, pueden recibir y devolver argumentos.


mi_perro = Perro("Toby", "Bulldog")

mi_perro.ladra()

mi_perro.camina(10)


# Creando perro Toby, Bulldog

# Guau

# Caminando 10 pasos


Métodos en Python: instancia, clase y estáticos

Es posible crear diferentes tipos de métodos:

  • Lo métodos de instancia “normales” que ya hemos visto como metodo()
  • Métodos de clase usando el decorador @classmethod
  • Y métodos estáticos usando el decorador @staticmethod

En la siguiente clase tenemos un ejemplo donde definimos los tres tipos de métodos.


class Clase:

    def metodo(self):

        return 'Método normal', self


    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


    @staticmethod

    def metodoestatico():

        return "Método estático"


Veamos su comportamiento en detalle uno por uno.


Métodos de instancia

Los métodos de instancia son los métodos normales, de toda la vida, que hemos visto anteriormente. Reciben como parámetro de entrada self que hace referencia a la instancia que llama al método. También pueden recibir otros argumentos como entrada.

Para saber más: El uso de "self" es totalmente arbitrario. Se trata de una convención acordada por los usuarios de Python, usada para referirse a la instancia que llama al método, pero podría ser cualquier otro nombre. Lo mismo ocurre con "cls", que veremos a continuación.


class Clase:

    def metodo(self, arg1, arg2):

        return 'Método normal', self


Y como ya sabemos, una vez creado un objeto pueden ser llamados.


mi_clase = Clase()

mi_clase.metodo("a", "b")

# ('Método normal', <__main__.Clase at 0x10b9daa90>)


En vista a esto, los métodos de instancia:

  •  Pueden acceder y modificar los atributos del objeto.
  •  Pueden acceder a otros métodos.
  •   Dado que desde el objeto self se puede acceder a la clase con ` self.class`, también pueden modificar el estado de la clase


Métodos de clase (classmethod)

A diferencia de los métodos de instancia, los métodos de clase reciben como argumento cls, que hace referencia a la clase. Por lo tanto, pueden acceder a la clase pero no a la instancia.


class Clase:

    @classmethod

    def metododeclase(cls):

        return 'Método de clase', cls


Se pueden llamar sobre la clase.


Clase.metododeclase()

# ('Método de clase', __main__.Clase)


Pero también se pueden llamar sobre el objeto.


mi_clase.metododeclase()

# ('Método de clase', __main__.Clase)


Por lo tanto, los métodos de clase:

  • No pueden acceder a los atributos de la instancia.
  • Pero si pueden modificar los atributos de la clase.
  • Métodos estáticos (staticmethod)

Por último, los métodos estáticos se pueden definir con el decorador @staticmethod y no aceptan como parámetro ni la instancia ni la clase. Es por ello por lo que no pueden modificar el estado ni de la clase ni de la instancia. Pero por supuesto pueden aceptar parámetros de entrada.


class Clase:

    @staticmethod

    def metodoestatico():

        return "Método estático"


mi_clase = Clase()

Clase.metodoestatico()

mi_clase.metodoestatico()


# 'Método estático'

# 'Método estático'


Por lo tanto el uso de los métodos estáticos pueden resultar útil para indicar que un método no modificará el estado de la instancia ni de la clase. Es cierto que se podría hacer lo mismo con un método de instancia por ejemplo, pero a veces resulta importante indicar de alguna manera estas peculiaridades, evitando así futuros problemas y malentendidos.

En otras palabras, los métodos estáticos se podrían ver como funciones normales, con la salvedad de que van ligadas a una clase concreta.

sábado, 1 de marzo de 2025

Programación Orientada a Aspectos (AOP) en Spring


La Programación Orientada a Aspectos (AOP, Aspect-Oriented Programming) es un paradigma que permite separar las preocupaciones transversales del código principal, como logging, seguridad, manejo de transacciones y monitoreo. Spring proporciona Spring AOP y compatibilidad con AspectJ para implementar esta funcionalidad de manera eficiente.

En una aplicación, hay funcionalidades que se repiten en diferentes partes del código (logging, manejo de excepciones, validaciones). Sin AOP, estas funciones deben implementarse de forma manual en cada clase, lo que genera código repetitivo y difícil de mantener.

AOP permite interceptar la ejecución de métodos y agregar lógica adicional sin modificar la implementación original.

Veamos un ejemplo. Para implementar un aspecto en Spring, necesitamos habilitar AOP y definir un aspecto con consejos (`advice`).

Primero agregamos la dependencia, en este ejemplo uso maven: 


<dependency>

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

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

</dependency>


@Configuration

@EnableAspectJAutoProxy

@ComponentScan("com.example")

public class AppConfig {

}


Ahora definimos el aspecto de Logging


import org.aspectj.lang.annotation.*;

import org.springframework.stereotype.Component;


@Aspect

@Component

public class LoggingAspect {

    

    @Before("execution(* com.example.service.*.*(..))")

    public void logBeforeMethod() {

        System.out.println("[LOG] - Método invocado...");

    }

}


@Aspect: Indica que la clase es un aspecto.

@Before("execution(* com.example.service.*.*(..))"): Aplica el aspecto antes de ejecutar cualquier método dentro del paquete `com.example.service`.

execution(* com.example.service.*.*(..)): Todos los métodos de cualquier clase en com.example.service.

Podemos usar @Around para envolver la ejecución de un método y medir su tiempo de respuesta.


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.*;

import org.springframework.stereotype.Component;


@Aspect

@Component

public class PerformanceAspect {

    

    @Around("execution(* com.example.service.*.*(..))")

    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();

        Object result = joinPoint.proceed(); // Llama al método original

        long end = System.currentTimeMillis();

        System.out.println("Tiempo de ejecución: " + (end - start) + "ms");

        return result;

    }

}

@Around: Permite modificar la ejecución del método original.

joinPoint.proceed(): Ejecuta el método interceptado.

System.currentTimeMillis(): Mide el tiempo antes y después de la ejecución.


Ahora veamos un ejemplo con AspectJ: Spring AOP solo funciona con beans de Spring, pero si queremos interceptar cualquier clase, podemos usar AspectJ full.

Agregamos a nuestro pom: 


<dependency>

    <groupId>org.aspectj</groupId>

    <artifactId>aspectjweaver</artifactId>

    <version>1.9.7</version>

</dependency>


Habilitamos AspectJ en Spring Boot


@EnableAspectJAutoProxy(proxyTargetClass = true)


Definimos un aspecto con AspectJ


@Aspect

public class SecurityAspect {

    

    @Before("execution(* *(..))") // Aplica a cualquier método

    public void checkSecurity() {

        System.out.println("[SECURITY] - Verificando permisos...");

    }

}

Spring AOP permite separar lógica transversal, reduciendo la repetición de código y mejorando la mantenibilidad. Con @Aspect y @Pointcut, podemos interceptar métodos y agregar funcionalidades como logging, seguridad y performance. Para casos avanzados, AspectJ permite interceptar cualquier clase en la aplicación.


viernes, 28 de febrero de 2025

API de Servidor Web Simple en Java



A partir de Java 18, se introdujo una API de Servidor Web Simple que permite crear servidores HTTP ligeros sin necesidad de dependencias externas. Esta funcionalidad es ideal para pruebas rápidas, prototipos y aplicaciones livianas.

Como características principales podemos nombrar:

  • Servidor HTTP embebido sin necesidad de librerías adicionales.
  • Manejo de solicitudes GET y POST.
  • Facilidad de uso con pocas líneas de código.
  • Soporte para configuración de rutas y respuestas personalizadas.


Veamos un hola mundo : 


import com.sun.net.httpserver.HttpServer;

import com.sun.net.httpserver.HttpHandler;

import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;

import java.io.OutputStream;

import java.net.InetSocketAddress;


public class SimpleWebServer {

    public static void main(String[] args) throws IOException {

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

        server.createContext("/hello", new HelloHandler());

        server.setExecutor(null);

        server.start();

        System.out.println("Servidor iniciado en http://localhost:8080/");

    }


    static class HelloHandler implements HttpHandler {

        @Override

        public void handle(HttpExchange exchange) throws IOException {

            String response = "¡Hola, mundo!";

            exchange.sendResponseHeaders(200, response.length());

            OutputStream os = exchange.getResponseBody();

            os.write(response.getBytes());

            os.close();

        }

    }

}


La API de Servidor Web Simple en Java es una herramienta útil para desarrollar servidores HTTP de forma rápida y sencilla. Aunque tiene limitaciones en cuanto a escalabilidad y características avanzadas, es perfecta para pruebas y aplicaciones pequeñas sin necesidad de frameworks externos.


jueves, 27 de febrero de 2025

Concurrencia en Erlang parte 14


Ahora que sabemos qué tiene que hacer cada componente y cómo comunicarse, una buena idea sería hacer una lista de todos los mensajes que se enviarán y especificar cómo se verán. Comencemos primero con la comunicación entre el cliente y el servidor de eventos.

El cliente puede enviar {subscribe, Self} al servidor de eventos, que solo puede responder con "ok". Tenga en cuenta que tanto el cliente como el servidor se monitorean entre sí. Aquí elegí usar dos monitores porque no hay una dependencia obvia entre el cliente y el servidor. Es decir, por supuesto, el cliente no funciona sin el servidor, pero el servidor puede vivir sin un cliente. Un enlace podría haber hecho el trabajo aquí, pero como queremos que nuestro sistema sea extensible con muchos clientes, no podemos asumir que todos los demás clientes querrán bloquearse cuando el servidor muera. Y tampoco podemos asumir que el cliente realmente se puede convertir en un proceso del sistema y que se produzcan salidas de trampa en caso de que el servidor muera. Ahora, el siguiente conjunto de mensajes.

El cliente puede enviar el mensaje {add, Name, Description, TimeOut}, al que el servidor puede responder 'ok' o {error, Reason}

Esto agrega un evento al servidor de eventos. Se envía una confirmación en forma de átomo ok, a menos que algo salga mal (tal vez el TimeOut esté en el formato incorrecto). La operación inversa, eliminar eventos, se puede realizar de la siguiente manera.

El cliente puede enviar el mensaje {cancel, Name} y el servidor de eventos debería devolver ok como un átomo

El servidor de eventos puede enviar una notificación cuando se deba realizar el evento.

El servidor de eventos reenvía un mensaje {done, Name, Description} al cliente

Luego, solo necesitamos los dos casos especiales siguientes para cuando queremos apagar el servidor o cuando falla.

Cuando el cliente envía el átomo 'shutdown' al servidor de eventos, este se apaga y devuelve {'DOWN', Ref, process, Pid, ​​shutoff} porque fue monitoreado

No se envía una confirmación directa cuando el servidor se apaga porque el monitor ya nos advertirá de eso. Eso es prácticamente todo lo que sucederá entre el cliente y el servidor de eventos. Ahora, los mensajes entre el servidor de eventos y los procesos de eventos en sí.

Una cosa a tener en cuenta aquí antes de empezar es que sería muy útil tener el servidor de eventos vinculado a los eventos. La razón de esto es que queremos que todos los eventos mueran si el servidor lo hace: no tienen sentido sin él.

Bien, volvamos a los eventos. Cuando el servidor de eventos los inicia, le da a cada uno de ellos un identificador especial (el nombre del evento). Una vez que llega el momento de uno de estos eventos, necesita enviar un mensaje que lo indique:

Un evento puede enviar {done, Id} al servidor de eventos

Por otro lado, el evento tiene que estar atento a las llamadas de cancelación del servidor de eventos.

El servidor envía 'cancel' a un evento, que responde con 'ok'

Y eso debería ser todo. Se necesitará un último mensaje para nuestro protocolo, el que nos permite actualizar el servidor.

el servidor de eventos tiene que aceptar un mensaje 'code_change' del shell

No es necesaria ninguna respuesta. Veremos por qué cuando realmente programemos esa función y verás que tiene sentido.

Una vez definido el protocolo y con la idea general de cómo se verá nuestra jerarquía de procesos, podemos empezar a trabajar en el proyecto.

miércoles, 26 de febrero de 2025

API de Acceso a Memoria Externa en Java


La API de Acceso a Memoria Externa (Foreign Function & Memory API) en Java es una funcionalidad introducida para interactuar de manera segura y eficiente con memoria fuera del heap de Java. Esta API ha evolucionado en versiones recientes y permite mejorar la interoperabilidad con código nativo sin necesidad de JNI (Java Native Interface).

Esta API proporciona una forma segura y estructurada de acceder a la memoria fuera del control del recolector de basura, lo que permite mejorar el rendimiento en aplicaciones que requieren interacción con bibliotecas nativas o manipulación intensiva de datos.

Veamos un ejemplo básico de cómo asignar y manipular memoria externa en Java utilizando la API:


import java.lang.foreign.*;

import java.lang.invoke.VarHandle;


public class ForeignMemoryExample {

    public static void main(String[] args) {

        try (MemorySegment segment = MemorySegment.allocateNative(100)) {

            VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

            intHandle.set(segment, 0, 42);

            System.out.println("Valor almacenado: " + intHandle.get(segment, 0));

        }

    }

}


Casos de uso:

  •  Interoperabilidad con bibliotecas en C: Facilita la comunicación con código nativo sin JNI.
  •  Manipulación de grandes volúmenes de datos: Ideal para manejar grandes bloques de memoria sin afectar el GC.
  •  Optimización en aplicaciones de alto rendimiento: Como motores de bases de datos y procesamiento de señales.


La API de Acceso a Memoria Externa en Java representa un avance importante en la forma en que interactuamos con memoria nativa. Al ofrecer una interfaz más segura y eficiente, facilita el desarrollo de aplicaciones de alto rendimiento sin comprometer la seguridad y estabilidad del código Java.


lunes, 24 de febrero de 2025

Pattern Matching en Java


La coincidencia de patrones (Pattern Matching) en switch es una característica introducida en Java 17 como una mejora en la sintaxis del switch, permitiendo evaluar tipos de manera más sencilla y concisa. Esta funcionalidad facilita la escritura de código más limpio y seguro al eliminar la necesidad de casting manual.

Veamos un ejemplo, antes de java 17 haciamos:

 

static void procesar(Object obj) {

    switch (obj.getClass().getSimpleName()) {

        case "String":

            String s = (String) obj;

            System.out.println("Es una cadena: " + s.toUpperCase());

            break;

        case "Integer":

            Integer i = (Integer) obj;

            System.out.println("Es un número: " + (i * 2));

            break;

        default:

            System.out.println("Tipo no soportado");

    }

}


Y en Java 17: 


static void procesar(Object obj) {

    switch (obj) {

        case String s -> System.out.println("Es una cadena: " + s.toUpperCase());

        case Integer i -> System.out.println("Es un número: " + (i * 2));

        default -> System.out.println("Tipo no soportado");

    }

}


Esta disponible a partir de Java 17 en vista previa y consolidado en versiones posteriores. Algo a tener en cuanta es que solo se puede utilizar en switch con expresiones y no con estructuras más antiguas.

El pattern matching en switch es una mejora significativa que permite un código más limpio y expresivo. Reduce el uso de casts y mejora la legibilidad del código, haciendo que la programación en Java sea más moderna y eficiente.


sábado, 22 de febrero de 2025

Clases Selladas en Java


Las clases selladas en Java, introducidas en Java 15 como una característica en vista previa y estandarizadas en Java 17, permiten restringir qué clases pueden heredar de una clase base, mejorando la seguridad y el diseño del código.

Las clases selladas (sealed) establecen un conjunto específico de clases que pueden extender o implementar una clase base, evitando la herencia no controlada.


public sealed class Figura permits Circulo, Rectangulo {}


public final class Circulo extends Figura {}

public final class Rectangulo extends Figura {}


Aquí, Figura es una clase sellada, lo que significa que solo Circulo y Rectangulo pueden heredar de ella.

Las clases que extienden una clase sellada deben usar una de las siguientes opciones:

  • final: No permite más subclases.
  • sealed: Permite definir otro conjunto restringido de subclases.
  • non-sealed: Permite herencia sin restricciones.


Ejemplo con diferentes modificaciones:


public sealed class Figura permits Circulo, Poligono {}


public final class Circulo extends Figura {}

public sealed class Poligono extends Figura permits Triangulo {}

public non-sealed class Triangulo extends Poligono {}


Las clases selladas en Java proporcionan un control más preciso sobre la herencia, facilitando la definición de modelos de dominio más seguros y expresivos. Son especialmente útiles cuando se combinan con switch y pattern matching para estructurar mejor el código.

viernes, 21 de febrero de 2025

Programación Orientada a Objetos en Python


Nos encontramos hoy en día en un mundo en donde debemos realizar software que cumpla al menos con las siguientes características, las cuales al menos hacen que ésta tarea sea bastante compleja:

    • El Dominio del Problema: muchas veces las herramientas y tecnologías a utilizar dependen del problema que queremos resolver, ésto quiere decir que en base al dominio del mismo se definen requisitos, características y modelos que nosotros como desarrolladores debemos plasmar; los mismos hacen que sea difícil diseñar soluciones adaptables a varios escenarios, ya que dichos requisitos son muchas veces contradictorios.

    • El Proceso de Desarrollo: si nos encontramos desarrollando software de mediano o gran porte, ésto hace que necesitemos trabajar en equipo, en conjunto con uno o más desarrolladores. Si bien ésto suena simple, trabajar en equipo requiere coordinar y definir metodologías de trabajo, nomenclaturas de archivos, estilos, efectos visuales, entre otras cosas; además muchas veces trae aparejada la tarea de desarrollar de forma que tal que una modificación realizada por una persona no vuelva atrás o cambie una funcionalidad hecha por otro desarrollador.

    • La Flexibilidad del software: muy raras veces un prototipo o primera versión de un sistema que desarrollados será lo que finalmente nuestro cliente use, generalmente el mismo querrá cambios o agregar/eliminar funcionalidades; puesto que el software constantemente cambia. En algunos proyectos de desarrollo algunas veces lamentablemente se debe cambiar de lenguaje o de tecnología, porque lo diseñado no puede ser escalable a las necesidades del cliente; ésto es visto y considerado como una gran pérdida de tiempo.

    • La Reutilización del código: al desarrollar sistemas similares o con características iguales, en el mundo de la Ingeniería del Software se trata de aplicar el principio de “No reinventar la rueda”, ésto quiere decir que nuestro código debe poder ser reusable en varios escenarios y en la medida de lo posible en distintos sistemas.

Principalmente por éstos 4 puntos decimos que el software es complejo y que su ciclo de desarrollo también lo es; es por ésto que se hace necesario contar con alguna herramienta o tecnología que nos permita sortear éstos problemas.

Definimos a la POO como a un paradigma de programación, es decir a una forma de analizar, diseñar y realizar soluciones en el mundo del software. Existen distintos paradigmas de programación (funcional, lógico, procedural, entre otros) los cuales tienen como objetivo el dar un marco de trabajo para el diseño y desarrollo de soluciones de software. En el caso de la POO vamos a pensar, diseñar e implementar nuestro sistema como un conjunto de Objetos que se comunican entre sí mediante mensajes para llevar a cabo un objetivo en común.

Objetivos que platea este Paradigma

    • Escribir software fácilmente modificable y escalable.

    • Escribir software reusable.

    • Disponer de un modelo natural para representar un dominio.


Ventajas que ofrece

    • Fomenta la reutilización del código.

    • Permite desarrollar software más flexible al cambio y escalable.

    • Nos ofrece una forma de pensar más cercana a la realidad de las personas.


Para desarrollar aplicaciones orientadas a objetos necesitamos poseer la cualidad de la abstracción. Esto consiste en aislar un elemento de su contexto y tomar de él sólo las características que importan del mismo y las acciones que éste puede realizar. 

Un mismo objeto (una Persona) puede ser observado desde diferentes puntos de vista dependiendo de su contexto y de lo que se desee modelar. Si estamos desarrollando un sistema de gestión de personal para una empresa, importaran sus datos personales (sexo, fecha de nacimiento, DNI); mientras que si programamos una aplicación para resultados de encuestas importarán más otros datos (opinión, postura a favor de ciertos aspectos, etc). 

El objetivo de la Abstracción es identificar objetos en el dominio del problema que buscamos resolver y definir claramente que es lo que cada uno puede hacer (comportamiento) y qué información nos interesa almacenar de cada uno (atributos).

En el mundo de la POO nos encontraremos con dos herramientas, las cuales son esenciales para el funcionamiento del paradigma, las cuales son:


Clases 

    • Son el molde o modelo sobre el cual se crean los objetos.

    • Definen atributos y comportamientos comunes a todos los objetos de la misma clase.

    • Son considerados como un plano o molde sobre el cual creamos los objetos.

    • Son definiciones estáticas.


Objetos

    • Están modelados en función de una clase.

    • Son entidades dinámicas, es decir, son los que se ejecutan durante el ciclo de vida de un programa.

    • Son instancias de una clase dada.


Denominaremos de ésta forma a las entidades que se ejecutan en nuestro sistema para poder llevar a cabo un objetivo o funcionalidad, colaborando entre sí. Todo objeto es capaz de almacenar información en sus atributos y de realizar operaciones a través de sus métodos. 

Cada entidad almacenará información dentro de su estructura, lo que le permitirá brindar o modificar esa información y además podrá interactuar con sí mismo o con otros objetos mediante operaciones o métodos, generalmente son éstos métodos o procedimientos los que permiten modificar u obtener información almacenada en una entidad u objeto. Podemos decir que la información que un objeto posee determina su estado, mientras que los métodos que el mismo contiene determinan su comportamiento. 

Definimos como estado, a el valor que contienen todos los atributos de un objeto en un momento dado. Una persona puede tener 33 años, tener por nombre Juan y por Apellido González y poseer una altura de 1.78, en éste caso estamos haciendo referencia a atributos deseables para un objeto de tipo Persona. 

Es recomendable no permitir modificar la información de un objeto (almacenada en sus atributos) con total libertad, sino que publicar esa información a través de su comportamiento o métodos, ésto se conoce como encapsulamiento.  A la hora de clasificar los atributos o propiedades de un objeto, podemos decir que el mismo tiene propiedades inherentes a cada objeto (propiedades dinámicas) y propiedades comunes a todos los objetos del mismo tipo (propiedades estáticas), exploraremos éstos conceptos más adelante en este capítulo.

Además de almacenar información, todo objeto puede realizar distintas acciones, comunicándose o no con otros objetos. Definimos ésta característica como el comportamiento de un objeto, el cual va a estar determinado por los distintos métodos que el objeto que posea y utilice. Es deseable en la mayoría de los casos que nos comuniquemos con un objeto a través de sus métodos y que, en todo caso, sean ellos los que modifican o publiquen la información del mismo.

Cada objeto es una instancia de una clase dada, por lo tanto debe ser identificable en todo momento y diferenciarse del resto. No debemos confundir el estado de un objeto con su identidad, ya que el estado puede ir variando en la medida en que cambia el valor de los atributos de un objeto, pero su identidad permanece constante durante todo su ciclo de vida. En muchos lenguajes de programación la identidad está definida por una variable que almacena la dirección de memoria de un objeto (similar al concepto de puntero en otros lenguajes), a través de la cual podemos acceder al comportamiento y a los métodos del mismo. 

En una aplicación OOP nos encontraremos con muchas instancias de objeto en la memoria, las cuales no actúan por sí solas, sino que colaboran en conjunto para llevar a cabo la ejecución del programa. Para realizar esta tarea dichos objetos se comunican a través de mensajes, es decir existirán objetos que emiten y objetos que reciben mensajes, por ejemplo un objeto Persona podría crear un objeto Venta y luego podría comunicarse con él mismo y solicitarle que realice una transacción, esta comunicación se da mediante el envío de un mensaje desde el primer objeto al segundo. Generalmente el objeto receptor del mensaje ejecuta un método como respuesta al llamado del mensaje.

Así como definimos a los objetos como entidades dinámicas en la ejecución de una aplicación POO, definiremos a las clases como entidades estáticas que se encargan de la definición de los atributos y métodos comunes a todos los objetos que sean instancias de ellas. Es decir, una clase es como un molde que define todo lo común que luego será utilizado por todos los objetos que sean instancias de sí. 

Un error muy común en el mundo de la POO es confundir y utilizar los términos “Objeto” y “Clase” en forma similar, veamos algunas diferencias:

    • En primer lugar las clases sirven como herramienta para la definición de atributos y métodos, pero solamente realizan esa tarea. Los objetos en cambio son los que llevan a cabo la ejecución del programa, es decir, contienen valores dentro de sus atributos y ejecutan el comportamiento de sus métodos, todo ésto de acuerdo a la clase de la que son instancia.

    • También podemos definir como diferencia a la relación que existe entre los mismos, es decir, un objeto es instancia de una sola clase, pero de una clase pueden existir N instancias (objetos) distintos al mismo tiempo.

Por lo tanto, resumiendo, las clases son entidades de definición (estáticas) y los objetos entidades de ejecución (dinámicas). 

Llamamos entonces instanciación al proceso de creación de un nuevo objeto (instancia) a partir de su clase dada y clasificación al proceso de definir la clase de la cual es instancia un objeto.

Al existir en la memoria en un momento dado, podemos decir que el estado de un objeto está formado por el valor de todos los atributos del mismo en un momento dado. Es decir que, por ejemplo, una instancia de la clase Persona tendrá un nombre, apellido y edad en un momento dado del programa, una instancia de la clase Venta tendrá una fecha de venta y un estado, una instancia de la clase Usuario tendrá un nombre y contraseña. Con esto queremos decir que el valor de los atributos define el estado de un objeto, el cual va variando con la ejecución de la aplicación.

Me quedo super largo, voy a tener que hacer varios post...

lunes, 17 de febrero de 2025

Pattern matching para instanceof en Java


Pattern matching para instanceof es una característica introducida en Java 14 como una mejora para escribir código más conciso y seguro al realizar comprobaciones de tipo y conversiones.

Antes de Java 14, verificar si un objeto era de cierto tipo y luego convertirlo requería código repetitivo:


if (obj instanceof String) {

    String str = (String) obj;

    System.out.println("Longitud: " + str.length());

}


La coincidencia de patrones para `instanceof` elimina la necesidad del casting explícito, reduciendo la repetición:


if (obj instanceof String str) {

    System.out.println("Longitud: " + str.length());

}


La coincidencia de patrones se puede usar en estructuras más complejas como `else if`:


public void imprimir(Object obj) {

    if (obj instanceof String str) {

        System.out.println("Es un String: " + str);

    } else if (obj instanceof Integer num) {

        System.out.println("Es un Integer: " + num);

    } else {

        System.out.println("Tipo desconocido");

    }

}


La variable resultante solo está disponible dentro del bloque if, lo que evita posibles accesos indebidos:


if (obj instanceof String str) {

    System.out.println(str.length()); // Válido

}

System.out.println(str); // Error: No está definido fuera del if


La coincidencia de patrones para instanceof simplifica la escritura de código en Java, haciéndolo más expresivo y seguro. Esta funcionalidad es el primer paso hacia mejoras más avanzadas en la manipulación de tipos en el lenguaje.


Lombok @Builder: Simplificando la Creación de Objetos en Java


En el blog nunca he hablado de lombok, porque no me gusta que exista un framework que haga lo que tendria que tener el lenguaje que programamos. Pero esta funcionalidad de lombok no la conocia y me esta buena. 

La creación de objetos con múltiples atributos puede volverse tediosa y propensa a errores si se usa el enfoque tradicional de constructores o setters. Lombok ofrece la anotación @Builder, que es una solución elegante que permite generar automáticamente un patrón de construcción de objetos de manera fluida y legible.

Lombok es una librería que reduce el código repetitivo en Java mediante anotaciones que generan automáticamente métodos como getter, setter, equals, hashCode y toString. Una de sus anotaciones más útiles es @Builder, que facilita la creación de objetos sin necesidad de escribir constructores manualmente.

Para agregar Lombok, si usas Maven, agrega la siguiente dependencia:


<dependency>

    <groupId>org.projectlombok</groupId>

    <artifactId>lombok</artifactId>

    <version>1.18.26</version>

    <scope>provided</scope>

</dependency>


Si usas Gradle:


dependencies {

    compileOnly 'org.projectlombok:lombok:1.18.26'

    annotationProcessor 'org.projectlombok:lombok:1.18.26'

}


Veamos un ejemplo de una clase que tiene un builder: 


import lombok.Builder;

import lombok.Getter;

import lombok.ToString;


@Getter

@ToString

@Builder

public class Usuario {

    private String nombre;

    private String email;

    private int edad;

}

public class Main {

    public static void main(String[] args) {

        Usuario usuario = Usuario.builder()

                .nombre("Juan Pérez")

                .email("juan.perez@example.com")

                .edad(30)

                .build();

        

        System.out.println(usuario);

    }

}


Se puede personalizar el constructor y el nombre del método de construcción con @Builder, veamos un ejemplo :


@Builder(builderMethodName = "nuevoUsuario")

public class Usuario {

    private String nombre;

    private String email;

    private int edad;

}


Uso:


Usuario usuario = Usuario.nuevoUsuario()

        .nombre("Ana López")

        .email("ana.lopez@example.com")

        .edad(28)

        .build();


El uso de @Builder en Lombok simplifica la creación de objetos en Java, haciendo que el código sea más conciso, flexible y fácil de mantener. Esta anotación es especialmente útil cuando se manejan objetos con múltiples atributos opcionales, evitando la necesidad de escribir múltiples constructores sobrecargados.


Dejo link:

https://www.baeldung.com/lombok-builder

Concurrencia en Erlang parte 13


Soy una persona un poco desorganizada. Con suerte, todavía necesitas recordatorios de lo que tienes que hacer, porque vamos a escribir una de estas aplicaciones de recordatorio de eventos que te incitan a hacer cosas y te recuerdan las citas.

El primer paso es saber qué diablos estamos haciendo. "Una aplicación de recordatorios", dices. "Por supuesto", digo yo. Pero hay más. ¿Cómo planeamos interactuar con el software? ¿Qué queremos que haga por nosotros? ¿Cómo representamos el programa con procesos? ¿Cómo sabemos qué mensajes enviar?

Como dice la cita, "Caminar sobre el agua y desarrollar software a partir de una especificación son fáciles si ambos están congelados". Así que obtengamos una especificación y apeguémonos a ella. Nuestro pequeño software nos permitirá hacer lo siguiente:

  • Agregar un evento. Los eventos contienen una fecha límite (el momento en el que se debe advertir), un nombre de evento y una descripción.
  • Mostrar una advertencia cuando haya llegado el momento.
  • Cancelar un evento por nombre.

Sin almacenamiento en disco persistente. No es necesario para mostrar los conceptos arquitectónicos que veremos. Sería un fastidio para una aplicación real, pero en su lugar solo mostraré dónde se podría insertar si quisiera hacerlo y también señalaré algunas funciones útiles. Dado que no tenemos almacenamiento persistente, tenemos que poder actualizar el código mientras se está ejecutando.

La interacción con el software se realizará a través de la línea de comandos, pero debería ser posible ampliarla más adelante para que se puedan utilizar otros medios (por ejemplo, una GUI, acceso a una página web, software de mensajería instantánea, correo electrónico, etc.)

Esta es la estructura del programa que elegí para hacerlo:

Hay 5 componentes: Un cliente (1) que puede comunicarse con un servidor de eventos (2) y 3 pequeños círculos etiquetados como 'x', 'y' y 'z'. Los tres están vinculados al servidor de eventos.

Donde el cliente, el servidor de eventos y x, y y z son todos procesos. Esto es lo que cada uno de ellos puede hacer:

Servidor de eventos

  • Acepta suscripciones de clientes
  • Reenvía notificaciones de procesos de eventos a cada uno de los suscriptores
  • Acepta mensajes para agregar eventos (e iniciar los procesos x, y, z necesarios)
  • Puede aceptar mensajes para cancelar un evento y, posteriormente, matar los procesos de eventos
  • Puede ser finalizado por un cliente
  • Puede hacer que su código se vuelva a cargar a través del shell.

Cliente

Se suscribe al servidor de eventos y recibe notificaciones como mensajes. Por lo tanto, debería ser fácil diseñar un grupo de clientes que se suscriban al servidor de eventos. Cada uno de ellos podría ser potencialmente una puerta de entrada a los diferentes puntos de interacción mencionados anteriormente (GUI, página web, software de mensajería instantánea, correo electrónico, etc.)

  • Pide al servidor que agregue un evento con todos sus detalles
  • Pide al servidor que cancele un evento
  • Monitorea el servidor (para saber si se cae)
  • Apaga el servidor de eventos si es necesario

x, y y z:

  • Representan una notificación que espera ser activada (básicamente son solo temporizadores vinculados al servidor de eventos)
  • Envían un mensaje al servidor de eventos cuando se acaba el tiempo
  • Reciben un mensaje de cancelación y mueren

Tenga en cuenta que todos los clientes (mensajería instantánea, correo, etc. que no están implementados en este libro) reciben notificaciones sobre todos los eventos, y una cancelación no es algo sobre lo que advertir a los clientes. Aquí el software está escrito para usted y para mí, y se supone que solo un usuario lo ejecutará.

Esto representa cada proceso que tendremos. Al dibujar todas las flechas allí y decir que son mensajes, hemos escrito un protocolo de alto nivel, o al menos su esqueleto.

Se debe tener en cuenta que usar un proceso por evento para recordar probablemente sea excesivo y difícil de escalar en una aplicación del mundo real. Sin embargo, para una aplicación de la que será el único usuario, esto es suficiente. Un enfoque diferente podría ser usar funciones como timer:send_after/2-3 para evitar generar demasiados procesos.