// // // // // // //

miércoles, 1 de julio de 2015

Más Patrón estrategia

    Hace tiempo que no escribo pero hace poco hicimos entrevistas en mi trabajo para contratar a alguien. Le propusimos un algoritmo tan sencillo como recorrer números del 1 al 100 y si son múltiplos de 3 mostrar fizz, si lo son de 5 buzz, si lo son de ambos fizzbuzz y si no mostrar simplemente el número. Obviamente, si el número es 15 por ejemplo, solo se mostraría fizz buzz. Aquí algunos ejemplos de los algoritmos que vi:
private static void Solucion() {

    for (int i = 0; i < 100; i++) {

        if (i % 3 == 0 && i % 5 == 0)
            System.out.println("fizzbuzz");
        else if (i % 3 == 0)
            System.out.println("fizz" + i);
        else if (i % 5 == 0)
            System.out.println("buzz" + i);
        else
            System.out.println(i);
    }
}
    Como vemos, no cumple. Sí, solo muestra uno de los textos y sí, los muestra correctamente pero... No son los números del 1 al 100. Sorprendentemente, ninguno uso números del 1 al 100. Raro ¿no?
private static void Solucion() {

    for (int i = 0; i < 100; i++) {

        if (multiplo3(i) && multiplo5(i))
            System.out.println("algoquenomeacuerdo");
        if (multiplo3(i))
            System.out.println("fizz" + i);
        if (multiplo5(i))
            System.out.println("buzz" + i);

        System.out.println(i);
    }
}

private static boolean multiplo5(int i) {
    return i % 5 == 0;
}

private static boolean multiplo3(int i) {
    return i % 3 == 0;
}
    Esta fué otra solución. Otra vez del 0 al 99 y en este caso para el 15 muestra 4 textos. Leyendo esto, ¿A alguno se le ocurre que fuera tan difícil el agoritmo?. No el algoritmo no es tan difícil pero los nervios pueden jugar una mala pasada. Pero estoy dando mucho rodeo, vamos al grano.

    La solución esperada no es para nada compleja y está escrita en Java, pero se les dio opción de usar lo que quisieran, tenían Visual Studio y NetBeans y alguna otra cosilla para elegir como IDE. Es esta:
private static void Solucion() {

    for (int i = 1; i <= 100; i++) {

        if (i % 3 == 0 && i % 5 == 0)
            System.out.println("fizzbuzz" + i);
        else if (i % 3 == 0)
            System.out.println("fizz" + i);
        else if (i % 5 == 0)
            System.out.println("buzz" + i);
        else
            System.out.println(i);
    }
}
    Ahora bien, ¿Este algoritmo es escalable? La respuesta es no. Si yo quiero añadir nuevos requisitos o quiero un algoritmo que ejecute unos ifs u otros en función de unos checks que se le muestren al usuario, no puedo usar este tipo de algoritmo o tendré un montón de condiciones complejas de mantener.

    Para solucionarlo, podemos crear una clase que contenga dos objetos, uno para comprobar si el número es válido para ejecutar el código (parte del if) y otro que contenga el algoritmo que se ejecute si el código es válido. Para ello necesitamos dos interface, una por algoritmo y con un único método, así son interface funcionales y puedo usar expresiones lambda con ellos. Esta es la implementación:
interface CompruebaValido {

    public boolean esValido(int i);
}

interface Procesador {

    public void procesa(int i);
}

class CompruebaYProcesa {

    private CompruebaValido compruebaValido;
    private Procesador procesador;

    public CompruebaYProcesa(CompruebaValido compruebaValido, Procesador procesador) {
        this.compruebaValido = compruebaValido;
        this.procesador = procesador;
    }

    public boolean esValido(int i) {
        return compruebaValido.esValido(i);
    }

    public void procesa(int i) {
        procesador.procesa(i);
    }
}
    Como se puede ver, la clase CompruebaYProcesa delega en sus objetos para crear sus dos métodos. Y la implementación del algoritmo queda así:
ArrayList<CompruebaYProcesa> comPro = new ArrayList<>();

