Resumen de clase: Ejercicio agricultores

El enunciado

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°) facilita pensar primero en qué quiero y después en cómo lo logro
2°) nos fuerza a no tomar decisiones apresuradas (early decisions)

Y eso hace a la diferencia interfaz vs. implementación.
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
  • UI (User Interfaces): no vamos a modelar pantallas ni ninguna otra vista de presentación, apenas si las vamos a trabajar para entender cómo manejar las excepciones, lo vamos a dejar para cuando cursen Algoritmos 3
  • Al no tener presentación ni persistencia tampoco vamos a necesitar pasar información entre "capas".
  • Objetos que abstraigan servicios: mmm… vamos a modelar interfaces con sistemas externos, pero no ahora.

Anotamos cosas que parece que podrían ser objetos

  • los tipos de cultivo
  • un agricultor
  • una parcela
  • ¿el silo?
  • ¿algún otro objeto más que se les ocurra?

Costo de un cultivo

Dividimos el pizarrón en varias partes:

  • en un sector vamos a ir escribiendo el diagrama de clases
  • en otro sector nos vamos a ayudar con ejemplos concretos: el diagrama de objetos muestra una foto
  • en otro sector vamos a tirar código que va a ser en xtend, un lenguaje que tiene reminiscencias de Smalltalk pero que funciona sobre una IDE de Java. El jueves van a conocerlo más en detalle
  • podríamos dejar un espacio para el diagrama de secuencia
  • ¡ah! y ya estuvimos definiendo objetos candidatos, no todos vamos a terminar programándolos, pero son abstracciones posibles en nuestro problema.

Para definir el punto 1, necesitamos saber

  • qué objeto va a resolver ese requerimiento
  • qué interfaz va a tener (nombre del método y qué parámetros)
  • con qué otros objetos va a colaborar

Ejemplo: el costo de la soja debería obtenerse enviando un mensaje

soja.costoTotal

Mmmm… pero si el costo de la soja es de 10$ por hectárea, necesito saber la superficie cultivada. Entonces aquí me di cuenta de que arranqué mal.
Si me equivoco, ¿qué hago? No persisto en el error, lo admito y vuelvo a empezar. No importa con qué herramientas diseñe, la metodología que usemos al diseñar debe permitir equivocarme tantas veces como sea necesario. Pensándolo bien nos conviene que haya un objeto Parcela, al que le podamos preguntar:

parcela.costoTotal

Otra opción sería pensar en el cultivo como el responsable de responder el costo, pero recibiendo una parcela como parámetro:

soja.costoTotal(unaParcela)

Cuando apareció la abstracción Parcela, podríamos habernos adelantado a decir "claro, la parcela conoce a un cultivo". Pensar en la interfaz demora esa decisión, pone el foco en definir primero cómo le pregunto a la parcela su costo, para después pasar a implementar ese costo total. Parece una diferencia sutil, pero cuando tenemos que resolver un problema complejo, son muchas las decisiones que tendremos en la cabeza y diferirlas permite allanar el camino y no abrumarnos con tantas definiciones de golpe.

Ahora sí, resolvemos el método costoTotal, en principio volviendo al cultivo:

def double getCostoTotal() {
        cultivo.costoPara(this)
    }

Entonces ahora sí sabemos que una Parcela conoce a su cultivo y le delega la pregunta del costo pasándose a sí mismo como parámetro.

Diferenciando los cultivos

El cultivo sigue una jerarquía que permite

  • definir una interfaz común para todos los cultivos
  • reutilizar comportamiento entre los cultivos

No obstante, hay que ser cuidadoso a la hora de subclasificar. En esta cursada veremos que la composición de objetos (hacer que dos objetos se conozcan) va a ser preferible antes que la subclasificación (hacer que una clase sea subclase de otra). ¿Por qué? Porque la herencia es rígida, al menos en la implementación que vamos a trabajar nosotros sólo es posible tener una superclase.

