Polimorfismo

En esta clase vamos a resolver un ejercicio aprovechando el polimorfismo. Veamos todos los objetos involucrados en calcular el sueldo de Pepe...

Objetivos de esta clase

  1. Repaso de los conceptos básicos vistos: objeto, referencia, mensaje (consulta o acción), método, atributo, asignación
  2. Plantear una solución a un ejercicio aprovechando el polimorfismo
  3. Conceptos de modelado
    1. 3 formas de conocer un objeto: global, atributo o parámetro
    2. Decidir si "me lo guardo o lo recibo?"
    3. Objetos que representan cosas más abstractas (a.k.a objetos chetardos)
  4. Metodología propuesta
    1. ¿Cuál es el requerimiento?
    2. ¿Qué mensaje le voy a enviar a qué objeto (y con qué parámetros)?
      • TDD en la consola
      • Perderle el miedo a que el programa explote (errores)
    3. ¿Método de acción o consulta?
    4. Implementación

Elementos del lenguaje

  • self
  • property
  • Comandos de la consola: :r :rr

Apuntes teóricos

El Sueldo de Pepe

#TODO Ver qué versión del sueldo de pepe usar como enunciado... Código inicial (con las categorías y métodos en blanco de pepe)? Pasar por la hoja en blanco? Requerimiento de sueldo base? O directamente el sueldo con bonos? El sistema debe ser extensible para nuevos empleados, nuevas categorías de trabajo, y nuevos contratos en el futuro

Presentamos el principio del enunciado (pepe, categorías y bonos) y el primer requerimiento. Después se abre un nuevo proyecto en blanco para comenzar...

1. Cómo encarar un ejercicio

Preguntas gatillo

¿Por dónde arrancamos?

Después de hacer un pequeño debate para entrar en calor con el ejercicio y repasar los conceptos básicos de objetos, proponemos nuestra metodología.

Metodología para encarar un ejercicio

Esta es la metodología que vamos a usar para resolver los ejercicios en toda la cursada:

  1. ¿Cuál es el requerimiento? (uno, el primero, o el siguiente si ya hay algo resuelto)
    • Focalizamos en una parte del problema: "dividir y conquistar"
    • Respuesta: Calcular el sueldo base de pepe
  2. ¿Qué mensaje vamos a enviar a qué objeto (y con qué parámetros)?
    • Pensamos primero en cómo vamos a usar los objetos (pensar en el mensaje antes que en el método)
    • Armar una lista de objetos candidatos, si es necesario
    • Acá puede salir varias posibilidades, está bueno anotarlas todas para después definir
    • Respuesta: pepe.cambiarCategoria(gerente)
  3. Escribimos un ejemplo concreto del mensaje
    • Lo escribimos en la consola. Sí, ¡antes de escribir el código!
    • Vemos que todo rompe porque no está implementado, pero ¡no nos asustamos!
  4. Acción o consulta
    • Este es ¿un mensaje de acción o de consulta?
      • Si es acción, ¿qué esperamos que pase?
      • Si es conuslta, ¿qué esperamos que devuelva?
    • Respuesta: Es de acción, Pepe debe cambiar su categoría de cadete a gerente.
  5. Implementación
    • Codear lo necesario para que el ejemplo se comporte como esperamos
    • Pensar si hay más ejemplos para tener en cuenta
      • Implementar lo necesario para el nuevo ejemplo si es necesario
  6. Repetir hasta terminar el ejercicio - Iterar

Proponemos esta metodología porque:

  • Focaliza en una parte del problema (y por el momento nos olvidamos del resto)
  • Primero piensa en el qué antes que en el cómo
    • O sea, primero pensamos en mensaje antes que en la implementación del método
  • Propone un objetivo claro
    • Al escribir el uso en la consola, ya tenemos en claro qué le falta al sistema
    • Le perdemos el miedo a que nos explote un programa en la cara (práctica de debugging)
    • El requerimiento se resuelve cuando la consola deje de fallarnos (los errores guían el desarrollo)
  • En fin, propone una forma de trabajar iterativa e incremental

2. Manos a la obra

Ahora que tenemos un objetivo fijo:

bash
> pepe.cambiarCategoria(gerente)

¿Y qué tipo de objeto es gerente? ¿Un String o un objeto nuestro? Guiar la solución para definir los objetos pepe, cadete y gerente desde el principio.

Lo probamos para ver los errores e ir resolviéndolos hasta que el programa se comporte como esperamos.

