Clase del 04/09/2007 (Diseño Avanzado con Objetos)
SELF: The Power of Simplicity
“SELF es un lenguaje orientado a objetos para programación exploratoria basado en un número pequeño de ideas concretas y simples: prototipos, slots y comportamiento”. Self es un ambiente compuesto exclusivamente por objetos, a su vez conformados por slots. Un slot tiene un nombre, siempre string, y un valor que puede ser cualquier objeto. Un slot puede designar a un parent, lo cual suele indicarse sintácticamente con un asterisco.
Cuando se envía un mensaje a un objeto, se busca dentro de sus slots uno cuyo nombre coincida con el del mensaje. Si no se encuentra ningún slot en el objeto receptor, la búsqueda continúa en los objetos referenciados por slots marcados como parent, y así recursivamente. La evaluación de un slot encontrado en un objeto diferente al receptor del mensaje se realiza, sin embargo, en el contexto del receptor. Esto quiere decir que cualquier mensaje enviado al nombre self disparará la búsqueda del slot nuevamente en el objeto receptor del mensaje original. Este comportamiento corresponde a la delegación. Esta es la forma mediante la que SELF implementa herencia.
Además de slots de “datos”, un objeto Self puede incluir código. Tales objetos son llamados methods, dado que hacen lo que los métodos en otros lenguajes. Sin embargo, en Self cualquier objeto puede ser considerado método. Este punto de vista sirve para unificar la computación y el acceso a datos: cuando un objeto es encontrado en un slot, como resultado del envío de un mensaje, éste se ejecuta; un dato se devuelve a sí mismo, mientras que un método invoca a su código. Esto refuerza la interpretación de que lo que importa es la experiencia del emisor, no los detalles internos del objeto receptor. La unificación sirve para proveer abstracción de datos, puesto que es imposible distinguir, aun dentro de la abstracción, si un valor está almacenado o es computado. En Self, también se unifica la asignación, la cual es llevada adelante por un slot (como x: o y: en la figura) que contiene un método especial simbolizado por una flecha. Este slot requiere un argumento y se encarga de asignarlo como valor al slot correspondiente (en nuestro ejemplo x o y).
Otra característica de Self es que la creación de nuevos objetos se efectúa simplemente copiando otros. No existen objetos “especiales”, como las clases, para la instanciación. Self enaltece lo concreto colocándolo, de alguna manera, en una posición de mayor relevancia frente a la abstracción.
En SELF la operación más fundamental y básica es el envío de mensajes. En SELF no hay variables, solamente hay slots conteniendo objetos que se devuelven a sí mismos. Mientras Smalltalk y la mayoría del resto de los lenguajes orientados a objetos dan soporte al acceso a variables, además del envío de mensajes, los objetos SELF acceden a su información de estado enviando mensajes a “self”, el receptor del actual mensaje. Naturalmente esto resulta en muchos mensajes enviados a “self”, y el nombre del lenguaje hace honor a éstos.
Acceder al estado por medio de mensajes simplifica el sharing del mismo. La inclusión de variables dificulta la especialización (por parte de una subclase) el reemplazo de una variable por un resultado calculado, dado que puede haber código en la superclase que accede directamente a la variable.
El clonado suena a una forma de creación más simple. Es sencillamente copiar, no interpretar un plan maestro abstracto. Además obedece al gusto de los creadores de SELF por la concretitud.
Para entender como se comporta un objeto en los lenguajes basados en clases uno debe entender dos tipos de relación: la relación “es un” o relación de instancia, y la relación de subclase. En un sistema como SELF, con prototipos en lugar de clases, solamente hay una relación: “hereda de”. Esta simplificación estructural hace más fácil entender el lenguaje y formular una jerarquía de herencia.
En self, debido a la no existencia de clases, no existe el problema de la metaregresión. Cualquier objeto puede cumplier el rol de instancia o servir de repositorio de información compartida.
USING PROTOTYPICAL OBJECTS TO IMPLEMENT SHARED BEHAVIOR IN OBJECT ORIENTED SYSTEMS (LIEBERMAN)
Conjuntos o prototipos
Los filósofos que estudian la teoría del conocimiento tienen un debate sobre cómo representa la gente el conocimiento adquirido por generalización a partir de experiencias concretas. La controversia tradicional entre la representación de conceptos como conjuntos y la representación de conceptos como prototipos da lugar a dos mecanismos de sharing, herencia y delegación.
Cuando una persona tiene una experiencia particular, por ejemplo, conoce a un elefante llamado Clyde, lo que sabe acerca de Clyde es útil cuando se encuentra con otro elefante (supongamos uno llamado Fred). Hay dos puntos de vista que podemos adoptar. El primero es la idea de un Conjunto Abstracto. A partir de lo que aprendemos de Clyde, podemos construir el concepto del conjunto (o clase) de todos los elefantes. Esto abstrae lo que creemos que es verdadero acerca de todo individuo que se parezca lo suficiente a Clyde como para que lo consideremos un elefante (las características esenciales). Ahora, Clyde sería un miembro (instancia) de ese conjunto. Si consideramos a Fred otro miembro, estaría compartiendo nuestro conocimiento sobre elefantes a través de la clase. Otro punto de vista alternativo es considerar que Clyde representa un elefante prototípico. Si alguien les dice: "piensen en un elefante", seguramente les viene la imagen de un elefante particular, y no una esencia. Ahora, si alguien pregunta algo acerca del elefante, es fácil responder con la información del elefante prototipo, a menos que haya una buena razón para no hacerlo. El concepto de Fred podría tener una relación con Clyde, indicando que ése es su prototipo, y este sería el mecanismo usado para compartir información entre ellos. La descripción de Fred puede almacenar cualquier información que sea propia (distinta de la de Clyde), y siempre se revisa antes de contestar con la del prototipo.
Ventajas de los prototipos para el aprendizaje incremental de conceptos
El enfoque de los prototipos corresponde mejor a la forma en que la gente parece adquirir los conocimientos a partir de situaciones concretas. El problema de los conjuntos abstractos es su abstracción. La gente parece responder mejor cuando enfrenta ejemplos específicos primero y generaliza después, que cuando trata de absorber principios generales abstractos primero y aplicarlos a casos concretos después.
La herencia implementa los conjuntos, la delegación los prototipos
El mecanismo de clases es el que ya conocemos. La delegación remueve la distinción entre clases e instancias. Cualquier objeto puede servir como prototipo. Para crear un objeto que comparta la información de un prototipo, se hace un objeto de extensión, que tiene una lista que indica sus prototipos, y una conducta personal y propia de este objeto. Cuando un objeto de extensión recibe un mensaje, primero intenta responderlo con la información que guarda en su parte personal. Si no, reenvía el mensaje a sus prototipos, a ver si alguno puede responderlo; a esto se le llama delegación del mensaje.
Las herramientas para representar comportamiento y estado interno son los ladrillos de los sistemas orientados a objetos
Cada sistema orientado a objetos debe proveer mecanismos lingüísticos para definir el comportamiento de los objetos. En lugar de definir de una sola vez la conducta procedural o los datos contenidos en un objeto, es conveniente dividirlos en conjuntos de partes más pequeñas, que se puedan modificar por separado. Así surgen las variables y los métodos. Los lenguajes deben, entonces, proveernos de formas de combinar grupos de métodos y variables para armar objetos, y medios que permitan que un objeto comparta métodos y variables de otros previamente definidos. Los mecanismos para compartir conocimiento en los lenguajes orientados a objetos se han vuelto tan complejos que es imposible alcanzar un consenso universal sobre cuál es el mejor. Pero el uso de los objetos para implementar estos mismos mecanismos nos permite experimentar con ellos, y hacer coexistir y competir distintos formalismos.
Diferencias entre delegación y herencia
Cuando un mensaje es delegado, se debe pasar el objeto que originalmente recibió el mensaje, ya que es en su contexto que debe resolverse. Esto se llama self en Smalltalk y otros, aunque es un término confuso, ya que no es el self de donde se define el método. Cuando un objeto delega en otro un mensaje, le está diciendo: "No sé cómo responder a este mensaje, así que te pido que lo contestes por mí (si podés). Pero si necesitás otras cosas para responderlo, como el valor de una variable o el resultado de algún otro mensaje, volvé y pedímelas a mí." No importa si el mensaje se sigue delegando, todos los pedidos de valores de variables o nuevos envíos de mensaje van a parar nuevamente al receptor original. Con la herencia, es necesario primero crear una clase para tener un objeto, después recurrir al mecanismo de instanciación. En vez de un objeto, tenemos dos, y un mecanismo adicional. Se deben especificar los valores de todas las variables de instancia, aunque no sean particulares para esta instancia. No se puede darle conducta propia a una instancia individual, para extender el comportamiento necesitamos una operación nueva, la de subclasificación. El paso desde la instancia a su conducta (almacenada en la clase) está puesto en un mecanismo de method lookup que es interno de la máquina virtual. Cuando una instancia necesita responder a un mensaje, el method lookup no puede pasar como parámetro el objeto receptor, como hacía la delegación. En lugar de eso, usa el binding de la pseudo variable self. Hablando del binding, en general en los métodos se puede referenciar directamente a las variables de instancia del objeto, sin usar el mecanismo de mensajes, introduciendo una nueva diferencia lingüística.
¿La herencia y la delegación tienen el mismo poder expresivo?
La respuesta (de Lieberman) es que no. Es fácil ver cómo implementar la herencia con delegación. Habría que crear un objeto clase por cada clase, que responda al mensaje #new creando una instancia según su especificación, y copiándole las variables de instancia indicadas por su cadena de superclases. La conducta de los objetos instancia debería implementar el equivalente del method lookup y de la búsqueda y binding de variables. En cambio, no se puede implementar la verdadera delegación con instancias y clases. El motivo es el tratamiento de la pseudo variable self. Por la forma en que está hecho, el self se mantiene ligado al objeto receptor mientras se hace la búsqueda del método remontando las superclases. Pero ante un nuevo envío de mensaje, se vuelve a ligar al objeto receptor, lo que hace que generalmente no se pueda diseñar un objeto que responda en lugar del receptor original del mensaje.
¿Y la eficiencia?
La comparación de eficiencia entre herencia y delegación se transporta a la de balances tiempo/espacio. La herencia requiere menos envíos de mensajes, pero al costo de tener objetos más grandes. El tener objetos más pequeños mejora el tiempo de creación de los objetos y la eficiencia de la memoria virtual y el garbage collection. A primera vista, el tiempo de búsqueda de los métodos en delegación puede ser más largo, pero se pueden usar con gran éxito caches para solucionarlo. No hay ninguna razón de eficiencia importante para no usar la delegación.
La delegación es más flexible para combinar comportamiento de varias fuentes
A veces se quiere usar en un objeto habilidades que existen en varios otros. Una solución es herencia múltiple. Además de los problemas que ya vimos, la herencia múltiple fija los patrones de comunicación entre los objetos antes de que las instancias se creen. Esto limita la posibilidad de usar dinámicamente el comportamiento de otros objetos. Con delegación, los patrones de comunicación se pueden determinar en el momento en que se reciba el mensaje. Otro problema de los esquemas de herencia son las clases con una sola instancia. A veces es necesario crear una clase sólo para poder combinar adecuadamente un grupo de funcionalidades.
La delegación es ventajosa para el desarrollo de software incremental y altamente interactivo.
Si un objeto prototípico cambia su conducta, inmediatamente todos los que lo tienen como prototipo sienten el efecto del cambio. Por otro lado, si modificamos las variables de instancia en una jerarquía de herencia, la información copiada en las viejas estructuras de las instancias de esas clases queda obsoleta y es necesario actualizarlas. La delegación ha sido un punto de vista minoritario dentro de los lenguajes orientados a objetos, en parte por razones históricas. Pero la discusión precedente debería convencerlos de que modelar conceptos usando prototipos y delegación tiene ciertas ventajas con respecto al uso de clases y herencia.
EL TRATADO DE ORLANDO (STEIN, LIEBERMAN, UNGAR)
SHARING ANTICIPADO VERSUS NO ANTICIPADO
Distinguimos dos tipos de motivación para introducir el sharing en un sistema de objetos. Una es el sharing anticipado: durante la fase conceptual de diseño, un diseñador puede anticipar propiedades en común entre diferentes partes del sistema, lo que lo lleva a desear compartir datos y procedimientos entre esas partes. Esto se logra mejor cuando hay mecanismos de lenguaje para escribir las estructuras por anticipado (por ejemplo, las clases). Por otra parte, el sharing no anticipado está menos favorecido por los mecanismos tradicionales de herencia. Surge cuando un diseñador quiere agregar funcionalidad a un objeto, y esto no fue anticipado en la programación original. El diseñador puede notar que la nueva funcionalidad se puede conseguir, en parte, usando componentes que ya existen, pero habría que agregar o modificar datos y procedimientos para ello. Así, surgiría una nueva relación de sharing entre los componentes que se usan para su propósito original y los que se usan para esta nueva funcionalidad. Obviamente, como la nueva funcionalidad no fue prevista, el hecho de establecer las relaciones de sharing por anticipado restringe los tipos de funcionalidad que se pueden agregar sin modificar el sistema. El mecanismo tradicional de clase/subclase/instancia requiere una distinción textual, estática, entre los elementos que van a representar las características comunes (las clases) y aquellos que se espera sean individuales, las instancias. Soportar el sharing no anticipado es importante, porque la evolución del software sigue caminos impredecibles. Un mecanismo soporta mejor el cambio no anticipado si se puede introducir nueva funcionalidad simplemente explicando en el sistema las diferencias entre el nuevo comportamiento y el existente. La delegación o la herencia dinámica logran esto al permitir que los nuevos objetos rehusen el comportamiento de los que ya existen sin necesidad de una especificación previa de su relación. Los resultados de Stein indican que, como una subclase es definida indicando las diferencias que tiene respecto de su superclase, la relación subclase/superclase es mejor para el cambio no anticipado que la relación clase/instancia.
MECANISMOS BASICOS
La discusión sobre qué es fundamental en la programación con objetos existió durante mucho tiempo. Aquí no se trata de resolver ese debate, sino de presentar dos mecanismos que se usan en la mayoría de los lenguajes de programación con objetos: empatía y templates. Son fundamentales, en el sentido de que ninguno de ellos puede definirse en términos del otro. La mayoría de los lenguajes de objetos pueden caracterizarse por la forma en que combinan estos mecanismos. En todos los lenguajes orientados a objetos hay alguna forma de que un objeto tome prestado un atributo (variable o método) de otro; eso es lo que llamamos empatía. Decimos que un objeto a es empático con otro objeto b para un mensaje m si a no tiene un método propio para responder a m, sino que cuando se le envía m responde como si hubiera tomado prestado el método de respuesta de b. a toma prestado sólo ese método, y ninguna otra cosa de b, o sea que si el método indica que se mande un mensaje a self, lo recibirá a. Todas las variantes de delegación y herencia incluyen alguna forma de empatía, pero difieren en cuándo y cómo se establece la relación. Puede ser explícita o por defecto, dinámica o estática, por objeto o por grupo. Estas elecciones de un lenguaje son una causa de la gran variedad de lenguajes orientados a objetos. Un template es una especie de molde de galletas para objetos: contiene todas las definiciones de métodos y variables necesarias para definir un nuevo objeto de un tipo determinado. Si el objeto no puede ganar ni perder atributos después, lo llamamos un template estricto. En varios lenguajes se permiten cambios al objeto después de que ha sido instanciado, debilitando las garantías de un comportamiento uniforme por grupos. En algunos lenguajes el template es un objeto común, mientras que en otros es un objeto generador distinto y especial, como la clase. También hay lenguajes que no tienen templates, y en estos no hay un concepto inherente de grupo o especie para los objetos. Tradicionalmente la relación clase/instancia es estricta: la clase lista estrictamente los atributos que cada objeto cortado a partir de su template puede definir, y ninguno de ellos puede definir otros. Como el template es estricto, cada objeto cortado a partir de él tiene una copia local de cada atributo, y los atributos no pueden ser agregados ni removidos, nunca son delegados. Esta es la forma en que una clase garantiza la uniformidad y la independencia de sus instancias. Esta relación se puede relajar de varias maneras. Por ejemplo, un template minimal es un molde de galletas como el estricto, pero en el que los objetos pueden definir otros atributos una vez creados. Una instancia extendida, creada a partir de un template minimal y a la que se le agregaron cosas, ya no tiene a priori un template para su tipo. Por otra parte, una instancia extendida se podría promover, transformándola a su vez en un template. Otras relajaciones en el respeto de los templates crean una variedad de templates no estrictos. En los lenguajes donde los templates son objetos comunes, es frecuente que sean completamente no estrictos.
SHARING Y EVOLUCION DEL SOFTWARE
Un tema de importancia central es qué tipo de extensiones podemos hacer a un sistema sin modificar el código previamente existente. Un principio importante es que una extensión conceptualmente pequeña al comportamiento de un sistema de objetos debería lograrse con una extensión pequeña del código. El análisis de los mecanismos de sharing alternativos debería considerar sus efectos en la necesidad de futuras modificaciones al código para lograr extensiones de comportamiento. Hay dos clases de cambios que hacemos sobre los sistemas de objetos: agregar nuevo código (extender el sistema) o editar el código existente (modificar el sistema). Se puede decir que el verdadero valor de las técnicas orientadas a objetos frente a las técnicas convencionales de programación no es que se puedan hacer cosas que no se pueden hacer con las convencionales, sino que frecuentemente se puede extender la funcionalidad agregando nuevo código en casos en los que, con técnicas convencionales, hubiera sido necesario modificar el código existente. Los objetos son buenos porque permiten agregar nuevos conceptos al sistema sin modificar el código existente. Los métodos son buenos porque permiten agregar funcionalidad a un sistema sin modificar el código existente. Las clases son buenas porque permiten usar el comportamiento de un objeto como parte de otro sin modificar el código existente.
La implementación del sharing no anticipado entre objetos en esquemas de herencia, frecuentemente fuerza a modificar el código existente, y por eso se dice que no lo soporta bien. La solución es permitir formas más dinámicas de empatía. Al mismo tiempo, no se puede minimizar la importancia de mecanismos de lenguaje para el sharing anticipado y tradicional, y creemos que los lenguajes futuros deberían buscar una síntesis de ambos. La búsqueda de formas de lograr extensiones interesantes sin modificación de código no se acaba ahí. Podemos mencionar un caso en que ningún lenguaje provee un mecanismo para lograrlo: hay ocasiones en que queremos hacer una extensión sobre una jerarquía completa, y no sobre un objeto (mencionar aspectos?).
CONCLUSION
Entre los beneficios prometidos por la tecnología de objetos están el dinamismo y la flexibilidad, pero la gran mayoría de los lenguajes orientados a objetos tienen jerarquías de herencia estrictas e imponen penalizaciones a la innovación. Esto dio lugar (en el 86 y 87) a un intenso debate, en el que se propuso un modelo alternativo de sharing basado en la delegación y los prototipos para reemplazar al de clases y herencia. Por otra parte Stein unió los puntos de vista de delegación y herencia indicando que la relación de clase/subclase es una delegación, así que si uno trabaja solamente con las clases como si fueran instancias, obtiene todos los beneficios indicados por Lieberman y otros. A partir de eso, definimos tres dimensiones independientes, para examinar y caracterizar los mecanismos de sharing: . ESTATICO O DINAMICO: ¿Cuándo se requiere que sean definidos los patrones de sharing? En un sistema estático, esto debe ser cuando se crea un objeto. En uno dinámico, cuando un objeto recibe un mensaje. . IMPLICITO O EXPLICITO: ¿Hay una operación para que el programador indique explícitamente los patrones de sharing, o el sistema lo hace automática y uniformemente? Hacerlo explícito permite delegar un único método, por ejemplo. . POR OBJETO O POR GRUPO: ¿La conducta se especifica para un grupo de objetos de una vez, o se puede poner comportamiento en un objeto individual? ¿Se puede especificar/garantizar la conducta de un grupo de objetos? Hay un amplio espectro de situaciones, desde la producción de grandes sistemas, en donde la eficiencia y la confiabilidad son los criterios principales y los cambios se presentan con relativa lentitud, hasta la programación exploratoria, que demanda flexibilidad y rápida respuesta a los cambios. Las diferentes situaciones pueden aprovechar mejor diferentes combinaciones de cualidades: para la programación experimental puede ser deseable tener un sharing dinámico, explícito y por objetos: mientras que para la producción de software a gran escala puede ser más apropiado el complemento: un sharing estático, implícito y por grupos. Hay un segundo mecanismo fundamental: la relación de clase-instancia o generador, en la que un objeto determina el tipo de otro, al que genera. Esta relación siempre se asoció a las características de estática, implícita y por grupo, pero esas restricciones no son necesarias ni inherentemente deseables. Los sistemas siguen una evolución natural de un estado dinámico y desorganizado a otro más estático y optimizado. La representación de los objetos también debería seguir una línea de evolución, y los entornos de programación deberían proveernos de representaciones más flexibles y de herramientas para cambiarlas a medida que el diseño (o partes de él) se estabilicen. Este acuerdo es conocido como El Tratado de Orlando.