El ejemplo típico del componer vs. heredar es el caso de la Pila, ¿hereda de Array o tiene un Array? Si Pila hereda de Array, sólo puede heredar comportamiento y atributos de la jerarquía superior de Array. Además si hereda de Array está obligado a definir muchos métodos que quizás no necesita.

Volvemos al ejercicio y tenemos entonces

  • Soja
    • Soja Transgénica
  • Trigo
  • Sorgo

Pero una vez más, la ansiedad nos jugó una mala pasada. ¿Qué estamos resolviendo? El costo de la soja, entonces el foco tiene que estar puesto en la soja: podríamos incluso no hacer aparecer el cultivo como abstracción, hasta que no sea necesario. Este juego nos permite que nuestra solución sea lo más simple posible, por más que la desventaja de que no sea general sea lo suficientemente obvia.

def double costoPara(Parcela parcela) {
    this.costoBasePorHectarea(parcela) * parcela.hectareasCultivadas
}

Si optamos por hacer pasos chicos, deberíamos ubicar el método costoPara en Soja. Una decisión de diseño fue separar el costo base del cálculo del costo total.
Codificamos el costo base para la soja:

def costoBasePorHectarea(Parcela parcela) {
    10
}

¿Qué pasa con la soja transgénica? El cálculo se mantiene. Entonces podemos definir que la Soja Transgénica hereda de Soja y no tenemos que escribir nada.

Repaso method lookup

¿Qué pasa cuando se ejecuta este código?

Parcela parcela200 = new Parcela(200, new SojaTransgenica(false))
parcela200.costoTotal