Si comenzamos con un archivo vacío:

wlk
// pepe.wlk

Y con el archivo cargado probamos en la consola:

bash
pepe> pepe.cambiarCategoria(gerente)
 Unknown reference gerente

Vemos que nos dice que no encuentra una referencia para gerente. Lo que tiene sentido, porque no definimos ningún objeto con ese nombre (de hecho, no definimos nada todavía).

Así que definimos un objeto que represente al gerente, en su forma más básica:

wlk
// pepe.wlk

object gerente { }

Y volvemos a probar. Para eso hay varias opciones:

  • Cerrar la consola actual y volver a levantarla :-1:
  • Recargar la consola con :r y volver a tirar el mensaje (buscar con al fecha arriba ⬆️) :+1_
  • Recargar y re-ejecutar todo con :rr 👌

Vemos que ahora el mensaje de error cambió:

bash
pepe> pepe.cambiarCategoria(gerente)
 Evaluation Error!
  wollok.lang.EvaluationError: Error: Could not resolve reference to pepe

Ahora, como es de esperar, falta pepe. Volvemos a hacer lo mismo, definiendo un objeto:

wlk
// pepe.wlk

object pepe { }

object gerente { }

Con esto, ya tenemos los objetos que necesitamos definidos, y los podemos ver en el diagrama dinámico:

image

Así que volvemos a probar en la consola. (Hay que meterles el hábito de la metodología).

bash
pepe> pepe.cambiarCategoria(gerente)
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: pepe does not understand cambiarCategoria(arg 0)

Ahora el error nos dice que pepe no entiende el mensaje cambiarCategoria(arg 0).

Acá se pone interesante...

Preguntas gatillo

¿Qué significa que pepe no entienda el mensaje?

Respuesta: que no tiene definido ningún método con la firma cambiarCategoria/1. (El /1 es una forma de anotar que es un método que espera 1 parámetro. Se puede tener métodos con el mismo nombre y distinta cantidad de parámetros, pero no vamos a entrar en eso ahora).

Así que debemos definir un método method cambiarCategoria(unaCategoria) en pepe.

Y, como habíamos dicho, este debe ser un método de acción. O sea, debe cambiar algo, pero no devolver nada...

¿Qué cosa debe cambiar este método?

Respuesta: la categoría de pepe.

El problema es que ahora pepe no tiene nada, es un objeto vacío. Así que una decisión acá es decir que el objeto pepe tiene que conocer a otro objeto como su categoría, o sea, que va a tener un atributo cateogria.

"Cambiar algo" en objetos es mover (asignar) un atributo, lo que sería re-apuntar una flecha en el diagrama dinámico. Sin embargo, nuestro programa todavía no tiene ninguna flecha.

Pero no desesperemos, sigamos firme con la metodología: escribamos cómo sería el método, y veamos los errores.

wlk
// pepe.wlk

object pepe { 

    method cambiarCategoria(unaCategoria) {
        categoria = unaCategoria
    }

}

object gerente { }

Ya con escribir ese código nos sale un error de que el programa tiene una referencia desconocida:

image

O sea, el programa no sabe lo que significa el categoria a la izquierda de la asignación.

Eso es porque falta definirle el atributo var categoria

wlk
// pepe.wlk

object pepe { 

    var categoria

    method cambiarCategoria(unaCategoria) {
        categoria = unaCategoria
    }

}

object gerente { }

Ahora vemos que el error anterior desapareció pero aparece uno nuevo en la definición del atributo, diciendo que los atributos de los objetos deben ser inicializados:

image

Esto es porque una referencia siempre debe apuntar a un objeto, y no puede nunca estar apuntando a "la nada".

Acá Wollok se está dando cuenta que al iniciar el programa el atributo categoria no tiene nada asignado, por ende no va a estar apuntando a ningún objeto.

Wollok fuerza a tener siempre objetos "listo para usarse", y considera que si no tiene un atributo inicializado entonces el objeto no va a poder funcionar como se espera. En Wollok existe el null, pero no lo vamos a ver ahora.

Por suerte, el enunciado nos aclara que pepe siempre comienza siendo cadete.

Preguntas gatillo

¿Cómo hacemos para que pepe comienza siendo cadete?

Respuesta: hay que crear un nuevo objeto cadete e inicializar el atributo categoria con él.

Al final el código queda:

wlk
object pepe { 

    var categoria = cadete

