Herencia, refinamiento, e interfaces

Esta es la cuarta parada de el Tour de Ceylon. En la parte anterior hemos visto atributos, variables, setters, y estructuras de control. En esta sección aprenderemos acerca de Herencia y refinamiento(conocido como “overriding” en muchos lenguajes).

La herencia es uno de dos caminos que Ceylon nos permite para abstraer los tipos. (La otra son generics, los cuales veremos mas adelante en este tour.) Una característica de Ceylon es un tipo de herencia múltiple llamada “mixin inheritance”. Tal vez has escuchado o experimentado la herencia múltiple en C++. Pero mixini inheritane en Ceylon mantiene una seria de restricciones que mantienen un buen balance entre poder y inocuidad.

Herencia y refinamiento

En programación orientada a objetos, frecuentemente remplazamos condicionales (if, switch) con subtipos. De hecho, acuerdo a algunas personas, esto es lo que hace un programa orientado a objetos. Tratemos de re-factorizar la clase Polar de la anterior parada del tour en dos clases, con dos diferentes implementaciones de description. Aquí esta la super clase:

"Una coordenada polar"
class Polar(Float angulo, Float radio) {

    shared Polar rotar(Float rotacion) =>
        Polar(angulo+rotacion, radio);

    shared Polar dilatar(Float dilatacion) =>
        Polar(angulo, radio*dilatacion);

    "La descripcion por default"
    shared default String descripcion =>
        "(``radio``,``angulo``)";
}

Notese que Ceylon fuerza a que declaremos atributos o metodos que pueden ser refinados(overriden) anotándolos con default.

Subclases especifican su super-clase usando la parabra reservada extends, seguida por el nombre de la super-clase, seguida por una lista de argumentos a ser enviados al inicializador de la super-clase. Parece solo una expresión que instancia una super-clase:

"Una coordenada polar con una etiqueta"
class EtiquetaPolar(Float angulo, Float radio, String etiqueta)
        extends Polar(angulo, radio) {

    "La descripcion con etiqueta"
    shared actual String descripcion =>
        etiqueta  + "-" + super.descripcion;
}

Ceylon también nos fuerza a declarar que atributo o método(override) un atributo o método de una super-clase anotándola con actual (no override en Java). Todas estas anotaciones tienen un pequeño costo extra al teclear, pero ayudan al compilador a detectar errores. No podemos inadvertidamente refina un miembros de una super-clase, o inadvertidamente fallar al refinarla.

Note que Ceylon deja fuera este camino al repudiar la idea del “duck” tipyng o structural typing. Si el camina() como un Pato, entonces el deberá de ser un subtipo de Duck y deberá de refinar explícitamente la definición de caminar() en Pato. Nosotros no creemos que solamente el nombre de un método o atributo es suficiente para identificar su semántica. Y mas importante, “structural typing” no trabaja adecuadamente con herramientas.

Sintaxis corta para refinamiento

Existe una manera mas compacta para refinar un miembro default de una super-clase simplemente especificando su refinada implementación usando =>, como en el siguiente ejemplo:

"Una coordenada polar con una etiqueta"
class EtiquetaPolar(Float angulo, Float radio, String etiqueta)
        extends Polar(angulo, radio) {

    descripcion => etiqueta  + ": " + super.descripcion;
}

O asignado un valor usando =, como el siguiente ejemplo:

"Una coordenada polar con una etiqueta"
class EtiquetaPolar(Float angulo, Float radio, String etiqueta)
        extends Polar(angulo, radio) {

    descripcion = "``etiqueta``: (``radio``, ``angulo``)";
}

Puedes refinar cualquier función o cualquiera que no sea variable usando esta sintaxis.

Refinando un miembro de Object

Nuestra clase Polar es un subtipo implicito de la clase Object en el paquete ceylon.language. Si quieres mirar dentro de esta clase, podrás ver que tiene un atributo default llamado string. Es común refinar este atributo para proveer a un desarrollador una amigable reprensentación del objeto.

Nuestra clase Polar es también un subtipo de la interfaz Identifiable la cual define implementaciones default de equals() y hash(). Tambien necesitamos refinar estos:

"Una coordenada polar"
class Polar(Float angulo, Float radio) {


    // ...

    shared default String descripcion =>
        "(``radio``,``angulo``)";

    value azimuth => pi * (angulo/pi).fractionalPart;

    shared actual Boolean equals(Object that){
        if (is Polar that) {
            return azimuth==that.azimuth &&
                   radio==that.radio;
        }
        else {
            return false;
        }
    }

    shared actual Integer hash => radio.hash;

    shared actual String string => description;
}

