Fecha de revisión: 25/Feb/2005

Los tipos de interfaz se colaron en la ciencia informática por la puerta trasera. Un día nos acostamos pensando que el Futuro, con mayúsculas, pertenecía a la Herencia Múltiple, tal como la implementaban Eiffel y San C++, y al despertarnos nos encontramos con interfaces hasta en la sopa (pasando por Java, COM y algo más tarde, Delphi).

PATRONES PARA CONTRATOS

El modelo básico de tipos de interfaz de Freya no se aparta demasiado (¡no podría hacerlo impunemente!) del modelo propuesto por la CLR y C#. No obstante, hay pequeñas adiciones pensadas para alegrarle la vida al programador. Un tipo de interfaz se puede explicar como una plantilla para implementar contratos. Sería entonces una pena desperdiciar la posibilidad de aplicar los conceptos del Diseño por Contratos a este tipo de datos. Freya permite especificar precondiciones y postcondiciones al definir un tipo de interfaz:

ICamera = interface
    procedure SetFrame(Eye, Target, Sky: Vector);
        requires (Target - Eye) xor Sky <> Vector.Null
            as 'El eje de visión no puede ser paralelo a la dirección del cielo';
    function GetRay(Row, Column: Integer): Ray;
        ensures Result.Length = 1
            as 'Los rayos de la cámara deben estar normalizados';
end;

Cualquier clase que implemente la interfaz ICamera puede asumir que se cumplen las precondiciones en las llamadas a sus métodos, y está obligada a satisfacer las postcondiciones al implementar los métodos definidos por el tipo de interfaz. Por supuesto, es imposible exigir a una implementación de ICamera escrita en C# que garantice el cumplimiento del contrato de GetRay... al menos, de momento. Pero esto no quita importancia a la posibilidad de especificar contratos más precisos.

Otra de las pequeñas mejoras que ofrece Freya es la definición de iteradores como parte de la definición de una interfaz:

ISampler = interface
    procedure Run;
    iterator Rays: Ray;
end;

Observe, en primer lugar, que se trata de un iterador "con nombre", no de un iterador "anónimo". Para obtener el equivalente en C#, en la versión 2.0, habría que escribir lo siguiente:

public interface ISampler
{
    void Run();
    IEnumerator Rays();
}

De hecho, esta es la forma en que la interfaz definida por Freya se vería desde C#, pero espero que coincidamos en que la notación de Freya es más clara y elegante. Un iterador "anónimo" de Freya tendría una declaración ligeramente diferente:

ISampler = interface
    procedure Run;
    iterator ISampler: Ray;
end;

La diferencia consiste en que el iterador se llama igual que el tipo, y la traducción a C# sería ésta:

public interface ISampler: IEnumerable
{
    void Run();
}

Recuerde que los tipos de interfaz pueden "heredad" de más un ancestro, por lo que no es problema añadir la interfaz IEnumerable a la lista explícita de ancestros (salvo si ya IEnumerable forma parte de esa lista, en cuyo caso estaríamos hablando de un error, sin más). Por supuesto, podemos hacer lo mismo directamente en Freya:

ISampler = interface(IEnumerable[Ray])
    procedure Run;
end;

La primera variante es mucho más clara, evidentemente.

IMPLEMENTACION IMPLICITA Y EXPLICITA

Los tipos de interfaz se declaran para que las clases (y estructuras o registros) puedan implementarlos posteriormente. Hay dos modos de implementación en C#, y Freya añade un tercer modo, que veremos al final. Los dos modos "clásicos" son la implementación implícita y la explícita.

La implícita es la más sencilla: usted incluye el tipo de interfaz a implementar en la lista de clases bases e interfaces del tipo implementador. Luego, cuando define un método con el mismo nombre que un método definido en el tipo de interfaz, el compilador asume que se trata de la implementación de ese método. C# impone una restricción: el método implementador debe ser público. En Freya, tendríamos lo siguiente:

public
    Camera = class(ICamera)
    public
        // Supongamos que ICamera sólo define dos métodos
        procedure Run;
        iterator Rays: Ray;
    end;

implementation for Camera is

    procedure Run;
    begin
        ...
    end;

    iterator Rays: Ray;
    begin
        ...
    end;

end.