// Uso lambdas en vez de clases anónimas, el código queda más claro 
comPro.add(new CompruebaYProcesa((i) -> i % 5 == 0 && i % 3 == 0, (i) -> System.out.println("no se que " + i)));
comPro.add(new CompruebaYProcesa((i) -> i % 3 == 0, (i) -> System.out.println("FIZZ " + i)));
comPro.add(new CompruebaYProcesa((i) -> i % 5 == 0, (i) -> System.out.println("BUZZ " + i)));


// Muestro para comprobarlo
System.out.println(comPro);

for (int i = 0; i < 100; i++) {

    boolean haSidoValido = false;
    
     //  Recorro los objetos para comprobar y si has sido válido lo guardo en el boolean y hago break
     //  para que no salgan cosas repetidas
    for (CompruebaYProcesa cp : comPro) {

        if (cp.esValido(i)) {
            cp.procesa(i);
            haSidoValido = true;
            break;
        }
    }

    // Si no ha sido válido lo muestro sin texto
    if (!haSidoValido)
        System.out.println(i);
}
    Pero esto tiene un problema, que se van a ejecutar en el orden en que las añada, si por ejemplo, la condición del 3 y el 5 la añadiera al final, esta no se ejecutaría para el 15, si no que se ejecutaría la primera, por ejemplo, la del 3, pues cumple la condición de ser múltiplo de 3. Para solucionar este problema, cambiamos un poco la clase CompruebaYProcesa para añadirle un int con la prioridad y antes de ejecutarlo ordenamos el ArrayList:
class CompruebaProcesaYPrioriza {

    private final CompruebaValido compruebaValido;
    private final Procesador procesador;
    public final int prioridad;

    public CompruebaProcesaYPrioriza(CompruebaValido compruebaValido, Procesador procesador, int prioridad) {
        this.compruebaValido = compruebaValido;
        this.procesador = procesador;
        this.prioridad = prioridad;
    }

    public boolean esValido(int i) {
        return compruebaValido.esValido(i);
    }

    public void procesa(int i) {
        procesador.procesa(i);
    }

    @Override
    public String toString() {
        return "CompruebaProcesaYPrioriza{" + "prioridad=" + prioridad + '}';
    }

}
Y el algoritmo queda así:
comPro = new ArrayList<>();

comPro.add(new CompruebaProcesaYPrioriza((i) -> i % 3 == 0, (i) -> System.out.println("FIZZ " + i), 50));
comPro.add(new CompruebaProcesaYPrioriza((i) -> i % 5 == 0, (i) -> System.out.println("BUZZ " + i), 10));
comPro.add(new CompruebaProcesaYPrioriza((i) -> i % 5 == 0 && i % 3 == 0, (i) -> System.out.println("no se que " + i), 100));

// Borro uno para ver resultado aleatorios
comPro.remove(new Random().nextInt(comPro.size()));

// Ordeno por prioridad descendente (de mayor a menor)
Collections.sort(comPro, (o1, o2) -> o1.prioridad < o2.prioridad ? 1 : -1);

// Muestro para comprobarlo
System.out.println(comPro);

for (int i = 0; i < 100; i++) {

    boolean haSidoValido = false;
    
     //  Recorro los objetos para comprobar y si has sido válido lo guardo en el boolean y hago break
     //  para que no salgan cosas repetidas
    for (CompruebaProcesaYPrioriza cp : comPro) {

        if (cp.esValido(i)) {

            cp.procesa(i);
            haSidoValido = true;
            break;
        }
    }

    /* Si no ha sido válido lo muestro sin texto */
    if (!haSidoValido)
        System.out.println(i);
}
    De esta manera, se le pueden añadir condiciones a partir de selecciones del usuario y ejecutarlas con prioridad, para que, por ejemplo si elige las condiciones del 3 y del 3 y el 5, se ejecute primero la que tenga más prioridad, da igual el orden.

    La anterior entrada que hablaba del Patrón estrategia es: http://morethansimplycode.blogspot.com.es/2015/02/enums.html

No hay comentarios:

Publicar un comentario