hacemos un diagrama de secuencia que muestra esto:

  1. enviamos el mensaje costoTotal a parcela200… (si no se acuerdan la diferencia entre mensaje y método: http://uqbar-wiki.org/index.php?title=Mensajes_y_m%C3%A9todos)
  2. el objeto parcela delega el costoPara al cultivo (la soja transgénica)
  3. buscamos el método costoPara(Parcela) en la Soja Transgénica. No está, ¿qué hacemos?
  4. buscamos el método en la superclase: Soja. Lo encontramos, lo ejecutamos para la instancia actual, que es un objeto Soja Transgénica (no existe el objeto Soja en nuestro ejemplo)

Para más información: http://uqbar-wiki.org/index.php?title=Method_lookup

Silver Sorgo

Aquí sí aparece la idea de tener código compartido entre la soja y lo que vamos a implementar con el sorgo. ¿Qué compartimos? el cálculo del costo total, que es igual para los 3 casos: costo base * cantidad cultivada de la hectárea. Fíjense que ahora sí aparece la necesidad de la clase Cultivo, subimos allí el código del costo total y definimos que todo cultivo debe definir el método costoBasePorHectarea:

abstract class Cultivo {

    def double costoPara(Parcela parcela) {
        this.costoBasePorHectarea(parcela) * parcela.hectareasCultivadas
    }

    def double costoBasePorHectarea(Parcela parcela)
}

Algunas cosas:
  • el Cultivo se define en xtend como abstracto. Esto es un contrato fuerte, porque no puedo hacer new Cultivo. En Smalltalk esto no ocurre porque la clase abstracta no tiene sentido instanciarla, pero no hay una prohibición.
  • el método costoBasePorHectarea no define código, está definiendo solamente una interfaz para que las subclases estén obligadas a implementarla. Esto se conoce como método abstracto, de nuevo introduce un contrato fuerte, las subclases no compilan si no se define el costo base por hectárea. En Smalltalk esto no se daba, teníamos que definir un método que explícitamente tirara error.

Hay que modificar la clase Soja, porque el costo base por hectárea redefine un método de la superclase (si no no compila):

override costoBasePorHectarea(Parcela parcela) {
    10
}

Y en Sorgo escribimos nuestro método costo por hectárea:

override costoBasePorHectarea(Parcela parcela) {
    if (parcela.hectareasCultivadas < 50) { 
        3 
    } else {
        2
    }
}

Hacemos un diagrama de secuencia para mostrar cómo queda el costo total de una parcela de sorgo.

Separando el sorgo del trigo

El trigo tiene un cálculo un tanto molesto, pero que se puede simplificar si antes pensamos un método que nos devuelve el menor entre dos números. Lo cómodo que tenía Smalltalk era que se podía pedir algo como

2 min: 4
7 min: 5

Lo bueno es que xtend propone algo similar, definiendo extension methods:

// Extension method que muestra cómo puedo enviar un mensaje min a un número
def double min(double numero1, double numero2) { 
        if (numero1 < numero2) {
        numero1
    } else {
        numero2
    }
}

Este código se puede escribir en Trigo, después hallaremos mejores lugares donde ubicarlo. El tema es que necesitamos:

  • definir un costo base por hectárea
  • pero no nos alcanza la definición del cálculo del costo total en Cultivo

Primero, pensemos que el costo base tiene que ser 5:

override costoBasePorHectarea(Parcela parcela) {
    5
}

No queremos volver a escribir el costo total de cultivo, pero necesitamos redefinir el comportamiento default:

override double costoPara(Parcela parcela) {
    super.costoPara(parcela).min(500)
}

Llamar a super evita tener que copiar y pegar el cálculo (y entrar en recursividad infinita como ocurriría con this). ¿Dudas con super? Chequeá http://uqbar-wiki.org/index.php?title=Super

Precio de Venta

Comenzaremos con el trigo, porque tiene cosas interesantes para marcar.

"El precio de venta es de $ 20 por kg a los cuales hay que restarle los conservantes del silo (se conoce el costo por kg de cada conservante)"

Bien, algunas decisiones que tenemos que tomar:

  • ¿cómo modelamos el silo?
  • ¿necesitamos objetos conservante?
  • ¿quién conoce a los conservantes?

El silo podemos diseñarlo de varias maneras, la más simple es tener

  • un entero cantidadEnSilo o similar
  • la lista de conservantes.

Otra opción es reificar el concepto de silo creando un objeto Silo. La ventaja es que hacemos aparecer una abstracción, la desventaja es que tiene un costo (es más complejo de implementar). Al diseñar jugamos mucho con implementar los detalles o simplificarlos, esto es algo con lo que vamos a trabajar largamente durante la cursada.

Vamos a elegir la primera opción, como para remarcar que si bien la segunda solución es más elegante, viene al costo de una mayor indirección.

Ahora bien, nuevamente tenemos que seleccionar el objeto que responde al precio de venta y la interfaz del objeto: podemos dejar que lo resuelva Parcela o bien cada Cultivo. La parcela delega al cultivo, entonces nos queda

def double precioVentaPorKg(Parcela)

Por default el precio de venta es 20 $:

>> Cultivo
def double precioVentaPorKg(Parcela parcela) {
    20
}

Pero el trigo tiene que descontar el costo de los conservantes.

>> Trigo
override precioVentaPorKg(Parcela parcela) {
    super.precioVentaPorKg(parcela) - this.costoConservantes
}

def double costoConservantes() { 
    conservantes.fold(0.0, [ acum, conservante | acum + conservante.precio ])
}

Otra opción podría ser definir un template method, de esta manera:

>>Cultivo
def precioVentaPorKg(Parcela parcela) {
    20 - this.ajuste
}

def double ajuste() {
      0.0
}
>>Trigo
override double ajuste() { 
    conservantes.fold(0.0, [ acum, conservante | acum + conservante.precio ])
}

Cultivo resuelve el precio de venta y les deja a las subclases la implementación del ajuste.
Esta solución, no obstante, tiene dos contras:

  1. necesita que el cálculo de todos los cultivos sea igual (importe - un valor). Esta es una restricción muy grande.
  2. obliga a definir un método ajuste en Cultivo (lo ensucia)

¿Cómo seguimos?

El resto del ejercicio lo pueden descargar aquí.

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