Por cierto, estos métodos pueden también declararse virtuales. La principal consecuencia de la implementación implícita, al estilo C#, es que podemos llamar a un método como Run de dos maneras diferentes: a través de una referencia al objeto, es decir, a través de una variable o expresión de tipo Camera, y a través del tipo de interfaz, es decir, a través de una variable o expresión de tipo ICamera. Luego veremos por qué enfatizo este detalle aparentemente inocuo.

La declaración explícita introduce una variante: en C#, los métodos de la interfaz que queremos implementar se consideran automáticamente privados, y el método recibe un nombre un poco extravagante, pues se concatena el nombre de la interfaz con un punto y el nombre del método que se implementa. Veamos primero un ejemplo en C#:

class SaveCursor: IDisposable
{
    // Este es un constructor público...
    public SaveCursor
    {
        ...
    }

    // Observe:
    // 1- No se indica private
    // 2- El curioso nombre del método
    void IDisposable.Dispose()
    {
        ...
    }
}

La técnica de Freya es muy similar... excepto que no tenemos que declarar el método con la clase. A fin de cuentas, se trata de un método privado, y en Freya los detalles de la implementación se pueden escribir directamente en la sección implementation for de la clase:

public
    SaveCursor = class(IDisposable)
    public
        constructor SaveCursor(NewCursor: Cursor);
    end;

implementation for SaveCursor is

    constructor SaveCursor(NewCursor: Cursor);
    begin
        ...
    end;

    procedure IDisposable.Scope;
    begin
        ...
    end;

end.

Esta situación no existe en C# sencillamente porque este lenguaje no separa físicamente las declaraciones de las implementaciones, como hace Freya.

¿Qué justificación tiene la implementación explícita? La respuesta es muy importante, porque tiene que ver con uno de los motivos que provocó la sustitución casi universal de la herencia múltiple por los tipos de interfaz. Imagine lo qué sucedería si una clase tuviese que implementar dos tipos de interfaz, y que existiesen coincidencias en los nombres de algunos de los métodos de ambas. Digamos que las interfaces IAlfa e IBeta tienen ambas un método llamado Gamma (no estoy muy imaginativo hoy, lo siento). Si sólo permitimos la implementación implícita, la clase implementadora sólo podría dar una implementación común a ambos métodos: a IAlfa.Gamma y a IBeta.Gamma. Puede que sea "eso" lo que deseemos. Si no lo es, tenemos un problema grave. Además, ¿y si los métodos de las interfaces tienen distintos prototipos?

En general, cualquier lenguaje que trabaje con interfaces tiene que dar una respuesta lo más general posible a estas situaciones... ¡y lo mismo ocurre con los lenguajes que implementan herencia múltiple! En efecto, el mismo problema se presenta con la herencia múltiple, con el agravante de que es más difícil de resolver (eficientemente) en ese caso. Con los tipos de interfaz, la solución "física" es sencilla, y existen varias alternativas sintácticas en los lenguajes que han ofrecido estos tipos. En el Delphi clásico, por ejemplo, existe una cláusula que permite indicar: "el método tal de la interfaz más-cual está implementado por el método ya-no-se-cuál de esta clase". Esta es una solución más directa, en general, que la de C#, el CLR y, transitivamente, la de Freya. Note, sin embargo, que son soluciones equivalentes. En algunos casos, C# nos obliga a realizar una llamada a método adicional, pero recuerde que el compilador JIT puede convertir aprovechar la expansión en línea del código para optimizar muchos de estos casos.

Hay una consecuencia adicional de la implementación explícita, que puede ser aprovechada por cierta metodología de programación. Antes dije que, con la implementación implícita, los métodos de la interfaz también podían ejecutarse a través de una variable o expresión del tipo de la clase. Con la implementación explícita, ya no es posible. Para empezar, el método implementador se considera como private, y para terminar, tiene un nombre deliberadamente retorcido para hacer imposible sintácticamente su llamada. ¿Es eso positivo o negativo? ¿Por qué podría interesarnos esta limitación?

DECLARACION DE INTENCIONES

Imagine un grupo de programadores que se pone de acuerdo para acometer un proyecto. Es un proyecto grande, por lo que hay que dividirlo en subproyectos que se repartirán entre varios equipos. Para simplificar, supondremos que en cada equipo hay una sola persona.

