Script Papá Noel

El enunciado

Es un parcial de Paradigmas, un test interesante si asumimos que vieron todo un cuatrimestre de programación OO,
en particular Java, y este es un examen de la UTN para gente que tuvo 6/7 clases en Objetos.

Podemos arrancar desde muchos ángulos.
Entonces la pregunta nuestra es: ok, si tenés que resolverlo, ¿por dónde empezás?
Les preguntamos a los chicos: quizás alguno que estuvo chusmeando UML podría sugerir diagrama de objetos, diagrama de clases…

La mayoría seguro que se van por el lado del código.
Eso no estaría mal, es un buen comienzo.
Los dejamos que se embalen… que piensen 10 minutos en silencio.

Cortamos. Vemos y empezamos a escribir en el pizarrón.

1) Si empezamos por el código, ¿qué código escribimos primero?

  • El punto 1, después el punto 2, etc.
  • Al leer el enunciado voy codificando lo que se me va ocurriendo
  • Si bien lo vamos a ver en otra clase les adelantamos que podríamos escribir el main, o algo parecido (un test) que pruebe el punto 1, luego el punto 2, etc.

2) Si empezamos por algún diagrama (y es un diagrama de clase), las opciones son:

  • Tratar de abarcar todo el ejercicio
  • Empezar por el punto 1, después el punto 2

3) En general, ¿qué diagramas conocen? ¿Para qué sirven? ¿Qué muestran, qué comunican?

Cosas que tienen que surgir:

  • Diferencia entre mensaje y método (desde dónde me ubico: mensaje me ubico desde afuera, qué le puedo pedir vs. cómo implemento eso que quiero: método)

En la materia vamos a preferir pararnos desde afuera, porque
1°) es lo más fácil
2°) nos fuerza a no tomar decisiones apresuradas (early decisions)

Y eso hace a la diferencia interfaz vs. implementación.
No queremos pensar en si modelamos el arrepentimiento con un boolean o un String hasta que no sea el momento.
Posponer esas decisiones nos permiten atacar problemas complejos sin volvernos locos.

  • Polimorfismo vs. ifs
  • Cuándo subclasificar y cuándo no

Y si aparece:

  • Qué cosas manejamos con instancias y qué cosas con clases

Ah, y de paso empezamos a tachar cosas que no nos van a interesar:

  • Base de datos: ya van a tener una materia para eso
  • GUI: el diseño de interfaces apenas si las vamos a trabajar para entender cómo manejar las excepciones, lo vamos a dejar para cuando cursen Algoritmos 3
  • Objetos de Transformación/POJOS/DTOs/Beans: quizás aparezcan objetos que modelan valores (como la Dirección de una persona), pero no nos quitará el sueño. Al no tener presentación ni persistencia tampoco vamos a necesitar pasar información entre "capas".
  • EJBs, Objetos que abstraigan servicios: mmm… vamos a modelar interfaces con sistemas externos, pero no ahora.

Anotamos cosas que parece que podrían ser objetos

  • una acción, buena o mala
  • una persona
  • ¿papá noel? no existe, pff…

Los chicos no son malos…

Es tentador para los que arrancan a modelar con objetos pensar en que la persona es buena o mala, o sea, asociar la bondad o la maldad a la persona que lo realiza. Si bien esto es cierto en la realidad, hay que tener en cuenta que nosotros armamos un modelo, una simplificación. Recordar la película "Zoolander", cuando a Derek Zoolander le muestran una maqueta del centro para niños y éste, indignado pregunta: "¿Cómo van a caber si el edificio es tan pequeño?"

En la realidad no le pregunto al cliente cuánto me debe, ni al alumno si aprobó, pero mi abstracción cliente / alumno no es la realidad, sino una simplificación.
Por eso está bueno llegar al objeto que modela una acción, no importa si es buena o mala…

1.a) Maldad

Desarrollamos el % de maldad:

    @Override
    public double getCantidadMaldad() {
        if (this.seArrepintio) {
            return this.gravedad * 0.5 * this.cantidadAfectados;
        } else {
            return this.gravedad * this.cantidadAfectados;
        }
    }