    method cambiarCategoria(unaCategoria) {
        categoria = unaCategoria
    }

}

object cadete { }

object gerente { }

Y volvemos a probar en la consola:

Screen Recording 2026-01-12 at 17 10 44

Vemos que pepe comienza apuntando a cadete como su categoría, y lo podemos cambiar a gerente. También podemos volver a la categoría de cadete si le mandamos este por parámetro al mensaje cambiarCategoria.

¡Excelente! Primer punto completado 🚀

Propiedades - Métodos que no se escriben

Paramos la pelota para meter algo de teoría sobre lo que tenemos...

  • El enunciado nos dice que Pepe puede ser cadete o genrente, y nos pide poder configurar su categoría en cualquier momento
  • Para modelar eso
    • creamos 3 objetos: pepe, cadete y gerente
    • y un atributo en pepe que apunta a alguno de los otros 2 objetos: categoria
  • Para poder cambiar el atributo de pepe: apuntar la flecha categoria
    • hay que enviarle un mensaje a pepe, por ejemplo: pepe.cambiarCategoria(gerente)
      • Esto se conoce como encapsulamiento
    • y pepe debe implementar un método que asigne la nueva categoría:
wlk
    method cambiarCategoria(unaCategoria) {
        categoria = unaCategoria
    }

Setters y Getters

Como el paradigma de objetos propone tener objetos encapsulados, o sea, cada uno maneja su estado interno (atributos), los métodos que solo cambian o retornan algún atributo son muy comunes en los programas.

Vamos a jugar un poco con el código:

  1. Abrimos una nueva sesión de la consola
  2. Cerramos el diagrama dinámico (en realidad conviene solo ocultarlo, para poder verlo rápido durante la explicación)
  3. Y le cambiamos la categoría a Pepe
bash
pepe> pepe.cambiarCategoria(gerente)
 

Preguntas gatillo

Si no vemos el diagrama ¿cómo sabemos que Pepe cambió de su categoría?

Respuesta: hay que mandarle un mensaje preguntándole su categoría: pepe.obtenerCategoria()

¿Ese es un mensaje de acción o consulta?

Respuesta: consulta. Debe retornar su categoría, o sea, el objeto apuntado por su atributo

Acá podemos mostrar el diagrama rápidamente para entender que queremos retornar el objeto apuntado (y no un string u otra cosa que lo represente).

También podemos evaluar directamente esos objetos para que se entienda:

bash
pepe> cadete
 cadete
pepe> gerente
 gerente

Pero si lo consultamos ahora nos da un error diciendo que no entiende el mensaje:

bash
pepe> pepe.obtenerCategoria()
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: pepe does not understand obtenerCategoria()

Repasamos cómo seguir a partir de acá:

  • No entiende el mensaje porque no implementa un método
  • Hay que definir un método en el objeto pepe que se llame obtenerCategoria/0
  • Ese método debe retornar el atributo categoria

Lo escribimos (acá solo se presenta el código de pepe, el resto queda igual)

wlk
object pepe { 

    var categoria = cadete

    method cambiarCategoria(unaCategoria) {
        categoria = unaCategoria
    }

    method obtenerCategoria() = categoria

}

(podría ser su versión con ( return ... ))

Y reevaluamos todo con :rr y vemos que ya no explota nada.

Ahora podemos intercalar los cambiar y obtener (podemos limpiar la consola con ctrl + k o ctrl + l o algo similar):

bash
pepe> pepe.cambiarCategoria(cadete)
 
pepe> pepe.obtenerCategoria()
 cadete
pepe> pepe.cambiarCategoria(gerente)
 
pepe> pepe.obtenerCategoria()
 gerente

Acá explicamos cómo se conocen (por sus nombres en inglés) estos métodos:

  • El método que cambia un atributo por un parámetro se conocen como setter: cambiarCategoria/1, cambiarEnergia/1, cambiarAlgo/1
  • El método que retorna un atributo (y no tiene parámetros) se conocen como getter: obtenerCategoria/0, obtenerEnergia/0, obtenerAlgo/0
  • Los lenguajes de programación (y su comunidad) generalmente definen una forma para el nombre de estos métodos
    • por ej: setCategoria/1 y getCategoria/0
    • esto se conoce como convención: una forma de nombrar algún elemento del programa en el que se ponen de acuerdo la comunidad

La convención de Wollok para los getters y setters es con el mismo nombre del atributo (sin prefijo): categoria/1 y categoria/0