CREDO
... sí, es uno de mis "prejuicios": no creo en la responsabilidad compartida a la hora de programar. La fuerza se puede sumar a la fuerza; con algunas restricciones, claro. Tiene sentido formar una cuadrilla para mover una piedra de lugar. En cambio, es mucho más complicado (excepto en las películas americanas) sumar inteligencia con inteligencia. Eso es posible cuando se trabaja en forma secuencial: un cerebro "hace", se detiene, el otro "revisa", por ejemplo... pero si se trata de una tarea a terminar con tiempo limitado, debe saber que estará desperdiciando la mitad del potencial intelectual de su equipo. La mejor forma de repartir el trabajo es asignar responsabilidades que no se solapen. Soy un individualista, lo reconozo... pero siempre podré echarle la culpa a la sociedad donde crecí y me eduqué, como suelen hacer los abogados de chorizos y mangantes.

¿Cómo se puede crear un sistema complejo a partir de módulos desarrollados por separado? Simplificando bastante, está claro que debe haber una autoridad central que decida las "reglas del juego": alguien debe dictaminar los contratos que servirán para comunicar esas partes. La mejor forma de expresar esos contratos es por medio de tipos de interfaz. Las interfaces introducen menos dependencias indeseables que las clases; incluso si consideramos clases abstractas. Si obviamos que esa especificación necesitará algo de tiempo para descubrirse y evolucionar a la situación óptima, podemos decir que el proyecto puede comenzar una vez que hayamos definido un conjunto básico de tipos de interfaz. Luego, cada equipo desarrollará clases que implementarán los tipos de interfaz de los que son responsables. Cuando estas clases necesiten utilizar funcionalidad desarrollada por otros equipos, deberán utilizar obligatoriamente estos tipos de interfaz, y NUNCA llamadas directas a recursos de otras clases. A través de las fronteras modulares, sólo deben utilizarse tipos de interfaz.

Ya se habrá dado cuenta de que, al describir la metodología anterior, he estado pensando en la implementación explícita... porque nos obliga, si queremos utilizar la funcionalidad de una clase, a utilizar referencias a la interfaz, nunca a la clase como tal. No obstante, hay un pequeño fallo en la técnica que utiliza C#, que complica la aplicación de esta metodología: al usar implementaciones explícitas hacemos que sea imposible acceder al método usando la clase... pero no sólo cuando se traspasan las fronteras de módulos, sino incluso cuando la llamada debe producirse desde dentro de la propia clase.

Para ilustrarlo, veamos un ejemplo inspirado en un sencillo ray tracer desarrollado en C#. Al principio de este artículo usé la siguiente interfaz como ejemplo:

ISampler = interface
    procedure Run;
    iterator Rays: Ray;
end;

Un sampler, o muestreador, es el componente que desencadena la generación de una imagen virtual a partir de una escena: ese es el papel del método Run (que por simplificar, no tiene parámetros ni tipo de retorno). Adicionalmente, un sampler debe poder lanzar una sucesión de rayos... Vale, no es un diseño perfecto: en realidad, el sampler del ejemplo real ha incorporado sin más contemplaciones el lanzamiento de rayos dentro de la implementación del equivalente de nuestro Run. Pero estas "imperfecciones" son comunes precisamente en situaciones reales.

Supongamos que la clase FocalSampler implementa explícitamente la interfaz ISampler:

implementation for FocalSampler is

    iterator ISampler.Rays: Ray;
    begin
        ...
    end;

    procedure Run;
    begin
        ...
        //¡Esto no es posible ahora!
        for r: Ray in Rays do
        begin
            ...
        end;
        ...
    end;

end.

Como ve, tenemos un problema: el cuerpo de Run necesita llamar al iterador Rays, pero no puede hacerlo, al tratarse de una implementación explícita. Hay soluciones en C#, sin renunciar a la implementación explícita:

  • Haga que Rays llame a un iterador protegido; entonces desde Run podrá usar directamente ese iterador protegido, en vez de Rays. Esto puede ser ineficiente, si lo hacemos ingenuamente, al tratarse de un iterador. En el caso de que lo que tengamos que llamar sea un método "regular", no es tan ineficiente, sobre todo si interviene el compilador JIT y realiza una expansión en línea. Pero esto introduce eslabones y dependencias que son fáciles de romper.
  • Convierta explícitamente el puntero this de C# al tipo de interfaz ISampler, y ejecute el iterador o el método sobre el resultado. Me temo que no hay JIT que arregle el destrozo causado a la eficiencia, en este caso. Esta "solución", sin embargo, parece ser más robusta.

En Freya, existe una tercera solución, a la que podemos llamar "dejémonos de chorradas":