Esta es la primera vez que hemos visto esta sintaxis:

if (is Polar that) { ... }

Como probablemente supusiste, if (is ...) trabaja como if (exists ...), probando y estrechando el tipo de un valor. En este caso prueba el tipo de that y lo estrecha a Polar si that es una instancia de Polar. Volveremos después en el tour a esta construcción.

Usando la sintaxis corta para refinamiento que hemos visto, podemos abreviar el código anterior como esto:

"Una coordenada polar"
class Polar(Float angulo, Float radio) {


    // ...

    shared default String descripcion =>
        "(``radio``,``angulo``)";

    value azimuth => pi * (angulo/pi).fractionalPart;

    shared actual Boolean equals(Object that){
        if (is Polar that) {
            return azimuth==that.azimuth &&
                   radio==that.radio;
        }
        else {
            return false;
        }
    }

    hash => radio.hash;

    string => descripcion;
}

(Pero en este caso, esta sintaxis corta es quizás no una mejora.)

Clases abstractas

Ahora consideremos un problemas mas interesante: Una abstracción sobre el sistema de coordenadas polar y cartesiana. Desde que una coordenada no es solo un tipo especial de coordenadas polares, esto es un caso para la introducción de una super-clase abstracta:

"Una libre abstraccion de un sistema de coordenadas para un punto
geometrico"
abstract class Point() {

    shared formal Polar polar;
    shared formal Cartesiano cartesiano;

    shared formal Punto rotar(Float  rotacion);
    shared formal Punto dilatar(Float dilatacion);

}

Ceylon requiere que anotemos las clases abstractas con abstract, al igual que en Java. Esta anotación indica que una clase no puede ser instanciada y puede definir miembros abstractos. Como en Java, Ceylon también requiere que anotemos con abstract los miembros a los cuales no especifiquemos una implementación. Sin embargo, la anotación requerida en formal. La razón para tener dos distintas anotaciones, como veremos mas adelante, es que clases anidadas pueden ser abstract o formal, y las clases anidadas abstract son un poco diferentes a una clase miembro formal. Una clase miembro formal puede ser instanciada; una clase abstract no puede.

Note que un atributo que nunca es inicializado es siempre un atributo formal. Ceylon Ceylon no inicializa los atributos a cero o null amenos que tu lo especifiques.

Una manera de definir una implementación para una atributo abstracto heredado es usar la sintaxis corta de refinamiento que hemos visto anteriormente.

"Una coordenada polar"
class Polar(Float angulo, Float radio)
        extends Punto() {

    polar => this;

    cartesiano => Cartesiano(radio*cos(angulo), radio*sin(angulo));

    rotar(Float rotacion) => Polar(angulo+rotacion, radio);

    dilatar(Float dilatacion) => Polar(angulo, radio*dilatacion);

}

Alternativamente, podemos escribirlo de la manera larga.

"Una coordenada polar"
class Polar(Float angulo, Float radio)
        extends Punto() {

    shared actual Polar polar => this;

    shared actual Cartesiano cartesiano =>
            Cartesiano(radio*cos(angulo), radio*sin(angulo));

    shared actual Polar rotar(Float rotacion) =>
            Polar(angulo+rotacion, radio);

    shared actual Polar dilatar(Float dilatacion) =>
            Polar(angulo, radio*dilatacion);

}

Note que Ceylon, como Java, permite el refinamiento co-variante del tipo de un miembro. Refinamos el tipo de retorno de rotar() y dilatar, estrechando a Polar desde el tipo mas general declarado por Punto. Pero Ceylon actualmente no soporta refinamiento contra variante de los tipos de un parámetro. No puedes refinar un método y ensanchar el tipo de un parámetro. (Algún día nos gustaría arreglar esto.)

Por supuesto, no puedes refinar un miembro y ensanchar el tipo de retorno, o cambiar a algún tipo arbitrario diferente, desde que en dicho caso la subclase deberá no ser a lo largo un subtipo de el supertipo. Si tu vas a refinar el tipo de retorno, tienes que refinarlo a un subtipo.

Cartesiano también de mannera covariante refina rotar() y dilatar(), pero a un tipo diferente de retorno.

import ceylon.math.float { atan }

"Una coordenadas cartesiana"
class Cartesiano(Float x, Float y)
        extends Punto() {

    shared actual Polar polar => Polar( (x^2+y^2)^0.5, atan(y/x) );

    shared actual Cartesiano cartesiano => this;

    shared actual Cartesiano rotar(Float rotacion) =>
            polar.rotar(rotacion).cartesiano;

    shared actual Cartesiano dilatar(Float dilatacion) =>
            Cartesiano(x*dilatacion, y*dilatacion);

}