Modificamos el código:

wlk
object pepe { 

    var categoria = cadete

    method categoria(unaCategoria) {
        categoria = unaCategoria
    }

    method categoria() = categoria

}

Levantamos una nueva consola y volvemos a hacer las consultas con los nuevos nombres:

bash
pepe> pepe.categoria(gerente)
 
pepe> pepe.categoria()
 gerente
pepe> pepe.categoria(cadete)
 
pepe> pepe.categoria()
 cadete

Atención

Resaltar que ahora en el código de pepe hay 3 cosas distintas que se llaman "categoria":

  1. Definición del atributo: var categoria
  2. Método setter: method categoria(unaCategoria)
  3. Método getter: method categoria()
  • Los atributos y métodos se definen y usan de forma distintas
    • Los atributos se definen con var o const, mientras que los métodos con method
    • Los atributos se usan como cualquier referencia, mientras que los métodos a través de envios de mensajes (después de un .)
  • Los getter y setter tienen distinta firma, debido a sus parámetros
    • Los getters no esperan parámetro y son de consulta: categoria/0
    • Los setters esperan el nuevo objeto y son de acción: categoria/1
    • Como tienen distintos parámetros, se invoca uno u otro método en base a los parámetros que tiene el mensaje

Atributos property

Como estos métodos son comunes en los lenguajes orientado a objetos, muchos traen una forma de declararlos fácilmente.

En Wollok, se puede usar el modificador property en la definición de un atributo para crear sus getters y setters.

El código de pepe quedaría así (dejo la versión con comentarios para los estudiantes, pero se sugiere probarlo sin los métodos escritos para que se entienda cómo funciona):

wlk
object pepe { 

    var property categoria = "asd."

/*
Este código se crea con el "property" del atributo categoria

Está comentado para recordar lo que hace, pero no hace falta escribirlos

GETTER:
    method categoria(unaCategoria) {
        categoria = unaCategoria
    }

SETTER:
    method categoria() = categoria
*/
}

Y repetimos las consultas para probar que anda (probar es siempre mucho muy importante):

bash
pepe> :rr
 Reloading environment
pepe> pepe.categoria(gerente)
 
pepe> pepe.categoria()
 gerente
pepe> pepe.categoria(cadete)
 
pepe> pepe.categoria()
 cadete

Posibles discusiones

  • var property vs const property
  • ¿Qué pasa si escribo un método getter o setter usando property?
  • Ahora tengo un getter y antes no, ¿es eso mejor?

Código final del primer punto

El código final para el primer punto queda en estas pocas líneas

wlk
object pepe { 

    var property categoria = cadete

}

object cadete { }

object gerente { }

Continuemos con el siguiente...

3. Segundo requerimiento

Ahora que ya conocemos la metodología, vamos a aplicarla para el segundo punto...

image

¿Cuál es el requerimiento?

Respuesta: conocer el sueldo base de pepe

¿Qué mensaje le vamos a enviar a qué objeto?

Respuesta: pepe.sueldoBase()

¿Es de acción o de consulta?

Respuesta: de consulta

Ejemplo de consulta

Respuesta: siendo cadete esperaríamos

bash
> pepe.sueldoBase()
1500

Comenzamos

Probamos la consulta en la consola

bash
pepe> pepe.sueldoBase()
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodExcegption: pepe does not understand sueldoBase()

Y leemos el error: pepe no entiende el mensaje sueldoBase()

¿Cómo se soluciona?

Respuesta: implementando el método (de consulta) en pepe

El enunciado dice que el sueldo base es siempre

  • 1000
  • más un extra dependiendo de la categoría
    • 500 para cadete
    • 1500 para gerente

Escribimos la parte fácil:

wlk
object pepe { 

    var property categoria = cadete

    method sueldoBase() = 1000 + ...

}

Preguntas gatillo

¿Cómo calculamos el extra de la categoría?

Respuesta: si el extra depende de la categoría, ¡lo mejor es preguntárselo a ella!

wlk
method sueldoBase() = 1000 + categoria.extra()

Terminamos de escribir el método y volvemos a probar:

wlk
pepe> :rr
✓ Reloading environment
pepe> pepe.sueldoBase()
✗ Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: cadete does not understand extra()
    at pepe.pepe.sueldoBase() [pepe.wlk:5]

Ahora nos dice que cadete no entiende el mensaje extra()

