La evolucion hacia una sentencia funcional en Java (Parte 1)

    Uno de los problemas con los que tuve que lidiar a la hora de aprender java fue dar a cada temática una mirada holística, es decir panorámica e integral para lograr entenderla en profundidad. Cuando leemos una ayuda, un manual, un tutorial, inclusive cuando practicamos usando un IDE terminamos adquiriendo un conocimiento, lo entendemos, lo sabemos pero eso no quiere decir que realmente lo comprendamos, tomando para mi, la definición de comprensión como el acto que nos permite tomar el mejor camino para la creación de determinada cosa, quizás esto se resume en la palabra inglesa skill.
Doy un ejemplo (tengo planificado en alguna otra entrada hablar mas profundamente de esto), relacionado al tema de la programación orientada a objetos, respecto a java leí varios manuales que explicaban el tema, también libros de ingeniería informática , recuerdo haberlo visto en antiguas clases de c++ en la universidad allá lejos y hace tiempo, lo vi en cursos, también a la hora de practicar programación, nunca estuve satisfecho a la hora de evaluarme, siempre quedaban zonas oscuras, siempre me sentía sin el material mental suficiente para solucionar un problema de programación con POO de manera consistente y  estructurada; hasta que una mañana recordé algo leído en la metafísica de Aristóteles y se me aclaró todo, note que toda esa bibliografía nos da de manera fragmentada un conocimiento que debemos unir para entender realmente la cuestión subyascente, inclusive sin necesidad de programar, di un paso mas y me vi enfrentado al patrón modelo vista controlador como una consecuencia de entender lo anterior. 
Yendo al tema de esta entrada, me pasó lo mismo con la cuestión de las interfaces, las interfaces funcionales, la programación declarativa en java 8 y la necesidad de entender el porqué de las cosas. Y por eso titulé a la entrada como una evolución hacia una sentencia declarativa, que es hacia donde la tendencia actual en programación java nos empuja, y es que cuando abrimos un programa profesional y moderno de java es muy probable que nos estemos topando con lambdas, interfaces funcionales con argumentos genéricos, streams, etc, y podemos usarlos exitosamente y hasta de manera intuitiva, pero  ¿realmente sabemos el porqué, el cómo y el cuándo? 

Programación imperativa vs declarativa


No voy a describir mucho la evolución del lenguaje java hasta nuestros días, lo que si creo necesario como prefacio es entender lo que significa esta manera de encarar la resolución de un problema en programación.
Tomemos a la programación imperativa como eso que venimos haciendo normalmente, en todos los lenguajes en donde encaramos las soluciones como bloques estancos ordenados secuencialmente o no pero con un desarrollo "a medida" si se me permite la expresión. Así puedo tener un bloque de código como este:

int contMenores=0;
for(Persona p:personas){
     if(p.getEdad()<menor)
     contMenores++;
 }

    Quizás no es el mejor ejemplo en tanto complejidad, porque viendo un poco el código interno de ese ciclo podemos inferir que lo que hace ese bloque es calcular la cantidad de personas menores a una edad dada como parámetro variable. Tenemos entonces leyendo lo anterior, unas cuantas palabras claves: "inferir", "ver el codigo interno"; quizás si no hubiésemos cumplido con ciertas prerrogativas de la buena programación..., (por ejemplo podríamos haber reemplazado el id menor por x, y el contMenores por  j), entonces quizás aun se nos hubiera complicado más saber lo que hace ese bloque. Aquí es cuando llegamos a la definición formal de la programación imperativa, y se dice que es aquella en que vemos el cómo por encima del qué, lo verán así en la mayoría de las definiciones, pero en este caso mio quería ejemplificar el problema que deviene de esta filosofía. Imaginemos por un momento tener un bloque como esos con 3 o 4 fors anidados, condicionales y switchs internos.. ya no sería muy fácil determinar exactamente qué hace el bloque o cada línea (sería la solución comentar todo?), aun así tendríamos el problema de la depuración ulterior.
Sin entrar por ahora en mayores detalles, tenemos una equivalencia al bloque anterior :

Long contMenores=  listaPers.stream()
                .filter(p->p.getEdad()<menor)
                .count();

¿Qué es lo primero que vemos? que en el mismo bloque de código queda implícita la funcionalidad,  por definición entonces estamos viendo un bloque de programación declarativa o funcional . No necesitamos por ende decodificar lo que vemos, no nos preocupemos por ahora por la abstracción que representan los streams, no nos preocupamos por un ciclo que recorra nuestro listado, sólo vemos y comprendemos lo que esta haciendo la sentencia.

Evolucionemos el código


    Hablamos de tutoriales y manuales, y cuando llegamos al punto de las interfaces en java podemos leer una y otra vez las mismas definiciones, las entendemos ya que son claras, dicen : una interface es una especie de clase-contrato que nos obliga a implementar los métodos especificados en su interior cuando la heredamos :