No hay una manera de prevenir que otro código extienda una clase (no hay un equivalente una clase final in Java). Desde que únicamente miembros que son declarados para soportar refinamiento usando alguno de formal o default puede ser refinado, un subtipo no puede romper la implementación de un supertipo. A menos que el supertipo fuese explícitamente diseñado a ser extendido, un subtipo puede agregar métodos, pero nunca cambiar el comportamiento de los miembros heredados.

Las clases abstractas son útiles. Pero desde que las interfaces en Ceylon son mas poderosas que las interfaces en Java, es mas frecuente usar una interfaz en vez de una clase abstracta.

Interfaces y herencia “múltiple”

Algunas veces, nos surge alguna situación donde una clase necesita de funcionalidades anidadas de mas de un supertipo. El modelo de herencia de Java no soporta esto, desde que una interfaz no puede define un miembro con una implementación concreta. Las interfaces en Ceylon son un poco más flexibles:

  • Una interfaz puede definir un método concreto, getter y setters de atributos, pero
  • Puede no definir referencia o inicialización lógica.

Note que prohibiendo las referencias y la inicialización lógica crea interfaces completamente sin estado. Una interfaz no puede mantener referencia a otros objetos.

Tomemos ventaja de herencia “mixin” para definir una interfaz Writer para Ceylon.

interface Writer {

    shared formal Formatter formatter;

    shared formal void write(String string);

    shared void writeLine(String string) {
        write(string);
        write("\n");
    }

    shared void writeFormattedLine(String format, Object* args) {
        writeLine(formatter.format(format, args));
    }
}

Note que no podemos definir un valor concreto para el atributo formatter, desde que una interfaz no puede mantener una referencia a otro objeto.

Ahora definamos una concreta implementación de la interfaz.

class ConsoleWriter() satifies Writer {
    formatter => StringFormatter();
    write(String string) => print(string);
}

La palabra reservada satisfies (no implements como en Java) es usada para especificar que una interfaz extiende a otra interfaz o que una clase implementa una intefaz. A diferencia de la declaración extends, una declaración satisfies no se le especifican argumentos , puesto que las interfaces no tienen parámetros o inicialización logica. Ademas, la declaración satisfies puede declarar mas de una interfaz.

El enfoque de Ceylon en las interfaces elimina un patrón común en Java donde una clase abstracta separada define una implementación por default para algún miembro de la interfaz. En Ceylon, la implementación por default puede ser especificada por la interfaz misma. Incluso mejor, es posible agregar un nuevo miembro a una interfaz sin romper una implementación existente de la interfaz.

Ambigüedad en herencia “múltiple”

Es ilegal para un tipo heredar dos miembros con el mismo nombre, amenos que ambos miembros(directa o indirectamente) refinen a un común miembros de un común supertipo, y el tipo heredado también refine el miembro para eliminar cualquier ambigüedad. Lo siguiente resulta en un error de compilación:

interface Party {
    shared formal String legalName;
    shared default String nombre => legalName;
}

interface User {
    shared formal String userId;
    shared default String name => userId;
}

class Customer(String customerName, String email)
        satisfies User & Party {

    shared actual String legalName = customerName;
    shared actual String userId = email;
    shared actual String name = customerName;  // error: refina dos diferentes
                                               // miembros
}

Para arreglar este código, deberá factorizar una declaración formal de el atributo name a un común supertipo. El siguiente código es legal.

interface Named {
    shared formal String name;
}

interface Party satisfies Named {
    shared formal String legalName;
    shared actual default String name => legalName;
}

interface User satisfies Named {
    shared formal String userId;
    shared formal default String name => userId;
}

class Customer(String customerName, String email)
        satisfies User & Party {
    shared actual String legalName = customerName;
    shared actual String userId = email;
    shared actual String name = customerName;
}

Oh, y por supuesto lo siguiente es ilegal:

interface Named {
    shared formal String name;
}

interface Party satisfies Named {
    shared formal String legalName;
    shared actual String name => legalName;
}

interface User satisfies Named {
    shared formal String userId;
    shared formal String name => userId;
}

class Customer(String customerName, String email)
        satisfies User & Party { // error: inherits multiple definitions
                                 // of name
    shared actual String legalName = customerName;
    shared actual String userId = email;
}

Para arreglar este código, name deberá de ser declarado default en ambos User y Party y explícitamente refinado en Customer.

Aun hay mas

En la siguiente parada, para finalizar la discusión de programación orientada a objetos en Ceylon, aprenderemos acerca de clases anónimas y clases miembro.