También nos muestra el stack trace, o sea, dónde se produjo el error: [pepe.wlk:5] (con ctrl + click nos navega al método que envía el mensaje)

¿Cómo se soluciona?

Respuesta: implementando el método en el objeto cadete

¿Es de acción o de consulta?

Respuesta: consulta. Debería retornar 500

Lo escribimos

wlk
object cadete { 
    method extra() = 500
}

Y volvemos a probar

bash
pepe> :rr
 Reloading environment
pepe> pepe.sueldoBase()
 1500

¡Funciona! ¡Bravo!

Preguntas gatillo

¿Ya terminamos?

Respuesta: no. Todavía falta probar siendo gerente.

Pensar todos los casos de prueba es algo importante que queremos ejercitar y lo vamos a estar practicando la clase que viene

¿Y cómo probamos ese caso?

Respuesta: hay que cambiarle la categoria a gerente y preguntarle el sueldo base

bash
pepe> pepe.categoria(gerente)
 
pepe> pepe.sueldoBase()
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: gerente does not understand extra()
    at pepe.pepe.sueldoBase() [pepe.wlk:5]

Nos dice que gerente no entiende el mensaje extra(). ¡Que bueno! que probamos este caso, nos falta implementar parte del código aún.

Implementamos el método que falta:

wlk
object gerente { 
    method extra() = 1500
}

Y volvemos a probar todo

bash
pepe> :rr
 Reloading environment
pepe> pepe.sueldoBase()
 1500
pepe> pepe.categoria(gerente)
 
pepe> pepe.sueldoBase()
 2500

¡Genial! Ahora sí ya terminamos

Polimorfismo de Categorías

Paramos la pelota nuevamente para analizar lo que hicimos...

La clave de este punto está en cómo calculamos el monto extra en base a la categoría:

wlk
method sueldoBase() = 1000 + categoria.extra()
  • Ignorancia: Para calcular el sueldo base, pepe no pregunta qué categoría tiene
  • Delegación: En vez de eso, delega la responsabilidad de "calcular" el extra en la categoría mandándole directamente un mensaje
  • Responsabilidad: Es responsabilidad de cada categoría el saber cuál es su extra (en base a quién representa el objeto)

Polimorfismo

Todas esas ideas se combinan para diseñar una solución con polimorfismo, uno de los pilares de la programación orientada a objetos

La clave está en que pepe usa indistintamente a cualquiera de sus categorías

Beneficios de este diseño:

  • Bajo acoplamiento: pepe no sabe cómo funcionan las categorías por dentro, las categorías tampoco saben lo que hace pepe. Solamente se comunican a través de un contrato que es el mensaje categoria.extra()
  • Extensividad: es "fácil" entender cómo agregar más categorías (siempre que entiendan el mensaje extra/0, pero sino también es "fácil" adaptar el mensaje a lo que se necesite)

Interfaz

Hablamos mucho sobre "categorías" pero nunca escribimos una definición sobre eso. Definimos a gerente y cadete, pero nada relacionado con "categoría". (El único lugar es el nombre del atributo en pepe, lo que habla bien sobre nuestra expresividad)

Podemos decir que la categoria es cómo pepe conoce al objeto a cual preguntarle el extra. Este conjunto de mensajes por el cual un objeto se comunica con otro se llama interfaz

En este programa, la interfaz categoria está constituída solamente por el mensaje extra/0

4. Último requerimiento

¿Cuál es el requerimiento?

Respuesta: conocer el sueldo (final) de pepe teniendo en cuenta su contrato

¿Qué mensaje le vamos a enviar a qué objeto?

Respuesta: pepe.sueldo()

¿Es de acción o de consulta?

Respuesta: de consulta

Ejemplo de consulta (comenzando por lo más fácil)

Respuesta: siendo cadete (1500) y con un contrato básico (1000) esperaríamos

bash
> pepe.sueldo()
2500

Lo escribimos en la consola para tener el ya conocido error de no entiende el mensaje y ponernos a codear...

"Yo mismo"

Este punto tiene como objetivo introducir la referencia de todo objeto a sí mismo: self

Sin embargo, podemos hacer una solución con las herramientas ya presentadas:

wlk
object pepe { 

    var property categoria = cadete

    method sueldo() = pepe.sueldoBase()

    method sueldoBase() = 1000 + categoria.extra()

}

Y, como siempre, lo probamos:

bash
pepe> :rr
 Reloading environment
> pepe.sueldo()
1500

Funciona!

Igual el IDE está avisando de algo:

