Clases 2011c1


Clase 6 - 19 de abril de 2011 - Prototipos

posted Apr 19, 2011, 3:16 PM by Nicolas Passerini   [ updated Apr 20, 2011, 1:23 PM ]

Vamos a ver una nueva forma de modelado con objetos, que también nos va a mostrar una alternativa a la programación tradicional orientada a objetos (asociando código a las clases). Como ya vimos existen múltiples problemas que no pueden ser atacados elegantemente mediante la clasificación y la herencia, al hacerlo nos vemos obligados a repetir código, hay porciones de código que no podemos reutilizar.

Una desventaja de las clases es que muchas veces nos llevan a pensar en la estructura y no en el comportamiento. Modelos como el UML colaboran para que nos focalicemos en los diagramas de clases y en la estructura.

Las clases no son la única forma de pensar la programación orientada a objetos.

Prototipos

En un ambiente de objetos basado en prototipos lo único que existe en el sistema son objetos, no hay clases. 

Existen ambientes como los de Smalltalk en los que todo es un objeto (incluyendo las clases) pero aquí todos los objetos son iguales, no hay objetos "especiales" (como las clases en Smalltalk.

La forma de crear un objeto es clonando (copiando) otro objeto ya existente. Un prototipo es un ejemplo de un objeto del cual puedo crear muchas copias. Estos prototipos nos permiten lograr todos los efectos que en los ambientes de objetos tradicionales son logrados por las clases, y muchas más variantes que en esos ambientes no estarían permitidas.

Una vez copiado el objeto no tiene nada que ver con el prototipo y puede ser modificado arbitrariamente.

Dado que no existe la noción de clases ni de herencia para la clasificación y/o reuso, necesitamos otros mecanismos para crear objetos y para compartir comportamiento:
  • La creación se basa en la clonación de objetos (denominados prototipos).
  • El mecanismo para compartir código se basa en traits o mixins.
El lenguaje que vamos a ver para ejemplificar esta idea se denomina Self.

¿Cómo se crean objetos si no tengo clases?

Hay dos formas de crea un objeto:
  • Clonando un objeto ya existente
  • Creando un objeto "literal" (similar a crear un objeto vacío).

¿Cómo se define el comportamiento?

El comportamiento y el estado (todo junto) de un objeto está dado por un conjunto de slots, que pueden ser de tres tipos:
  • "Data" slots
    • Constantes (definidos con "=")
    • Asignables (definidos con "<-")
  • "Method" slots
Todo lo que tiene el objeto son slots y todos los slots son intercambiables en cualquier momento, es decir, quien envía un mensaje no tiene por qué enterarse de si el slot que se está utilizando para resolver su mensaje es un data slot o un method slot.

Composición y reutilización

La forma de reutilización es a partir de lo que se denomina parents o delegates. Cuando un objeto recibe un mensaje que no entiende ese mensaje se reenvía a alguno de sus delegates, Para agregar un delegate a un objeto, en self se le agrega un slot que tenga sufijo "*".

La estrategia para resolver conflictos es explícita, es decir, si más de un delegate entiende un mensaje que llega a la cadena de delegación, entonces ese conflicto debe ser resuelto manualmente por el programador (utilizando un mecanismo similar al que se usaba para resolver conflictos entre los traits en Pharo).

No hay "super" (porque no hay herencia), pero en caso de necesitar invocar a un mensaje de un padre que se está sobreescribiendo en el propio objeto, esto se puede hacer mediante un resend.


También es muy importante el objeto lobby, que nos permite acceso a todos los objetos predefinidos del lenguaje (por ejemplo colecciones, strings, números). Es un objeto que es visible globalmente (está en la cadena de delegación de todos los objetos).

Algunos ejemplos básicos en Self
  • (). 
    Me devuelve un objeto sin slots

  • (|a. b|)
    Un objeto con dos slots asignables a y b, que aparecen inicializados en nil.

  • (|a = 42. b<-21. c = (error: 'no se puede')|)
    Me devuelve un objeto con un slot constante a = 42, uno variable b inicializado en 21 y un método en el slot c que manda el mensaje error:

    Un problema que tenemos es que error: no es algo conocido, tenemos que agregarle un delegate que tenga el mensaje definido error:
    El obejto más común con ese comportamiento tiene un nombre raro: oddball, y para agregar un parent podemos hacer así:

    (|
      parent* = traits oddball.
      a = 42.
      b<-21.
      c = (error: 'no se puede')
    |)

Un ejemplo un poco más complejo: Animales

  • Para pensar este ejemplo arrancamos definiendo un objeto que representa a un pato, que entiende dos mensajes:
    caminar = (^ 'Camino con mis dos patas')
    nadar = (^ 'Nado un poco')

    También le agregamos el delegate usual:
    parent* = traits oddball. 

  • A continuación queremos tener un cocodrilo, como tiene cosas en común con pato arrancamos copiándolo (le enviamos clone). Y modificamos el caminar:
    caminar = (^ 'Camino con mis cuatro patas')

  • Pero el nadar es común, entonces podríamos tener ganas de crear un trait, creamos un objeto nuevo y movemos (botón derecho, "move") el método nadar a él.

    Luego ponemos ese nuevo objeto como parent de los dos anteriores (lo referenciamos mediante un slot llamado acuatico*)

    Es muy interesante notar que no se necesita una construcción especial para tener un trait: es simplemente un objeto que pongo en mi cadena de delegación y tiene sólo el mensaje nadar (es importante que no tenga nada más). No hay diferencia entre los "objetos" y los "traits"

¿Por qué todas las ventanitas dicen "A slots object"?

    Un problema que tenemos es que los tres objetos y los dos traits que creamos no tienen nombre, para eso tenemos varios pasos un poco burocráticos:
    • Desde un shell (o algún objeto) agregar slots constantes denominados pato, perro y cocodrilo que referencien a los objetos que creamos antes.
    • Agregar el objeto o shell al objeto globals (parent del lobby), con el nombre animales.
    • Marcar  el slot animales como "creator" y a continuación lo mismo con los  objetos pato, perro y cocodrilo.
    Al hacer eso podemos ver que cambiaron los nombres de las ventanas. Nuestro objeto o shell pasó a denominarse "animales" y los demás se llaman "animales pato", "animales perro" y "animales cocodrilo" respectivamente.



    Clase 5 - 12 de abril de 2011 - Aspectos

    posted Apr 7, 2011, 12:52 PM by Nicolas Passerini   [ updated Apr 15, 2011, 5:07 PM ]

    Introducción

    Como parte de la unidad vamos a seguir viendo diferentes estratégias para romper con "la dictadura de la clase". Es decir, para definir comportamiento del sistema fuera de las formas tradicionales: la clase y su jerarquía (superclases).

    La clase pasada vimos mixins y traits, como variantes a los modelos de jerarquía múltiple, simple y composición.
    Vimos que la intención de los traits era la de reutilizar un cierto código, que de otra manera estaría disperso, cuando este aplica a varias jerarquías diferentes.

    Todos estos modelos se refieren a la capacidad de construir la clase en base a componer métodos. Pero esta construcción es intencional, necesaria e interna, con lo cual cada clase que quiera reutilizar la lógica debe ser modificada desde ella misma. Por ejemplo, por cada subclase de Animal que querramos que tenga el trait TCuadrupedo, debemos modificar su declaración para incluirlo.

    Lo anterior se explica, por el hecho de que en realidad la mayoría de los traits van a modelar comportamiento de negocio, que van a estar muy vinculados con la lógica de las clases y vice-versa. Salvo algunos casos, por más que sea funcionalidad "adicional", va a ser simplemente funcionalidad, que cada clase utilizará concientemente. Por esto, el trait mismo establece exigencias o un contrato por parte de la clase. Entonces, hay una colaboración entre ellas.
    Esto demuestra que estos mecanismos si bien permiten aplicar a varias jerarquías, tienen un campo de acción limitado al diseño de nuestras clases y a su estructura (mensajes).

    De esto surgen dos características:
    • acoplamiento: las clases estan acopladas al trait (lo declaran y usan)
    • estatismo: sería como el acoplamiento a la inversa, el trait requiere ciertas cosas de la clase. Entonces está acoplado al diseño, y a la estructura de las clases. Además estos traits no permiten meterse en el flujo dinámico de ejecución de la clase, salvo que ellas se lo deleguen (con mucha burocracia, algo así como template methods).

    Entonces, este mecanismo, si bien útil, parece tener ciertas limitaciones:
    • ¿Qué pasa si la funcionalidad a agregar no es simplemente agregar comportamiento o métodos, sino, requiere meterse en el medio de la ejecución de la lógica ya definida por las clases ? Ej: observer, fíjense que si bien se puede implementar con traits, este no es el que lanza los eventos, sino que cada clase debe encargarse de hacerlo.
    • ¿Qué pasa si no quiero que mis clases si quiera sepan de la funcionalidad a agregar ?
    • ¿Qué pasa si quiero afectar múltiples clases, de hecho quizás, sin saber cuales son esas clases o jerarquías ? Y que sea extensible a futuro (o sea, cuando alguien agregue otra clase o jerarquía ?
    • ¿Qué pasa si quiero poder activar / desactivar la funcionalidad ?
    Acá entra la idea de "aspecto".

    Cross-cutting concerns

    Todos los paradigmas utilizan la idea de abstracción y encapsulamiento, aunque en diferentes maneras. Y todos ellos intentan entonces modelar de forma cohesiva y encapsulada ciertos bloques de construcción. Es la vieja estratégica de "divide y conquistarás".
    Algunos ejemplos: procedimientos, funciones, módulos, clases, métodos, y hasta traits :)

    ¿Qué pasa cuando una funcionalidad de mi sistema atraviesa todas esas estructuras del lenguaje ?
    Por ejemplo, en objetos, ¿qué pasa cuando se necesita agregar la misma lógica a muchos métodos?

    A esa lógica o funcionalidad que atraviesa las estructuras del lenguaj se la llama cross-cutting concern.
    (Fíjense que esta es una de las limitaciones que mencionamos a los traits.)

    Un aspecto, es una herramienta para modelar y resolver estos cross-cutting concerns. Lo que se intenta es conseguir una unidad cohesiva, y encapsulada.

    Consta, básicamente de 2 cosas:
    1. La lógica a ejecutar, llamada advice
    2. Dónde ejecutarla, en qué puntos del código y de la ejecución del sistema. A los puntos a los que les agrego comportamiento mediante aspectos se los denomina joinpoints.
      Por otro lado, un pointcut es una query que refiere a un conjunto determinado de joinpoints, a los que se le aplicará un advice.

    En resumen, las ventajas de programar con aspectos (AOP) son:
    • Menos intrusivo (pointcut + weaving), por lo menos que los de Pharo... se agregan transparentemente. Un Advice es como un glue method.
      • Los glue methods en Pharo están en la clase, en AspectJ están fuera de la clase.
    • Puede hacer más que agregar métodos, nos dan más variantes, por ejemplo:
      • Puedo interceptar la asignación de una variable o la invocación de un método o constructor.
      • Trabajar con la estructura sintáctica del método o el flujo de ejecución.

    Generalmente estas cuestiones cross-cutting se utilizan para cosas tecnológicas ("frameworkosas"):
    • Objetos observables como los que le gustaban al Arena.
    • Políticas de seguridad.
    • Loggear todas las veces que pasa x cosa (para debug).
    • Objetos transaccionales.
    • Lazyness.

    Para llevar estas ideas a la práctica vamos a usar un lenguaje que extiende al Java, denominado AspectJ.

    Primer ejemplo: Loggeo

    Algunos conceptos básicos de AspectJ:
    • Defino un aspecto:
      public aspect SysoutSimpleObservableAspect

    • Luego un pointcut denominado fieldWrite, que atrapa todas las modificaciones a cualquier field de cualquier objeto de cualquier clase en el package examples.simple:

      pointcut fieldWrite(Object target, Object newValue) :  // Nombre y parámetros
          set(* examples.simple..*)    // Todos los fields de cualquier tipo en ese package                      
          && args(newValue)            // Capturo el nuevo valor del field
          && target(target)            // Capturo el objeto al que se le está modificando el field


      Es importante ver que es declarativo, es decir target y newValue no son parámetros que "llegan" sino que son indentificadores que se vinculan con el nuevo valor del field y el objeto receptor, respectivamente.

    • Luego queremos definir un advice, que se parece a un método con una cabecera especial:

      void around(Object target, Object newValue) : fieldWrite(target, newValue) {
          ...                          // código que quiera agregar
          proceed(target, newValue    
      // se ejecuta el código original (*)
          ...                          // más código que se ejecuta después del field set.

      }

      La palabra clave around permite reemplazar el código original (en este caso la modificación de un field), por el código que nosotros querramos.

      Para ser más estrictos, proceed no ejecuta el código original, porque puede haber más de un aspecto sobre el mismo joinpoint, entonces proceed pasa al siguiente aspecto en la chain of responsibility.

    • También podemos ponerle variables al aspecto para guardar cualquier información que necesite para trabajar. Para eso también es importante definir el ciclo de vida del aspecto, el caso más simple es marcarlo como "singleton", lo que hace que tengamos una única instancia para toda la aplicación:

      public aspect SysoutSimpleObservableAspect isSingleton() {
          public int counter = 0;
          ...
      }  

      Define una variable counter que será única para todas las veces que se utilice el aspecto.

      Otros scopes posibles son: perthis (por cada lugar desde donde se llama el código), pertarget (uno por cada objeto al que se accede).

    • Otra variante es utilizar una annotation, en lugar de patrones por nombre:

      pointcut fieldWrite(...) : ... && target(@LoggeableAnnotation target);

      Permite trabajar con fields (o lo que fuera) en clases que tengan la annotation @LoggeableAnnotation.

    Segundo ejemplo: Mixins.

    Para agregar un mixin debemos realizar dos pasos:
    1. Desde el aspecto hacemos que la clase que nos interesa implemente una interfaz definida por nosotros:

          declare parents : @Observable * implements ObservableObjectSupport;

      Esto hace que todas las clases anotadas con @Observable implementen la interfaz ObservableObjectSupport

    2. En el aspecto definimos los métodos que proveen la implementación de esa interfaz, por ejemplo:

          public void ObservableObject.addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
              this.changeSupport.addPropertyChangeListener(propertyName, listener);
          }
         
          public void ObservableObject.removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
              this.changeSupport.removePropertyChangeListener(propertyName, listener);
          }
         
          public void ObservableObject.fieldChanged(String fieldName, Object oldValue, Object newValue) {
              this.changeSupport.firePropertyChange(fieldName, oldValue, newValue);
          }

      También podemos agregar variables:

          private transient PropertyChangeSupport ObservableObject.changeSupport;



    El resto del proceso es ya conocido, declaramos un pointcut y un Advice:

        pointcut fieldWrite(ObservableObjectSupport target, Object newValue) :
            set(* *..*)
            && args(newValue)
            && target(@Observable target)
            && !withincode(*.new(..))
            && within(@Observable *);

        void around(ObservableObjectSupport target, Object newValue) : fieldWrite(target, newValue) {
            String fieldName = thisJoinPoint.getSignature().getName();
            Object oldValue = Utils.getField(target, fieldName);

            proceed(target, newValue);

            if (oldValue != newValue) {
                target.fieldChanged(fieldName, oldValue, newValue);
            }
        }



    Se puede ver que desde el advice podemos mandarle mensajes al target que son los que agregamos mediante aspectos.

    En el uso desde AspectJ, podemos utilizar la clase y mandarle también los mensajes de la interfaz que agregamos por aspectos, es decir que tiene el mismo comportamiento que una interfaz agregada de la forma tracicional (implements).

        public static void main(String[] args) {
            ObservableTestObject object = new ObservableTestObject();
           
            // Agrego un listener...
            //Este mensaje fue agregado por el aspecto!
            object.addPropertyChangeListener("name", new PropertyChangeListener(){

                ...

            });
           
            // El setName dispara la llamada al property listener.
            object.setName("nuevoNombre");

        }


    Un detalle técnico es que tenemos dos interfaces: ObservableObject, que es la que usa el dominio y ObservableObjectSupport, que la extiende y que nos permite utilizar el objeto desde el aspecto.



    Clase 4 - Traits y Mixins (5 de abril)

    posted Apr 5, 2011, 3:13 PM by Nicolas Passerini   [ updated Sep 5, 2011, 12:44 PM ]

    Ya nos metemos de lleno en la unidad 2. Vamos a tocar un tema un poco más abstracto.

    Lo importante en el paradigma de objetos son los mensajes y en la primera unidad pensamos mucho en eso: mensajes, polimorfismo, tipos. Ahora vamos a ver las diferentes formas de definir lo que hay del otro lado de los mensajes.

    Clases

    La forma tradicional de ponerle comportamiento a un objeto son las clases. Una clase tiene dos objetivos:
    • Es una unidad de creación, sirve como molde para crear objetos. Cuando yo quiero crear un objeto lo hago (en la versión más simple) a partir de su clase. Para poder crear objetos entonces, estas clases tienen que ser "completas". No pueden definir un objeto a medias.
    • Es una unidad de reuso; la clasificación, la subclasificación y la herencia son las formas tradicionales de evitar la repetición de código.
    Vamos a analizar la forma de reutilizar código en objetos. Para ello podemos ver un ejemplo muy fácil, con animales. Si tenemos las clases Perro y Humano, ambas podrían ser subclases de Mamífero, que a su vez es subclase de Animal. En otra rama podemos tener Cocodrilo, que es subclase de Reptil y también depende de Animal. Si además queremos tener la característica "bípedo" o "cuadrúpedo" se complica modelarlo con herencia simple, ya que tenemos mamíferos bípedos y cuadrúpedos, y lo mismo podría pasar con los reptiles.
    En un lenguaje con herencia simple estoy obligado a elegir una única entre dos posibles jerarquías. Para el código asociado a la otra jerarquía no tengo una herramienta de reutilización.

    ¿Qué formas tenemos de solucionar este problema?
    • La opción que parece más lógica, es la herencia múltiple, presente en lenguajes como C++ o Eiffel. Esto me permite tener más de una superclase y entonces reutilizar código proveniente de diferentes lugares. El problema de la herencia múltiple es que resulta en modelos a veces complejos, propio de las posibles colisiones entre métodos heredados de más de los múltiples padres.
    • Una forma de solucionar esto es la delegación, sin embargo es un esquema muy manual y resulta a veces pesado hacerlo de esta manera.
    • Un problema de la herencia simple es el evil root.
    • Las interfaces de lenguajes como Java nos permiten definir polimorfismos independientes de la herencia, pero no nos permiten asociar código con ellas.

    La raiz del problema es que estamos utilizando las clases para dos cosas. Como unidad de clasificación y creación por un lado; y como unidad de reuso del otro. Entonces surgieron alternativas a la clasificación y a la herencia que nos dan otros mecanismos de reutilización.

    Porque como dijimos, una clase para poder crear instancias debe definir un objeto "completo". En cambio muchas veces lo que necesitamos es poder reutilizar un "pedazo de código" o un comportamiento específico, que define cierta característica de un objeto, pero que no lo define completamente. Por ejemplo: Observable, Comparable, etc.

    Mixins

    Los mixins tienen todas las características de una clase, sin definir una superclase, por eso a veces se los menciona como subclases abstractas. Son "pedazos de clases", pueden tener atributos y métodos, pero no tienen constructores ni superclase.

    Al agregar un mixin en una clase, este pasa a formar parte de la cadena de resolución de métodos, es decir forma parte de la cadena de herencia. Al hacer esto proveen una forma de resolución de los conflictos implícita, que depende del orden en el que se agregan los mixins a la clase en cuestión.
    Esto es verdaderamente una limitación ya que la clase no tiene control sobre la composición de los mixins.
    Y por ejemplo combinado con la herencia, puede traer varios problemas (por ej: si heredamos de una clase que ya tiene mixins, desde nuestra clase no podemos controlar o cambiar esa composición).

    Otro problema de los mixins es la posibilidad de tener estado, ya que los conflictos entre las variables pueden ser más complejos que los conflictos entre los métodos.

    Traits

    El concepto detrás de los traits es formar unidades de composición. En esta visión, una clase podría ser simplemente la composición de varios traits.  Y un trait, como unidad de reutilización, no requiere definir un objeto "completo".

    Para llevar esto a cabo, las diferencias fundamentales con los mixins son:
    • No pueden tener estado
    • Se componen "en tiempo de diseño", o sea los métodos definidos en el trait pasan a formar parte integral de la clase, en tiempo de ejecución son indistinguibles los métodos del trait de los métodos de la clase. No existen en tiempo de ejecución.
    • Los conflictos se resuelven explícitamente.

    ¿Qué puede hacer un trait?
    • Proveer un conjunto de métodos que implementan un comportamiento.
    • Requerir, que la clase (u otro trait) entienda mensajes que el trait envía.
    • Componer otros traits.

    Desde esta visión, una clase consiste de:
    • Superclase
    • Estado
    • Traits
    • Métodos
    La versión más radical dice "glue methods", es decir, únicamente métodos que juntan todo lo demás, el comportamiento no está en los métodos sino en los mixins y los métodos simplemente coordinan los diferentes traits.

    Ejemplos

    Finalizada la parte teórica pasamos a ver algunos ejemplos en Pharo y Scala.

    Animales

    Modelamos Animales que subclasivicamos en Mamíferos, Aves y Reptiles. Encontramos que algunas de sus características no podían ponerse en una jerarquía de herencia y las modelamos como traits:
    • forma de reproducción (vivíparos, ovíparos)
    • su forma de desplazarse (bípedos, cuadrúpedos, acuáticos).

    En este ejemplo pudimos ver:
    • Que los traits nos permitían modelar características independientemente de la jerarquía de clases, que luego podemos asociar a todas las clases que quisiéramos. Tener traits nos agrega una nueva forma de abstraer y de conceptualizar, que se agrega a las clases, la herencia, etc.
    • Que nos permitían entonces reutilizar el código, a partir de ponerle el mismo trait en todas las clases que compartieran esa característica. Por ejemplo TAcuático lo podemos asociar a las clases Delfin, Cocodrilo y Pato, que están en diferentes lugares de la jerarquía.
    • Que podemos componer nuestras clases asociándole muchos mixins, por ejemplo a la clase Pato le asociamos los traits TBipedo y TAcuatico.
    • Que la composición se realiza en tiempo de diseño. Esto quiere decir que una vez asociado un trait a una clase, los métodos que estaban en el trait son indistinguibles de los métodos de la clase, en tiempo de ejecución los traits no existen.
    Y luego vimos algunas reglas para definir cómo se componen los métodos en caso de conflictos:
    • En la clase puedo sobreescribir los métodos que me vienen de los traits.
    • Los métodos en los traits tienen preponderancia ("pisan") sobre los de las superclases
    • Si tengo el mismo mensaje en dos traits se produce un conflicto que hay que resolver sí o sí.
    • Puedo cancelar métodos: - {#caminar}
    • O agregar un alias: @{#correr -> #caminar}

    Algo a tener en cuenta: si bien el nombre que se le da en Scala es "trait", se parece más a lo que en la teoría llamamos "mixin".

    Alquileres
    Teníamos muchas cosas que se podían alquilar y no están en la misma jerarquía: Bicicleta, Auto, DVD, etc. Entonces pusimos el comportamiento en un trait TAlquilable que define el comportamiento necesario para alquilar un objeto cualquiera.

    El trait define el comportamiento alquilarA: unArrendatario, que se implementa:
    alquilarA: unArrendatario
        self estaAlquilado
           ifFalse: [
               unArrendatario debitar: self precio.
               self arrendatario: unArrendatario ]
           ifTrue: [ self error: 'Ya está alquilado' ]

    estaAlquilado
        ^self arrendatario isNil

    La dificultad nueva que aparece con este trait, es que este necesita almacenar en algún lado a quién le alquiló el objeto (el arrendatario) y como dijimos los traits no pueden definir estado. Entonces ese estado deberá estar en la clase a la que se agrega el trait.

    ¿Cómo se logra eso? Verán que el trait envía tres mensajes que no están definidos en el trait: #arrendatario, #arrendatario: y #precio. Decimos que el trait requiere estos mensajes, esto quiere decir que el contexto en el cual agreguemos este trait deberá proveer implementaciones para esos mensajes.

    Este concepto se puede asociar a la idea de un método abstracto. En Pharo se los suele definir como #required:
    arrendatario
        self required

    arrendatario:
        self required

    precio
        self required


    A continuación implementamos el mismo código en Scala. La principal diferencia que notamos es que los traits/mixins de Scala sí permiten tener estado, entonces el trait puede ya acordarse del arrendatario sin requerir nada del objeto.

    Otra diferencia (sutil) es que acá el chequeo de tipos va a verificar que la clase a la que le agreguemos el mixin va a chequear tipos y no va a compilar si no cumplimos el requerimiento (#precioAlquiler).

    El código del mixin nos queda:
    trait Alquilable {
        var inquilino : Inquilino = null
       
        // restriccion sobre la clase en que se va a aplicar
        def precioAlquiler : Int

        def alquilar (inquilino : Inquilino) = {
            if (!(this disponible)) {
                error("Este objeto '" + this + "' ya esta alquilado a: " + this.inquilino)
            }
            inquilino debitar(this precioAlquiler)
            this inquilino = inquilino
        }
       
        def disponible = this.inquilino == null    
    }

    Finalmente la diferencia en Scala es que los mixins se pueden agregar tanto a las clases como a los objetos. Agregarlo a las clases es parecido a lo que hacemos en Pharo:
    class Automovil (marca : String, modelo : String) extends Vehiculo with Alquilable {
        def precioAlquiler : Int = 25
    }

    Pero también lo podemos asociar a los objetos:
    var miBicicleta = new Bicicleta with Alquilable

    Múltiples mixins

    En el último ejemplo definimos múltiples mixins sobre la clase Ball y eso nos permitió ver cómo al componerlos de diferente manera cambiaba el comportamiento. Esto nos muestra que los mixins se meten en la jerarquía de herencia, a diferencia de los traits.

    Por lo tanto las dos líneas siguientes producen comportamientos distintos:
     
       val myBall = new Ball with Shiny with Red
       val anotherBall = new Ball with Red with Shiny

    Para que esto funcione tenemos que ver cómo hace un mixin para llamar al método que está sobreescribiendo (super):
    trait Red extends Ball {
        override def properties() = super.properties ::: List("red")   
    }


    La diferencia concreta es:
    • Entre dos traits con el mismo mensaje se produce un conflicto, que hay que resolver a mano.
    • Entre dos mixins con el mismo mensaje, uno sobreescribe al otro y el comportamiento depende del orden en que se agreguen.

    Si en un mixin usamos super, eso va a llamar al siguiente mixin en la cadena de delegación, en cambio si en un trait hacemos super va a buscar en los métodos de la superclase, ignorando otros traits en esta clase.



    Clase 3 - 29/03/2011

    posted Mar 29, 2011, 4:13 PM by Esteban Lorenzano   [ updated Mar 30, 2011, 4:52 AM by Nicolas Passerini ]

    En esta clase continuamos con algunos puntos que nos habían quedado de las clases anteriores.

    Duck Typing

    En la primer parte de la clase, continuamos el tema de Sistemas de Tipos. Específicamente vimos la idea de duck typing en un lenguaje con checkeos en tiempos de compilación, como scala.

    Scala es un lenguaje que vamos a usar para varios ejemplos, que se compila y se ejecuta sobre la VM de Java. Tiene varios features interesantes que vamos a ir viendo para diferentes conceptos de la materia, pero hoy nos interesa el Duck Typing.

    Vimos ducktyping en general, a nivel de métodos, definiendo un mensaje que espera recibir un parámetro definido por "entiendo el mensaje 'quack' y 'feathers'".

    def inTheForest( duck : { def quack ; def feathers }) =  ...

    Esta es una definición de tipos "alternativa" a la tradicional que usamos generalmente en lenguajes como java. Donde declaramos los tipos de las variables a través de un nombre de tipo (de clase, interface, enum, annotation, etc... -uff cada versión de java se agregan más cosas ;)). Y donde se asocian los tipos entonces, directamente a las clases.

    Esta forma tradicional se llama nominal. Recordamos que en java, otra forma de salvar el polimorfismo es mediante las interfaces. Esto sigue siendo, igualmente nominal.

    En cambio duck typing es básicamente una forma de declarar un tipo a través de definir la estructura (los mensajes que debe entender). Esta otra forma se denomina estructural.

    Como segundo ejemplo vimos la implementación del ejemplo de código de los animales que habiamos utilizado la clase pasada, tanto en java como en smalltalk, pero en este caso con duck typing.
    Vimos que scala también tiene generics en sus colecciones, y que los ducktypings también se pueden usar en esos generics.

    Como conclusión vimos que este feature no es exclusivo de lenguajes "dinámicos" como smalltalk, o sin checkeos, y que por el contrario pueden convivir en un lenguaje fuertemente tipado, o mejor dicho con checkeos en tiempo de compilación, estáticos.
    Entonces encontramos dos diferencias entre el tipado estructural de Scala y lo que teníamos en Smalltalk:
    • Es explícito
    • Se chequea en tiempo de compilación.

    Lo interesante de la intersección de estos dos features (checkeos + ducktyping), es que nos provee el nivel de abstracción y desacoplamiento de las clases (de sus nombres), y por lo tanto la flexibilidad, pero al mismo tiempo, seguir teniendo los checkeos y la validación del programa en tiempo de compilación.

    Finalmente, comentamos la idea de que este tipo de concepto, feature y/o herramienta, como varias otras que vamos a ver durante la materia, son en definitiva, todavía inmaduras, o novedosas, en el sentido de que aún no es claro su impacto o el resultado de experiencias a nivel masivo, o en el desarrollo de grandes sistemas de la industria.
    Esperemos ver a futuro como evoluciona esto, o aún mejor, si es posible hagamos nuestra propia experiencia probando, experimentando y analizando los resultados de nuestros propios sistemas utilizandolos :)


    TDD

    Como segunda parte de la clase vimos el concepto de Test-Driven Development.
    Que es básicamente una práctica que viene del mundo smalltalk, y bastante relacionada con las buenas prácticas de XP (extremme programming), que se refiere a la idea de:
    • comenzar a implementar un nuevo requerimiento (user story, feature, etc.) a través de escribir el testcase (definición de lo que debería hacer el sistema)
    • tratar de llegar al verde lo más rápido posible. Porque significa que ya tengo mi requerimiento implementado y funcionando de acuerdo a lo esperado.
    • en el paso anterior lo importante es tener algo "andando", el famoso "make it work". Una vez verde, ya nos pondremos a diseñar, y a modificar la implementación, para que se ajuste a nuestras buenas prácticas de diseño. Y esta es la segunda parte de la frase "make it right". Acá entra la idea de refactoring.
    • Y luego el ciclo se repite.
    Luego de las diapositivas, vimos un ejemplo práctico de TDD en smalltalk.

    Otra cosa interesante de hacer los testcases primero es que, en primera instancia usamos nuestras clases antes de implementarlas, entonces nos ayuda a concentrarnos en la interfaz, y no en la implementación de nuestras clases.

    El punto de incluir TDD en esta materia, no era explicar esta práctica completamente, sino ver cómo se implementa en un lenguaje dinámico como smalltalk, donde uno puede ir construyendo desde el test, y a medida que se va ejecutando, ir creando y completando la aplicación. En otros lenguajes con checkeos nos va forzando a escribir mucho más código inicialmente, antes de correr el test.

    Los slides de TDD se pueden descargar de acá: Unidad 1 - Esquemas de tipado

    Bloques y Colecciones

    Vimos un ejemplo simple en smalltalk utilizando el mensaje "collect"

    #(1 2 3 4 5).collect: [n | n^2] 

    Este mensaje devuelve una nueva collection que contiene cada elemento de la original "transformado" utilizando el bloque enviado como parámetro.

    Vimos que Java no tiene bloques, pero lo más parecido que podemos hacer son clases anónimas.
    Y que existe una librería útil, del proyecto jakarta de apache, llamada collection-utils que "imita" estos features.

    Aunque, no son mensajes a la lista, sino un método static, como una función.

    Un bloque, se parece a una función.

    Luego comparamos un poco los ejemplos de collect:
    • en smalltalk
    • en java con jakarta collection-utils
    • en java con otra implementación de collection-utils + generics
    Y en este caso en java vimos que empiezan a aparecer "problemas" o incomodidades con el hecho de tener tipos explícitos (no tener inferencia), en las variables, en la declaración de la clase anónima, en la declaración de los generics, etc.
    Esto incrementa la cantidad de código considerablemente, y por ende, reduce la claridad.

    Vimos que la idea de closure se refiere a la capacidad de los bloques de hacer referencia a un scope más amplio que él mismo, y por ejemplo hacer referencias a las variables disponibles en el scope del método donde lo crearon. Y no de quien lo está ejecutando (recuerden que los bloques se pasan como cualquier otro objeto, como parámetro).

    Clase 2 - 22/03/2010

    posted Mar 22, 2011, 8:23 PM by Nicolas Passerini   [ updated Mar 22, 2011, 8:37 PM ]

    Sistemas de Tipos

    ¿Qué es un tipo?

    • conjunto de valores o "elementos de ese conjunto"
    • conjunto de operaciones que puedo realizar sobre ellos
    Ej: Números: 1, 2, pi,

    Y en objetos ?
    La primer idea es matchear un tipo con una clase.
    Pero no es la única idea de tipos en OOP. Porque un objeto puede implementar varios tipos.

    Sistema de tipos en Java:

    Tipo las variables. Ej: Perro perro
    Me agrega restricciones. Por ejemplo, Perro perro = new ArrayList<Object>();

    Establece un contrato entre
    • el que asigna la variable.
    • el que luego la usa.
    ¿Con qué objeto?
    1. Detectar errores
    2. Como documentación o guía
    3. En función del tipo, en algunos casos, puede cambiar el comportamiento (polimorfismo).

    Tipado-No Tipado, Débilmente/Fuertemente Tipado, Estático/Dinámico

    Categorizaciones de lenguajes, ambiguas, o parciales.

    Cuál es son las categorizaciones más precisas entonces ??
    • Checkeo
      • Dinámico: no es que no hace checkeos, los hace al momento de ejecutar!
      • Estático: se hace en tiempo de compilación.
    • Tipado & polimorfismo
      • Explícito: en java tienen que tener un tipo en común
      • Implicito: en smalltalks solo deben entender el mismo mensaje (en runtime)
        • Existen lenguajes implícitos pero igualmente CON checkeos: Haskell, Scala, etc.
    • Definiciones de Tipos:
      • Nominales: tienen que ser explícitos
      • Estructurales pueden explícitos o implícitos.
    Ejemplo:
    Puedo tener checkeos estructurales en scala (duck typing)

    Ahora definimos los lenguajes según estos criterios:
    • Java: Estático, Explícito y Nominal.
    • Smalltalk: Dinamico, Implicito y Nominal.
    • Haskell: Estático, Implícito (inferencia de tipos)

    Binding/Dispatch

    Es la relación que se establece entre el envío de un mensaje y el momento en que se ejecuta. Nos interesa diferenciar en qué momento se produce, si se produce en tiempo de compilación lo llamamos early binding, si se produce en tiempo de ejecución lo llamamos late binding.
    • Mismo mensaje a diferentes objetos
      • late
      • dinámico
      • polimórfico
      • method look-up, en cuanto al RECEPTOR
    • Mensaje: mismo nombre, pero diferente parámetro
      • Estático
      • Sobrecarga
      • NO polimorfismo
      • NO method  look-up (porque no se hace en cuanto al PARAMETRO)
    • Clases
      • En java los métodos de clase no tienen binding dinámicos (static)
      • En smalltalk las clases son objetos y sus métodos son polimórficos.

    Tareas:

    • Pharo by example
    • Comparar collection utils de java con smalltalk
    • En la página de la Unidad 1 hay algunos links con material para leer.

    Clase 1 - 15/3/2011

    posted Mar 22, 2011, 8:21 PM by Nicolas Passerini   [ updated Mar 22, 2011, 8:27 PM ]

    Arrancamos 7:25 (gracias a un pequeño inconveniente con el hecho de que competimos con 2 otras materas :), viendo una intro a la materia. Definimos que ibamos a ver "herramientas", pero cognitivas, conceptos, que son los que perduran.

    Hicimos referencia a este google site.

    Definimos que la forma de evaluación de la materia es puramente basada en las prácticas, con 3 Trabajos Prácticos:
    1. Traits en smalltalk, con fecha de entrega 26/4
    2. Reflection y Metaprogramación en java y en smalltalk, con fecha 24/5
    3. TP a definir, con fecha 21/6
    Les contamos que vamos a trabajar con la Máquina Virtual de la materia que ya contiene todos entornos para trabajar sin perder tiempo en complejidad accidental de instalaciones y configuraciones.

    Dijimos que en principio vamos a estar trabajando los martes. Los jueves los trataremos "on-demand", para despejar dudas, o si hace falta, para recuperar algún tema si nos atrasamos. La planificación de la materia está en esta página.

    Mencionamos las unidades de la materia, que se encuentran en definidas en el Temario, y dimos una breve descripción de cada una.

    Luego de toda esta introducción entramos ya en la primer unidad a las 7:40.

    Arrancó Esteban contando, como introducción, los orígenes de la OOP, con simula y smalltalk. El concepto de encapsulamiento, que nos da capacidad de abstracción, con lo cual podemos separar los problemas en unidades "no tan" dependientes que permiten mayor flexibilidad ante cambios.

    Vimos que las clases son "una" forma de compartir código, pero no necesariamente la única. A lo largo de la cursada de la materia, justamente vamos a ver qué limitaciones tiene, y qué otras formas existen para modelar esto, y salvar las limitaciones.

    Hicimos hincapié en la importancia de los mensajes en el paradigma, y no tanto en las clases. Repasamos que el ambiente es básicamente un conjunto de objetos interactuando a través de mensajes.

    Luego, a las 8 arrancamos con Smalltalk.

    Vimos que básicamente todo es un objeto, y que existe el concepto de "ambiente de ejecución", como una especie de mundo donde viven nuestros objetos todo el tiempo, que se persiste (perdura a lo largo del tiempo, luego de "cerrar" y volver a abrir el pharo).
    Dimos mucha importancia a esta idea de ambiente "vivo", que nos permite inspeccionar en cualquier momento el estado de los objetos, e interactuar con ellos.
    Esto es importante, porque rompe la idea de fases o ciclo de desarrollo tradicionales que existe en lenguajes que provienen de la rama de ALGOL, como C, C++, java, etc, donde existe las fases de: escritura de código, compilación, empaquetado y ejecución.

    Esto convierte al programador en un usuario más del ambiente, en la misma forma en que el usuario final también interactúa con los objetos (indirectamente a través de una UI, etc.).

    Después arrancamos ya a jugar con Pharo.
    Vimos los diferentes browsers, que sirven para inspeccionar el ambiente: class browser, hierarchy browser, implementors, etc. Y evaluamos un poquito de código.

    Luego, 8:20 nos metimos completamente a ver una descripción del lenguaje.

    Vimos que tiene tipos, pero no así sus variables. Es decir que no se hacen checkeos de tipos, como hace por ejemplo el compilador de java. Entonces, los mensajes se evalúan dinámicamente, realmente se intenta enviar el mensaje, en tiempo de ejecución, y ahí se verá si el objeto lo entiende o no. De hecho, en caso de no entenderlo, tiene un mensaje propio llamado "doesNotUnderstand" que también se podría redefinir ! :)
    Esto se llama late binding y vamos a profundizarlo en la siguiente clase.

    Contamos que al no existir tipado de variables ni checkeos, se tiene polimorfismo, sin necesidad de interfaces (en contraposición con java). Cuando dos objetos entienden el mismo mensaje (en tiempo de ejecución), se dice que son polimórficos, y ya.

    Vimos que el objetivo de la sintaxis es de ser simple, y natural.

    Smalltalk tiene interface ricas con muchísimos mensajes a diferencia de lenguajes como java.
    Vimos que en smalltalk no hay estructuras de control, todo es un mensaje. Que "true" es un objeto boolean que entiende el mensaje "if". Los operadores como mayor ">", son mensajes, que retornan el objeto "true" o "false" (únicas instancias en el sistema).

    Vimos que hay diferentes tipos de operadores:
    • unarios: mensaje sin parámetro
    • binarios: mensaje (aritmético o comparativo) + parámetro
    • keywords: receptor + varios parámetros (con nombre). Ej: unaCollection at: 3 put: elemento.
    Jugamos con la precedencia de los operadores (mensajes en realidad). Vimos que NO hay precedencia matemática, porque todo es un mensaje!
    La solución es utilizar paréntesis para hacer explícita esa precedencia.
    Los operadores unarios tienen precedencia sobre los operadores binarios.

    Vimos que los bloques, son en realidad objetos! Se pueden manipular como cualquier otro objeto. Por ejemplo, se pueden pasar por parámetro, para ejecutarse luego.

    Ya casi al final, vimos que las colecciones, obviamente también son objetos! Y las vinculamos con los bloques, ya que tienen muchos mensajes que se basan en el uso de estos, para operar sobre ellas. Esto nos da un gran poder en el manejo de colecciones y un alto nivel de declaratividad (que vamos a profundizar en la última unidad).
    Básicamente el lenguaje no tiene sentencias de control, como for, while, do en java. En su lugar, esto se logra con mensajes y bloques en las colecciones.

    Y por último vimos un sintax sugar llamado "cascadeo de mensajes", que permite enviar sucesivos mensajes a un mismo receptor. A través del caracter "punto y coma" (;). Vimos el mensaje "yourSelf" que permite terminar las cascadas devolviendo el receptor.

    Finalmente nos fuimos antes de que nos echaran o nos dejaran dentro de la facultad hasta el otro día :)

    Presentación

    Se puede bajar la presentación que se usó aquí: 

    Referencias

    Algunos referencias que hicimos:
    • Alan Kay: quien inventó el concepto de OOP.
    • Dan Ingalls: ingeniero que trabajó junto a Alan Kay en Smalltalk.
    • Simula 68: primer lenguaje con un tinte objetoso.
    • Smalltalk 72, 78 y 80

    Tareas

    • Hacer andar la VM
    • Jugar un poco con el Pharo
    • (para dentro de 15 días) jugar con las colecciones de ST y comparar con CollectionUtils.

    1-6 of 6