implementation for FocalSampler is

    iterator ISampler.Rays: Ray;
    begin
        ...
    end;

    procedure Run;
    begin
        ...
        for r: Ray in ISampler.Rays do
        begin
            ...
        end;
        ...
    end;

end.

Freya permite usar el método implícito, siempre que lo hagamos desde la misma clase donde se ha introducido. La sintaxis se parece a la llamada de un método estático... pero no hay confusión posible, al ser ISampler un tipo de interfaz, no un tipo de clase.

IMPLEMENTACIÓN POR DELEGACIÓN

Además de las implementaciones explícita e implícita, Freya permite otra forma de implementación de interfaces, inspirada en Delphi. Se trata de la implementación por delegación. Supongamos que tenemos que implementar la interfaz IAlpha en varias clases. Las implementaciones son idénticas, o casi. ¿Podemos ahorrarnos algo de trabajo? Si podemos hacer que todas las clases tengan un ancestro común en el que podamos introducir la implementación, está claro que sí. Pero, ¿y si no podemos disponer de un ancestro común? Recuerde que seguimos trabajando con herencia simple...

Esta situación es parte del pequeño precio que pagamos por renunciar a la herencia múltiple. Como dicen los teóricos: la herencia de clases tiene dos aspectos. Por una parte, está la herencia de tipos, y por la otra, la herencia de implementaciones, o de código. Las interfaces son estupendas porque, al tratarse de entes etéreos, sin cuerpo material, simplifican el tratamiento de la herencia de tipos sin tenernos que preocupar por la herencia de código: ¿qué código, si estamos hablando de interfaces? Ahora bien, el problema antes planteado es muy común. ¿Qué podemos hacer en tal caso?

La solución consiste en crear una clase auxiliar que implemente la interfaz. A continuación, todas las clases finales que deban implementar la interfaz, según los requisitos iniciales, deben incorporar una instancia de esta clase auxiliar en su interior. Al implementar la interfaz, deben delegar las llamadas a sus métodos a la instancia interna. Por ejemplo:

public
    // La interfaz a implementar
    ICamera = interface
        procedure SetFrame(Eye, Target, Sky: Vector);
        function GetRay(Row, Column: Integer): Ray;
    end;

    // Una implementación reutilizable
    BaseCamera = class(ICamera)
    public
        procedure SetFrame(Eye, Target, Sky: Vector);
        function GetRay(Row, Column: Integer): Ray;
    end;

    // Reutilizará la implementación de BaseCamera
    // La implementación de ICamera será explícita
    ReflexCamera = class(Gadget, ICamera, ISampler)
    public
        constructor ReflexCamera;
    end;

implementation for BaseCamera is

    //... obviaremos los detalles...

implementation for ReflexCamera is

    // ¡Este es un campo privado en Freya, de la clase ReflexCamera!
    var camera: ICamera;

    constructor ReflexCamera;
    begin
        camera := new BaseCamera;
        ...
    end;

    procedure SetFrame(Eye, Target, Sky: Vector);
    begin
        if camera <> nil then
            camera.SetFrame(Eye, Target, Sky)
        else
            raise DelegationChainBrokenException;
    end;

    function GetRay(Row, Column: Integer): Ray;
    begin
        if camera <> nil then
            Result := camera.GetRay(Row, Column)
        else
            raise DelegationChainBrokenException;
    end;

end.

¡Esto es asquerosamente complicado de escribir, leer y por supuesto, de mantener! Freya simplifica su vida permitiendo la delegación de forma declarativa:

public
    ReflexCamera = class(Gadget, ICamera, ISampler)
    public
        constructor ReflexCamera;
    end;

implementation for ReflexCamera is

    var camera: ICamera;

    constructor ReflexCamera;
    begin
        camera := new BaseCamera;
        ...
    end;

    interface ICamera = camera;

end.

Ahora, una línea nos basta para implementar todos los métodos requeridos por la interfaz. El hecho de que la interfaz se implemente por delegación es un detalle interno, y por eso, no hay pistas al respecto en la declaración de la clase (queda claro, no obstante, que no se tratará de una implementación explícita). La delegación puede referirse a un campo o a una propiedad. Seguimos teniendo la responsabilidad de declarar e instanciar la variable, pero nos hemos ahorrado mucho trabajo.


Vea también:

Iteradores en Freya Propiedades en Freya
Métodos en Freya Constructores en Freya
Excepciones en Freya