image

Presentar self

Es una referencia que conocen todos los objetos y que apunta a sí mismos

Cambiar el código para arreglar el aviso y probar que la consola se sigue comportando igual

wlk
method sueldo() = self.sueldoBase()

En el diagrama dinámico, las referencias a self no aparecen, pero podemos forzarlas definiendo un atributo (yoMismo) inicializado en self en todos los objetos

wlk
object pepe { 
    const yoMismo = self

    var property categoria = cadete

    method sueldo() = self.sueldoBase()

    method sueldoBase() = 1000 + categoria.extra()

}

object cadete { 
    const yoMismo = self

    method extra() = 500
}

object gerente { 
    const yoMismo = self

    method extra() = 1500
}
image

Acá se visualiza:

  • El "rulo" que significa una referencia a "sí mismo"
  • En un programa prueden existir muchos atributos con el mismo nombre, siempre que salgan de objetos distintos
  • En el código, la paralabra self significa algo distinto depende en qué objeto esté escrito

Comentar (o borrar) los atributos yoMismo para seguir con el ejercicio

Posibles discusiones

Algunas aristas que puede tomar esta clase y tocan conceptos importantes

Repetir código

Este punto se puede solucionar sin self copy-pasteando la definición de sueldoBase()

wlk
method sueldo() = 1000 + categoria.extra()

method sueldoBase() = 1000 + categoria.extra()

Vamos a evitar repetir lógica a toda costa, y fomentar soluciones con reutilización de lógica

Si ya existe un componente (método) que resuelve parte de mi problema, lo mejor es aprovecharlo

Romper el requerimiento anterior

También se podría renombrar el método anterior para que se llame sueldo() y continuar desde ahí (de esa forma se evita el código repetido)

wlk
method sueldo() = 1000 + categoria.extra()

Hacer notar que esto rompe con el requierimiento anterior, y que además es contraproducente: perdemos una abstracción que ya existe

Romper la sintaxis

Un error común al comenzar la POO es olvidarse el receptor de un mensaje

wlk
method sueldo() = sueldoBase()

method sueldoBase() = 1000 + categoria.extra()

Esto marca un error en el IDE

image

Aún así es bueno recordar que en este paradigma (puro), todas las computaciones suceden a partir de enviar un mensaje.

Un mensaje siempre tiene (necesita de) un objeto receptor: objeto.mensaje()

Segundo polimorfismo

Volver a al problema

Esperábamos que nos diera

bash
> pepe.sueldo()
2500

Pero ahora nos da

bash
pepe> :rr
 Reloading environment
> pepe.sueldo()
1500

Hardcodear una solución

Si al resultado le falta 1000, se lo sumamos...

wlk
method sueldo() = self.sueldoBase() + 1000

Y probamos

bash
pepe> :rr
 Reloading environment
> pepe.sueldo()
2500

Excelente...

o bueno, no tanto... esta implementación fue para salir del paso, pero no resuelve el enunciado

Los contratos

Acá buscamos introducir a los distintos contratos de forma similar a las categorías

  • Entender que Pepe debe estar asociado a una de los contratos: básico, porcentual o presentismo
  • Se debe poder elegir (configurar) qué contrato tiene Pepe
  • El sueldo se debe calcular a partir del contrato (y categoría) configurado(a)

Para eso

  1. Definimos 3 nuevos objetos
wlk
object basico {

}

object porcentual {
    
}

object presentismo {
    
}
  1. Agregamos un nuevo atributo en pepe
wlk
var property contrato = basico
  1. Le mandamos un mensaje al contrato para calcular el sueldo
wlk
method sueldo() = self.sueldoBase() + contrato.remuneracion()

Probamos qué onda

bash
pepe> :rr
 Reloading environment
> pepe.sueldo()
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: basico does not understand remuneracion()
    at pepe.pepe.sueldo() [pepe.wlk:5]

Y leemos el error

basico no entiende el mensaje remuneracion()

¿Es de acción o de consulta?

Respuesta: de consulta

Implementamos el método con el código necesario para que la prueba funcione como antes

wlk
object basico {
    method remuneracion() = 1000
}

Y probamos

bash
pepe> :rr
 Reloading environment
> pepe.sueldo()
2500

Al repetir estos pasos buscamos

  • que se adopte la metodología de trabajo
  • fortalecer el diseño que aprovecha el polimorfismo
  • mostrar cómo el programa crece de forma iterativa e incremental