Claro, si alguien se aviva tenemos que pensar en que la cantidad de afectados no es un int común y corriente,
sino que tiene que salir de una lista de personas.
Lo podemos poner en maldad, o en otro lado.
Acá vemos cuánto manejan de herencia.
Podemos arrancar en forma naif y cuando lleguemos a la Bondad podemos abstraer la Accion.

Estaría bueno esto mostrarlo en cañón, para usar un test, pero vamos a dejarlo para más adelante.

Acción es una clase que no tiene sentido instanciar… o bien no podemos instanciar, la convertimos en abstracta.

Accion tiene VI:

    private List<Persona> afectados;
    private int anio;

Repasamos variable de instancia vs. variable de clase (estático)

Y tiene que definir en principio un método abstracto:

    public abstract double getCantidadMaldad();

Repaso de método abstracto: define una interfaz para que yo pueda hacer:

    private Accion tortazoSinArrepentirse = new Maldad(5, false);
    this.tortazoSinArrepentirse.getCantidadMaldad()

Si no ¿qué pasa?
No compila, por más que el método esté definido en la clase Maldad.

Y getCantidadAfectados() es un método que ponemos en Accion:

    public int getCantidadAfectados() {
        return this.getAfectados().size();
    }

Entonces reescribimos el método getCantidadMaldad() para Maldad (que hereda ahora de Accion):

    public double getCantidadMaldad() {
        if (this.seArrepintio) {
            return this.gravedad * 0.5 * this.getCantidadAfectados();
        } else {
            return this.gravedad * this.getCantidadAfectados();
        }
    }

¿Qué pasó en clase?

La idea original de los chicos fue que el método estuviera en PapaNoel.

Después de ver que en realidad necesitábamos conocer a la acción de alguna manera, saltó que PapáNoel le preguntaba demasiado a la acción. Entonces movimos el método de PapáNoel a Acción.

En Acción se propuso ponerle un flag a la acción para saber si es buena o mala:

    public double getCantidadMaldad() {
           if (this.getTipoAccion() == "M") {
              if (this.seArrepintio) {
                 return this.getGravedad() * 0.5 * this.getCantidadAfectados();
              } else {
                 return this.getGravedad() * this.getCantidadAfectados();
              }
           } else {
              return 0;
           }
    }

Lo primero que les dije es: ojo con el ==, hablamos de equals() vs. ==:
    public double getCantidadMaldad() {
           if (this.getTipoAccion().equalsIgnoreCase("M")) {
              if (this.seArrepintio) {
                 return this.getGravedad() * 0.5 * this.getCantidadAfectados();
              } else {
                 return this.getGravedad() * this.getCantidadAfectados();
              }
           } else {
              return 0;
           }
    }

Lo segundo: marqué el primer if y el segundo if y les pregunté cuál les parecía que podía eliminarse. Arrancamos con el if(this.seArrepintio) y vimos que en realidad era un if propio del negocio, lo había pedido Papá Noel, entonces era lógico que estuviera.

El primer if, si bien lo determina Papá Noel, podemos resolverlo a través de acciones buenas y malas que sean polimórficas. Entonces ese if desaparece, y nos queda:

    public double getCantidadMaldad() {
           if (this.seArrepintio) {
              return this.getGravedad() * 0.5 * this.getCantidadAfectados();
           } else {
              return this.getGravedad() * this.getCantidadAfectados();
           }
    }

Entonces se propuso llevar afuera la pregunta de si se arrepintió, y nos quedó:
    public double getCantidadMaldad() {
           return this.getGravedad() * this.getCantidadAfectados() * this.getCoeficienteAjuste();
    }

    public double getCoeficienteAjuste() {
       if (this.seArrepintio) {
          return 0.5;
       } else {
          return 1;
       }
    }

Y nos quedaron dos métodos cohesivos, más cortos, evitamos duplicar la multiplicación gravedad * cantidad de afectados
y si en otro contexto lo requiero puedo reutilizar el coeficiente de ajuste (esto mucho no los convenció, pero quedó).

Maldad tiene como VI:

    private double gravedad;
    private boolean seArrepintio;

