Clases anónimas y miembro

Esta es la cuarta parada en el tour de Ceylon. En la parada anterior hemos aprendido acerca de herencia y refinamientos. Es momento de completar nuestra introducción a la programación orientada a objetos en Ceylon aprendiendo acerca de las clases anónimas y clases miembro.

Clases anónimas

Si una clase no tiene parámetros, es posible usar una declaración corta que defina una instancia con nombre de la clase, sin proveer un nombre para la clase misma. Esto es usualmente de mas ayuda cuando estamos extendiendo una clases abstracta o implementando una interfaz.

doc "El origen"
object origen extends Polar(0.0, 0.0) {
    descripcion => "origen";
}

Una clase anónima puede extender una clase ordinaria y satisfacer una interfaz.

shared object consoleWriter satisfies Writer {
    formatter = StringFormatter();
    write(String string) => process.write(string);
}

La desventaja de la declaración de un objeto es que no podemos escribir código que refiera a el tipo concreto de origen o consoleWriter, únicamente a la instancia con nombre.

Tal vez estés tentado a pensar a la declaración de objetos como definición de singletons, pero eso no es del todo correcto.

  • La declaración de un objeto en toplevel, de hecho define un singleton.
  • La declaración de un objeto anidado en una clase define un objeto por cada instancia de la clase contenedora.
  • La declaración anidada en un método, getter, o setter da como resultado un nuevo objeto cada vez que el método, getter o setter es ejecutado.

Veamos como esto puede ser útil:

interface Subscription {
    shared formal void cancel();
}

Subscription register(Suscriber s) {
    subscribers.append(s);
    object subscription satisfies Subscription {
        shared actual void cancel() =>
            subscribers.remove(s);
    }
    return subscription;
}

Note como este código de ejemplo hace hábilmente uso de el hecho que la declaración anidada recibe una closure de los valores contenidos en la declaración del método contenedor.

Una manera distinta de entender acerca de la diferencia de un objeto y una clase es pensar que una clase es como un object con parámetros. (Por supuesto hay una gran diferencia: una declaración de una clase define un tipo con nombre y con esto podemos referirnos a el en otras partes del programa.) Como veremos después, Ceylon nos permite pensar en los métodos como atributos con parámetros.

Un declaración object puede refinar un atributo declarado con formal o default mientras este sea un suptipo del tipo declarado en el atributo a refinar.

shared abstract class App() {
    shared formal OutputStream stream;
    ...
}


class ConsoleApp() extends App() {
    shared actual object stream
        satisfies OutputStream { ... }
    ...
}

Sin embargo, un object no podra ser mismo declarado formal o default.

Clases miembro y su refinamiento

Probablemente has usado anidar una clase dentro de un método o clase. Desde que Ceylon es un lenguaje con bloques de estructura recursivos, la idea de una clase anidada es mas que natural. Pero en Ceylon, una clase anidada no abstracta es un miembro del tipo contenedor. Por ejemplo, BufferReader define la clase miembro Buffer:

class BufferReader(Reader reader)
        satifies Reader {

    shared default class Buffer()
        satisfies List<Character> { ... }

    ...

}

La clase miembro buffer es anotada con shared, entonces podemos instanciar la clase de la siguiente manera:

BufferReader br = BufferReader(ExampleReader());
BufferReader.buffer b = br.Buffer();

Note que el tipo anidada deberá ser identificado junto con el tipo contenedor cuando es usada fuera de la clase.

El miembro de la clase Buffer es también anotado con`default`, así que podemos refinarlo en un subtipo de BufferReader:

class BufferFileReader(File file)
        extends BufferReader(FileReader(file)) {
    shared actual class Buffer()
            extends super.Buffer() { ... }
    ...
}

Es correcto: ¡Ceylon nos permite “sobreescribir” una clase miembro de un supertipo!

Note que BufferFileReader.Buffer es una subclase de BufferReader.Buffer.

Ahora, la instancia anterior br.buffer(), !es una operación polimorfa¡ Puede devolver una instancia de BufferReader.Buffer o de BufferReader.buffer, dependiendo al que refiera br de BufferReader o BufferFileReader. Esto es mas que un lindo truco. La instanciación nos permite eliminar al concepto llamado “factory method pattern” de nuestro código.

Es posible incluso, definir una clase miembro formal. Una clase miembro formal puede declarar miembros formal.

abstract class BufferReader (Reader reader)
        satisfies Reader {
    shared formal lass Buffer() {
        shared formal Byte read();
    }
    ...
}

En este caso, una subclase concreta de la clase abstract deberá refinar los miembros formal de la clase.

shared class BufferFileReader(File file)
        extends BufferReader(FileReader(file)) {
    shared actual class Buffer()
            extends super.Buffer() {
        shared actual Byte read() {
            ...
        }
    }
    ...
}

Nótese la diferencia entre una clase abstract y una clase miembro formal. Una clase anidada abstract y no necesita ser refinada por subclases concretas de la clase contenedora. Una clase miembro formal puede ser instanciada y deberá ser refinada por cada subclase de la clase contenedora.

Es un interesante ejercicio comparar el refinamiento de las clase miembro en Ceylon con la funcionalidad de inyección de dependencias en los frameworks con Java. Ambos mecanismos proveen un significado de abstraer la operación de instanciación de un tipo. Puedes pensar que las subclases que refinan un miembro tipo como llenando el mismo rol como una configuración de dependencia en un framework de inyección de dependencias.

Aun hay mas

Clases miembro y su refinamiento permiten a Ceylon soportar type families. No hablaremos de ello en este tour.

En la siguiente estación, conoceremos a las secuencias, basandose Ceylon en el tipo “Array”.