Porcentual, ¿nos conocemos?

Una vez implementado el contrato más fácil, continuamos con el siguiente: porcentual

Este punto plantea el problema de cómo el porcentual conoce a pepe

El objetivo es introducir las 3 formas de conocer un objeto desde otro:

  • global: la referencia está hardcodeada en el código
  • atributo: la referencia está en un atributo del objeto
  • paramétrico: la referencia viene por parámetro

Comenzamos a implementar el método

Global

La primera forma de implementar este punto suele ser la global

wlk
object porcentual {
    const porcentaje = 15
    method remuneracion() = pepe.sueldoBase() * porcentaje / 100
}

Probamos, configurando a pepe antes

bash
pepe> pepe.contrato(porcentual)
 
pepe> pepe.sueldo()
 1725

Joya!

Además vemos cómo queda el diagrama

image

Marcamos algunas características de esta solución:

  • La referencia está hardcodeada en el código del método, lo que significa que no se puede cambiar sin reestructurar el código
  • Eso genera una dependencia implícita de un objeto a otro: no hay flecha en el diagrama (de porcentual hacia pepe) ni en la firma del método
  • Recordamos que las referencias son "flechas direccionadas" que tienen un sentido (desde el objeto que la tiene hacia el que apunta)

Atributo

Si la referencia a pepe hardcodeada en el método molesta, entonces agreguemos otro atributo en porcentual

wlk
object porcentual {
    const porcentaje = 15
    const empleado = pepe

    method remuneracion() = empleado.sueldoBase() * porcentaje / 100
}

Así la dependencia queda evidente en el diagrama

image

Y todo sigue funcionando como esperábamos

bash
pepe> pepe.contrato(porcentual)
 
pepe> pepe.sueldo()
 1725

Características de esta solución:

  • La referencia de porcentual hacia pepe es explícita y tiene un nombre: empleado 👍
  • Si bien ahora es un atributo constante, podríamos cambiar el programa para que sea variable y apunte a otro posible empleado
  • Este modelo propone un par de referencias circulares entre contrato y empleado. Esto no es un problema per se, pero hay que tener cuidado: armar y romper estos vínculos implican cambiar dos referencias, con una no basta (podría dejar el un estado inconsistente)

Parámetro

Como última solución (y con la que nos vamos a quedar para este ejercicio) es pasar al empleado por parámetro:

wlk
object porcentual {
    const porcentaje = 15

    method remuneracion(empleado) = empleado.sueldoBase() * porcentaje / 100
}

A diferencia de las otras dos opciones, esta variante cambia la firma del mensaje que pepe debe mandarle a su contrato

bash
wollok:pepe> :rr
 Reloading environment
wollok:pepe> pepe.contrato(porcentual)
 
wollok:pepe> pepe.sueldo()
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: porcentual does not understand remuneracion()
    at pepe.pepe.sueldo() [pepe.wlk:5]

remuneracion() sin parámetros y remuneracion(empleado) con parámetro tienen distinta firma, o sea que responden a mensajes distintos. Uno es remuneracion/0 y el otro remuneracion/1

Ajustamos el mensaje en el método sueldo():

wlk
method sueldo() = self.sueldoBase() + contrato.remuneracion(self)

Atención que esta es la primera vez que mandamos self por parámetro

Llamar la atención a esto y recordar que es una referencia al propio objeto

Probar que ahora la solución vuelve a funcionar

bash
wollok:pepe> :rr
 Reloading environment
wollok:pepe> pepe.contrato(porcentual)
 
wollok:pepe> pepe.sueldo()
 1725

Y por qué no mandamos el sueldo base?

Otra variante que puede surgir es mandar por parámetro directamente el sueldo base

wlk
object porcentual {
    const porcentaje = 15

    method remuneracion(sueldoBase) = sueldoBase * porcentaje / 100
}

Y que el mensaje se mande en el código del método sueldo()

wlk
    method sueldo() {
        const base = self.sueldoBase() 
        return base + contrato.remuneracion(base)
    }

Esta también es una solución válida

Por detalles del ejercicio vamos a continuar pasando todo el empleado

Algunos posibles comentarios oportunos:

  • Tanto el empleado como el sueldo base son objetos que se pasan por referencia: no hay objetos más pesados que otros
  • Pepe sabe contestar más cosas que un número, por ejemplo su sueldo base
  • Pasar el número evita que el contrato tenga que enviarle mensajes a pepe, eso reduce el acoplamiento