No nos preocupamos mucho para "mejorar" el método getCantidadMaldad(), ya vamos a tener una clase que se encarga de "refactorizar" o pulir el código. De todas maneras si alguien tira ideas son bienvenidas… pero no vamos a poner énfasis en eso porque ahora estamos tratando de resolver el ejercicio y tener una primera solución.

¿Cómo medimos si lo que hicimos está bien?
Bueno, por el momento no tenemos muchas herramientas, revisando el enunciado y el código parecen concordar.
1) podríamos ahora pensar en un main
2) o a futuro vamos a automatizar varios mains en casos de prueba.

Si quieren hacer el main, ok, es en definitiva éste:

    private Accion tortazoSinArrepentirse = new Maldad(5, false);
    System.out.println(tortazoSinArrepentirse.getCantidadMaldad());

Si nos da 5 es que está ok.

1.b) Bondad

La bondad nos obliga a cambiar la interfaz de la Accion:

    public abstract double getCantidadBondad();

La implementación en Maldad es fácil:

    public double getCantidadBondad() {
        return 0;
    }

Pero también tenemos que crear la clase Bondad, que hereda de acción y tiene una implementación fácil, la que calcula
el % de maldad:

    public double getCantidadMaldad() {
        return 0;
    }

La bondad depende de algunas cosas, tiramos algo de código:

    public double getCantidadBondad() {
        double total = 0;
        for (Persona afectado : this.getAfectados()) {
            if (!afectado.esBueno()) {
                total += this.importancia * PORCENTAJE_BUEN_SAMARITANO;
            } else {
                total += this.importancia;
            }
        }
        return total;
    }

Decimos que:
1) aparecen objetos acciones que son polimórficos: tanto las buenas como las malas tienen cantidades de bondad y de maldad.
2) la clase Persona no tenía necesidad de aparecer… hasta ahora.
¡Pero las personas son las que hacen las acciones! Claro, pero nuestro modelo podría no haberlo contemplado.
De hecho, no modelamos ni el pompón del gorro de Papá Noel, ni su colección de venados que tiran del trineo.
Porque el modelo no es la realidad, sino una simplificación que nos ayuda a tomar decisiones.

Una persona puede ser buena o no, eso lo refleja la interfaz esBueno(), por ahora vamos a dejarlo que devuelva siempre true:

    public boolean esBueno() {
        return true;
    }

¿Pero las personas no tienen acciones?
Sí, pero eso nos desvía de resolver el punto 1.b.
Ok, no estamos resolviéndolo entero, pero el punto 3 se va a ocupar de eso.
La complejidad de un punto puede hacernos olvidar de las cosas importantes: si el punto 3 no pidiera lo que ahora ignoramos,
está bueno ir anotando en un papel las decisiones que tenemos que tomar, para atacarlas una por vez:

  • Resolver la cantidad de bondad de una acción
  • Saber si una persona es buena

Vamos agregando decisiones que tenemos que tomar cuando vemos que aparecen y vamos tachando a medida que las encaramos.
La Bondad necesita una VI importancia (un int/double) y el porcentaje de buen samaritano, que es "configurable". Cuando el enunciado dice esto es porque justamente no queremos una constante, el usuario no sabe programar. Ahora qué tipo de variable:

  • de instancia, como la importancia, depende de cada acción buena:
    private double importancia;
  • de clase o estática, la comparten todas las acciones buenas, por eso se define como:
    private final static double PORCENTAJE_BUEN_SAMARITANO = 1.1;   // ponele

El método getCantidadBondad se puede mejorar, una vez más depende de qué tan afilados estén los chicos para darlo vuelta, por ahora lo dejamos así.

2) Saber el nivel de bondad de un chico en un año determinado

Entonces sabemos que vamos a pedirle el año como parámetro.
¿A qué objeto? A una persona.
Esto es importante: la persona modela a la persona, no hace falta poner PersonaRuleBean o PersonaManager.
No vamos a buscar managers en esta cursada, en todo caso lo dejamos para algo3 para que conozcan a los Home o Repository.

Ok, ¿cómo lo implementamos?
"Esto está dado por bondad de sus acciones de ese año menos la maldad de estas."
Claro.
La persona tiene que conocer las acciones de un año.

    public double getNivelBondad(int anio) {
        double total = 0;
        for (Accion accion : this.getAcciones(anio)) {
            total += accion.getCantidadBondad() - accion.getCantidadMaldad();
        }
        return total;
    }