public interface inter {
        void metodo1();
        int metodo2();
        void metodo3(int x, int y);
    }
   Les podemos llamar métodos abstractos aunque es algo redundante hablando de interfaces y no de clases abstractas.
    También es interesante destacar que una interface puede ser pensada como un tipo de dato definido por el usuario, recordar esto porque volveré sobre este concepto mas adelante.
   Cuando heredamos una interface (mediante la etiqueta implements) sí o sí debemos implementar (llenar de código) los métodos definidos en su declaración.
    La aplicación de esta filosofía esta algo difusa, mas allá de presuponer que el uso de interfaces permite la actualización y la consistencia de librerías, personalmente no encontré algo que me dejara clara su utilidad. Así fue que tomando diversas fuentes de información entre otras el excelente manual de oracle, fuí deconstruyendo a las interfaces a partir de la belleza de la programación declarativa. Si las interfaces nos permiten ir en dirección de una programación elegante y sólida, pues veamos cuando y cómo las implementamos.

Planteemos el siguiente problema:

1) una clase Persona.
2) un listado de objetos de esa clase.
3) una consulta sobre la cantidad de personas mayores a cierta edad.

Solución:

public class Persona {
   private int edad;
   public static enum Genero{
       MASCULINO,FEMENINO
   }

   private String nombre,apellido;
   private Genero gen;

    public Persona(int edad, String nombre, Genero gen) {
        this.edad = edad;
        this.nombre = nombre;
        this.gen = gen;
    }

    public int getEdad() {
        return edad;
    }

    public String getNombre() {
        return nombre;
    }

    public String getApellido() {
        return apellido;
    }

    public Genero getGen() {
        return gen;
    }
   public void imprimirNombre(){
       System.out.println(this.nombre);
   }
   
}

Estructuremos la solución prolijamente, podríamos hacer una clase con un método que nos permita enviarle como parámetros una lista de objetos persona y una variable que marque el corte de edad.
Mas o menos así:

public class EvaluarPersona {
    public static void imprimirMasViejoQue(List<Persona> lista, int edad){
        for (Persona p: lista){
            if(p.getEdad()>edad){
                p.imprimirNombre();
            }
        }
    }
}

Y ahora usamos el código:

EvaluarPersona.imprimirMasViejoQue(listaPers, 30);

Muy bello verdad?,  ¿que pasaría si ahora nuestro problema se extendiera y nos pidiera que imprimiesemos los nombres de aquellas personas mayores a cierta edad que sean solamente hombres?

 public static void imprimirMasViejoQue(List<Persona> lista, int edad, Persona.genero gen){
        for (Persona p: lista){
            if(p.getEdad()>edad && p.getGen().equals(gen)){
                p.imprimirNombre();
            }
        }
    }

A este punto habremos dado una respuesta adecuada al problema, sin embargo tuvimos que modificar el código en su profundidad, estamos en falta con una de las máximas de la buena programación : hacer código que necesite la menor mantención posible.

Ahora pedimos algo mas, por ejemplo otro método que nos entregue todas las personas con determinado nombre.

  public static void imprimirNombre(List<Persona> lista, String nombre){
       for (Persona p: lista){
            if( p.getNombre().equals(nombre)){
                p.imprimir();
            }
        } 
    }

Así, tendremos dos métodos o más en una misma clase,  si prestamos atención podemos notar que todo los bloques de código, desde el primer planteo fueron variando en una sola línea, y es la que esta dentro del condicional, ¿no seria genial hacer un método genérico al cual le podamos entregar el condicional como si fuera un parámetro mas?. Traduciendo, quedaría de la siguiente manera:

public static void printPersonas(List<Persona> lista, condicional){
        for(Persona p:lista){
            if (condicional){
                p.imprimir();
            }
        }
    }

Es oportuno recordar a propósito de la firma de un método, que incluímos en ella la definición de tipo de datos, y ahora viene al caso refrescar lo que dijimos de las maravillosas interfaces, que son una forma de definir un tipo de dato, si dentro de una interface podemos implementar un método ¿por que no podríamos pasar un método como un parámetro?.
   Por supuesto si quisiéramos pasar como parámetro de un método una interface funcional y si lo incluímos dentro de un condicional, debemos suponer que el método deberia ser uno que devuelva un booleano.
     Dejemos de lado todo lo que vimos hasta ahora, menos la clase persona y entremos a pensar funcionalmente. Definamos una interface funcional (recordar que la definicion de una interface funcional dice que es aquella que contiene un solo método abstracto, lo cual sonaría redundante si definimos una interface no-funcional, pero no una interface funcional ya que en java 8 existen los métodos default -que tienen codigo implementado- cuya existencia no hace que una interface funcional deje de serlo):