Bien! Perooo, anda todo o rompimos algo que ya andaba?

Tuvimos que tocar el código de pepe. Cómo podemos saber que no rompimos algo que ya estaba funcionando?

Esto es algo en lo que haremos foco la próxima clase: testing.

Por ahora decimos que, ante cada cambio, hay que probar todos los casos.

Sí, todos. Cuáles son?

Respuesta: 2 categorías * 3 contratos = 6 configuraciones posibles. O sea que al menos 6.

Acá se recomienda armar una sesión de consola con las 6 configuraciones y ver qué responde el programa (incluso sabiendo que todavía no codeamos el contrato por presentismo) y no cerrarla para poder jugar con el código y hacer :rr.

#TODO: Abrir un issue para que el :rr se frene ante un error (y pregunte si continuar / reiniciar / abortar)

El caso interesante es calcular el sueldo de pepe con el contrato básico, ya que rompimos el contrato (mensaje) que había entre los dos objetos.

bash
pepe> pepe.contrato(basico)
 
pepe> pepe.sueldo()
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: basico does not understand remuneracion(arg 0)
    at pepe.pepe.sueldo() [pepe.wlk:5]

Rompimos el polimorfismo de los contratos 👎 Qué hacemos?

Bueno, una solución sencilla es cambiar el método en basico para que reciba un parámetro que no usa:

wlk
object basico {
    method remuneracion(empleado) = 1000
}

Mostrar que esto puede pasar, y si bien recibir parámetros que no se usan no es lo mejor, favorece una solución polimórfica, lo que sí es bueno.

Diseñar es tomar decisiones

De las pruebas anteriores llegamos al presentismo:

bash
pepe> pepe.contrato(presentismo)
 
pepe> pepe.sueldo()
 Evaluation Error!
  wollok.lang.MessageNotUnderstoodException: presentismo does not understand remuneracion(arg 0)
    at pepe.pepe.sueldo() [pepe.wlk:5]

A estas alturas, ya debería salir fácil que debemos implementar un método en presentismo

wlk
object presentismo { 
    method remuneracion(empleado) = ...
}

Pero acá sale una pregunta importante:

Quién tiene la responsibilidad de saber la cantidad de faltas?

Respuesta: Una primera intuición puede que nos diga el empleado. Pero esa información también podría estar en el mismo objeto presentismo

El modelo (de objetos) NO es la realidad

Para demostrar que si bien la realidad nos intuye a pensar de una forma, pero que nosotros podemos elejir cómo modelarla, vamos a ponerlo en el presentismo

wlk
object presentismo { 
    var property faltas = 0

    method remuneracion(empleado) = empleado.sueldoBase() - faltas
}

Y probar que funciona:

bash
pepe> :rr
 Reloading environment
pepe> pepe.contrato(presentismo)
 
pepe> pepe.sueldo()
 3000

Mmmm... hay más casos para probar? 🤔

Respuesta: estaría bueno probar algún caso con faltas

bash
pepe> presentismo.faltas(1)
 
pepe> pepe.sueldo()
 2900

Prestar atención a que, para probar el caso de pepe con una falta, hay que configurar el objeto presentismo, y no hace falta pasar por pepe para eso

En un programa de objetos, los objetos colaboran entre ellos para llegar al resultado. No hay objetos más importantes que otros

También se puede mostrar la variante donde las faltas la tenga pepe y mostrar cómo se usaría ese modelado.

5. Conclusiones

Una vez terminado el ejercicio, hacemos un resumen de lo que aprendimos

PolimorfismoS

  • El paradigma de objetos sale beneficiado con soluciones polimórficas
  • Hay 2 polimorfismos acá: categorías y contratos
    • Es pepe quién los usa polimórficamente (importante!)

#TODO: Poner el gif de las cómo pepe cambia de categorías y contratos. Mostrar el sueldo.

Objetos chetardos

  • Comenzamos modelando a pepe que es un empleado (algo tangible)
  • Luego modelamos cosas más abstractas
    • Objetos que representan categorías o contratos
    • No tienen una contrapartida en la realidad
  • El modelo NO es la realidad

Pruebas

  • La importancia de probar todos los casos posibles
  • El testing lo hacemos durante el desarrollo
  • Incluso escribíamos la prueba antes que la implementación: TDD
  • Sin embargo, la consola se queda corta para esto, la clase que viene vamos a ver algo más profesional: tests automatizados