¿De quién es la responsabilidad de saber qué acciones tiene? De una persona.
¿Quién sabe si una acción es de un año? La acción.
Eso lo codificamos:

    private List<Accion> getAcciones(int anio) {
        List<Accion> result = new ArrayList<Accion>();
        for (Accion accion : this.acciones) {
            if (accion.esDelAnio(anio)) {
                result.add(accion);
            }
        }
        return result;
    }

    >>Accion (recordemos que es abstracta)
    public boolean esDelAnio(int anio) {
            return this.anio == anio;
    }

Dos preguntas:
1) ¿No podríamos haber hecho todo en un método?
Sí, pero si un método hace más cosas (filtrar las acciones de un año y sumar bondades - maldades)
a) es más difícil de testear
b) si da error, la causa puede ser alguno de los objetivos
c) no promueve la reutilización: si en otro método necesito hacer algo con las acciones de un año
tengo que volver a escribirlo en el nuevo método
En general hablamos de mayor o menor cohesión en tanto un método haga menos o más cosas, respectivamente.
Buscamos que haya más cohesión, entonces siempre es más conveniente que un método tenga un objetivo
claramente definido.

2) ¿No podemos hacer que la persona pregunte directamente si la acción es de un año?
Sí, no es un error grave hacer eso, pero vamos a hablar de otro concepto de diseño, que es acoplamiento,
o el grado en que dos componentes se conocen.
Cuando una persona le pregunta el año a una acción, sabe que hay un getter donde se devuelve el int
con el año. Podríamos cambiar la implementación (por ejemplo, podríamos guardar la fecha de la acción
como Date) y aún así mantener la interfaz del int. Por eso no es tan grave.
Pero tener un método esDelAnio(anio) es todavía mejor, porque Persona conoce menos cosas de Accion.
Vamos a buscar mucho en la cursada que cuando dos objetos tienen que interactuar juntos
1) se conozcan, porque si no no pueden trabajar juntos
2) conozcan sólo lo que tienen que conocer, esto es: pedirle al otro objeto sólo lo que necesita
sin que tengan que conocer cómo lo resuelve.

Repasamos method lookup.
¿Qué pasa cuando hacemos?

tortazoSinArrepentirse.esDelAnio(1997);

Claro, no está definido como método en Maldad, pero seguimos buscando en la superclase que es donde
efectivamente está definida.

3) Saber si un chico fue bueno

¿Quién lo resuelve?
Claro, la Persona.

Ahora sí revisamos el método provisorio

    public boolean esBueno() {
        return true;
    }

y lo cambiamos:

    /**
     * Decimos que un chico fue bueno cuando su nivel de bondad en 
     * el año actual es mayor a cero. También lo consideramos bueno 
     * si tiene un nivel de bondad menor a cero, pero es mayor al nivel 
     * de bondad del año anterior (está intentando mejorar y eso es 
     * lo que importa, ¿no?)
     * @return
     */
    public boolean esBueno() {
        int anioActual = Calendar.getInstance().get(Calendar.YEAR);
        int anioAnterior = anioActual - 1;
        return this.getNivelBondad(anioActual) > 0 || this.getNivelBondad(anioAnterior) < this.getNivelBondad(anioActual);
    }

Seguramente los chicos lo van a hacer con más variables, estaría bueno evitar código del estilo:

    if (condicion == true) {
           return true;
    }


etc,
pero una vez más, no nos quita el sueño eso.

4) Dada una bolsa de regalos, encontrar el regalo perfecto para un chico

Acá quizás haya alguna que otra dificultad para encontrar el objeto receptor.
¿Un regalo? ¿Un conjunto de regalos? ¿o una persona?
Y… el regalo perfecto para un chico en la realidad no lo puede decir un chico, porque va a elegir lo que él quiere.
Pero nuevamente, el chico no es el de la realidad, sino un modelo.

La bolsa de regalos, ¿cómo se modela?

  • un List (los elementos están ordenados: el orden es según fueron ingresados)
  • un Set (los elementos no están repetidos, hay que ver el código de equals() y hashCode() de los elementos)
  • una Collection (es un conjunto de cosas, no puedo asegurar nada)