public interface ChekearPersona{
        boolean test(Persona p);
    }

    ¿Se entiende verdad? solo creamos una interface para evaluar un objeto de la clase Persona (podriamos haber definido a este argumento como un genérico, para poderlo aplicar a cualquier objeto, pero no quiero entrar en ese terreno ahora).
    Seguimos desarrollando y entonces nos queda poder mandar a ese condicional como un parámetro que se "inyectará" dentro del for, Borramos todo de nuestra clase original EvaluarPersona y escribimos :

public class EvaluarPersona {

    public static void printPersonas(List<Persona> lista, ChekearPersona tester){
        for(Persona p:lista){
            if (tester.test(p)){
                p.imprimir();
            }
        }
    }
}
     Se puede observar que tratamos a tester como un objeto al cual le llamamos al método aún no implementado, ya que es una interface, éste método así como lo definimos en la interface funcional tiene como argumento un objeto del tipo Persona al cual le aplicamos la evaluación, como el método de la interface devuelve un booleano lo podemos usar como switcher del condicional.
    Nos quedaría escribir entonces la implementación de la interface obviamente dentro de nuestra clase administradora. Y aquí es cuando paro la evolución de nuestro código, seamos ingenuos y sigamos trabajando a la vieja usanza, ¿cómo utilizaríamos ahora este nuevo patrón de diseño sin adentrarnos en las "oscuras" aguas de la funcionalidad?, pues de esta manera:

public class Listar implements ChekearPersona {

        @Override
        public boolean test(Persona p) {
           //definicion de un condicional
        }
        
    }

    Y luego la instanciación de la clase para el uso del condicional:

     public void implementoCheck(){
           Listar list= new Listar();
           EvaluarPersona.printPersonas(listaPers, list); 
    }

    Estamos cerca pero no lo suficiente de una solución elegante, resulta algo  ilógico ésto si considero que necesitamos cambiar ese condicional n cantidad de veces, no estaría bien crear una clase que implemente esa interface cada vez que se nos ocurre una nueva consulta, pero sí tendría sentido implementar ese método de una forma dinámica, digamos, como una clase anónima, por ejemplo:

EvaluarPersona.printPersonas(listaPers, new ChekearPersona() {
            @Override
            public boolean test(Persona p) {
                return p.getEdad()>30;
            }
        }); 

    Simplemente aqui le estamos mandando como parámetro al método printPersonas un listado y la implementación del argumento-interface, lo interesante es que nunca mas vamos a reprogramar el método que recorre la lista ni tampoco la interface, lograremos como por arte de "magia" que esta sentencia nos imprima el nombre de las personas mayores a 30 años de manera transparente.
    Pero como en este caso estamos hablando de las maravillosas interfaces funcionales podremos dar un paso mas alla y transformar este bloque-sentencia en una secuencia funcional mediante una expresión lambda, según el IDE que usemos éste seguramente nos ayudara a crear la expresión lambda automáticamente, nos quedará de la siguiente manera:

EvaluarPersona.printPersonas(listaPers,  p -> p.getEdad()>30);

Como se trata de una interface funcional, condición fundamental para aplicar lambdas, es obvio que no se necesita declarar el nombre de la interface ni la clase que se pasa como argumento. El compilador maneja ese tipo de redundancias hasta hacer de nuestra sentencia una estructura funcional pura y fácilmente legible.
   Luego podríamos por supuesto implementar diversos condicionales utilizando inclusive otros campos de nuestro objeto, por ej:

EvaluarPersona.printPersonas(listaPers,  p -> p.getGen().equals(Persona.Genero.MASCULINO));

Notas finales:

    Creo que si entendemos este tipo de evolución podemos encarar la programación funcional de una manera mas amigable, por supuesto java 8 tiene su colección de interfaces y abstracciones para que no necesitemos implementar ningún código, por ejemplo el equivalente a lo que trabajamos en esta entrada, el Predicate, cuando exploren estas soluciones en java también verán que lógicamente se aplica la noción de genéricos por lo tanto deberá encerrarse en un diamond el objeto evaluado ej:

 Predicate<Persona> pred = new Predicate<Persona>() {
        @Override
        public boolean test(Persona p) {  
            return p.getGen().equals(Persona.Genero.MASCULINO);
        } 
  };

   Y el equivalente lambda:

Predicate<Persona> pred =  p -> p.getGen().equals(Persona.Genero.MASCULINO);

por supuesto ésto entrega un valor booleano, en nuestro caso de ejemplo empaquetamos todo el ciclo en una clase que también recorre nuestra lista, en cambio usando el predicado deberemos inyectarlo dentro de una abstracción stream (en otra entrada comentaré los  streams):

listaPers.stream().filter(pred).
forEach(p -> System.out.println(p.getNombre()));


Comentarios