Fìjense que vamos de lo más específico a lo más general.
El Set puede andar, pero habría que preguntarle a Papá Noel si en esa bolsa puede haber dos trenes. Si es así, el Set
de Java no nos sirve porque no admite duplicados.

Anoto: es importante preguntar al usuario cuando hay dudas, no tomar decisiones en soledad.

    private Regalo regaloPerfecto(List<Regalo> regalos) {

    }

El uso del List es discutible, pero nos va a servir para que veamos lo que queremos proponer.

  • tomar la lista de regalos y quedarme con aquellos cuyo nivel de bondad requerida es mayor que el nivel que tiene esa persona
  • ordenar la lista de regalos en base a cuánto le falta a la persona para llegar a ese nivel de bondad requerido
  • tomamos el primero
  • una última disquisición, si no hay regalos devolvemos null (no es importante que aparezca en clase esto)

En Persona:

    public Regalo regaloPerfecto(List<Regalo> regalos) {
        Comparator<Regalo> comparadorPorDiferencia = new Comparator<Regalo> () {

            @Override
            public int compare(Regalo o1, Regalo o2) {
                return cuantoLeFaltaPara(o1).compareTo(cuantoLeFaltaPara(o2));
            }

        };
        List<Regalo> regalosAConsiderar = this.getRegalosConBondadMayorA(regalos, this.getNivelBondad(Calendar.getInstance().get(Calendar.YEAR))); 
        if (regalosAConsiderar.isEmpty()) {
            return null;
        }        
        Collections.sort(regalosAConsiderar, comparadorPorDiferencia);
        System.out.println(regalosAConsiderar);
        return regalosAConsiderar.get(0);
    }

Acá definimos un Comparator especial, que vamos a necesitar porque para saber cuánto le falta tienen que colaborar

  • Persona y
  • Regalo

Ambos sí o sí.

En Persona

    public BigDecimal cuantoLeFaltaPara(Regalo regalo) {
        return new BigDecimal(regalo.getBondadRequerida() - this.getNivelBondad(Calendar.getInstance().get(Calendar.YEAR)));
    }

Y también filtramos los regalos que tengan un mayor bondad de nivel que el que la persona tiene
para que no me traiga el regalo más barato (pedazo de carbón, por ejemplo). En Persona:

    private List<Regalo> getRegalosConBondadMayorA(List<Regalo> regalos, double nivelBondad) {
        List<Regalo> result = new ArrayList<Regalo>();
        for (Regalo regalo : regalos) {
            if (regalo.getBondadRequerida() > nivelBondad) {
                result.add(regalo);
            }
        }
        return result;
    }

Y nos anotamos la repetición constante de

Calendar.getInstance().get(Calendar.YEAR)

para construir el año actual.

Claro, podríamos haber puesto todo junto en un método:

for de regalos que queda en regalo
    si el regalo supera el nivel de bondad
        obtengo la diferencia
        si la diferencia es mayor que la que existía antes (originalmente cero)
            guardo regalo actual y diferencia

el tema es nuevamente la pérdida de cohesión.
Ahora tengo 3 métodos de los cuales puedo reutilizar 2.
Por supuesto es menos performante, porque mientras que en la segunda opción hay un solo for (donde la cantidad de iteraciones
es n) en la primera tengo filtro + ordenamiento (2n).
Pero no todo es rapidez, es preferible a veces poder separar las tareas y dejarlo más simple de programar,
los ciclos de reloj se acortan con el hardware (hagan la prueba en sus casas a ver cuánto se gana o con cuántos regalos
hacemos la diferencia).

Hasta acá está bien,
el jueves haría un repaso + lo que no lleguemos de acá + punto 5 ¿y 6?.
Podríamos dejar el punto 6 para la clase de refactoring o si la clase avanzó mucho la damos y listo.

5.a Obtener los regalos disponibles en el taller

Se agrega la clase Taller, que hasta ahora no necesitamos (está bueno ver que aparezca recién acá, antes no hace falta)

¿Asumimos que cada taller tiene su propio stock?
Podríamos suponer que sí, pero lo vamos a dejar para clase de refactor

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License