Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.13.4 Técnica Manejador-Cuerpo

§1 Preámbulo

En informática el concepto manejador ("handle") es muy general; un manejador puede ser un puntero, una referencia, o incluso un objeto que sirve para acceder a otro objeto o entidad. Por ejemplo, un puntero inteligente ( 4.12.2b1). En C++ también recibe este nombre la sentencia que recibe una excepción ( 1.6), pero en este capítulo nos referimos a un tipo especial de manejador: ciertos tipos abstractos que sirven como interfaz o marco de comunicación y acceso de otros tipos. Estos "handles" utilizan una técnica denominada manejador-cuerpo, aunque es más conocida por su designaciones inglesas "handle-body idiom". Se refiere a un tipo de diseño de POO en el que se disocia la interfaz de la clase, aquí denominada manejador ("handle"), de su implementación, aquí designada cuerpo ("body").

La técnica fue originalmente presentada por James Coplien [1] y representa un paso más en el proceso ya señalado ( 4.11) de separar los detalles de implementación de la interfaz que ve el usuario de la clase.  Presenta una serie de ventajas técnicas sobre el sistema tradicional de diseño, en el que la interfaz está constituida por los miembros públicos de la clase, mientras la implementación es confiada a los miembros privados, y ha hecho fortuna incluso fuera del ámbito de la programación C++, ya que como veremos, el concepto subyacente puede ser utilizado en cualquier otro lenguaje que permita POO y en multitud de circunstancias.

§2 Sinopsis

El principio de funcionamiento es muy simple: se define una clase-cuerpo (body) que contiene la implementación (lo que podríamos llamar la inteligencia u operatividad), y otra clase-manejador (handle) que contiene la interfaz. Todas las solicitudes al manejador son pasadas por este al cuerpo, generalmente por medio de un puntero.

El diseño esquemático de ambas clases es como sigue:

class Body {         // Cuerpo (implementacion)

  friend class Handle

  private:

    void funcB();

};

...

class Handle {       // Manejador
  public:
    Handle();
    ~Handle();
    void funcH();    // un método público
  private:
    Body* bPtr;      // puntero a la implementación
};

...

void Handle::funcH() {

  bPtr->funcB();

}

Las solicitudes al manejador, tales como invocación del método funcH, son pasados mediante el puntero bPtr a la función correspondiente de la implementación (funcB). Todos los miembros del cuerpo se han declarado privados, por lo que estos objetos solo pueden ser accedidos a través del manejador que ha sido declarado friend ( 4.11.2a1) con este propósito.

Generalmente la implementación se realiza en módulos separados, en la forma esquematizada a continuación.

// body.h

// interfaz de Body
 
class Body {         // Cuerpo 
  friend class Handle
  private:
  void funcB();
};

// handle.h
// interfaz de Handle
#include <body.h>
 
class Handle {   // Manejador
  public:
    Handle();
    ~Handle();
    void funcH();
  private:
    Body* bPtr;
};

// body.cpp
// implementación de Body
include <body.h>
 
void Body::funcB() {
// detalles de implementación
}

// handle.cpp
// implementación de Hanle
#include <handle.h>
 
void Handle::funcH() {
  bPtr->funcB();
}

De esta forma se consigue que las modificaciones en el cuerpo solo exijan recompilar su implementación (obtener una nueva versión del fichero body.obj), mientras que la interfaz handle.obj permanece invariable.

Es importante observar que en un diseño como el anterior, al ser todos los miembros de Body privados, incluso la creación y destrucción de estos objetos deben ser encomendados al manejador. Debemos suponer por tanto, que los constructores de Handle no solo se encargan de crear sus propias instancias h1, h2, etc. También de crear los objetos Body , b1, b2, etc, e iniciar los miembros bPtr (h1.bptr, h2.bptr, etc) con los valores adecuados a cada caso.

Esta técnica, cuyas características básicas hemos señalado, se presta a muchas utilizaciones concretas. Por ejemplo, cuando un solo cuerpo es accedido desde múltiples "handles". Es el caso de los objetos-puntero cuando se utiliza la técnica denominada contador de referencias ("Reference-counter").

Cuando se utilizan solo dos clases, una como manejador y la otra como cuerpo, la técnica se denomina también de carta-sobre ("Letter-envelope"), en cuyo caso, la "carta" sería el cuerpo y el "sobre" el manejador. Observe que no existe ninguna restricción sobre las características del cuerpo, de forma que el manejador "handle" puede ser a su vez el cuerpo de otro manejador. El proceso puede repetirse las veces que sean necesarias, de forma que una "carta" puede ser a su vez "sobre" de otra "carta" interior en un proceso parecido al de las muñecas rusas.

La técnica manejador-cuerpo resulta especialmente útil en aplicaciones de manejo de datos. En estos casos la capa de presentación de datos (que denominaremos cliente) puede ser separada de la que realmente maneja los datos a bajo nivel (capa de datos).  Esta última está generalmente representada por gestor de base de datos DBMS ("Data Base Management System"), que suele ser un motor de base de datos relacional RDBMS ("Relational Database Management System"), aunque puede ser de cualquier otro tipo, incluso un simple fichero en disco; una tabla en memoria, o una interfaz Web que envía su petición a un servidor remoto.

En estas circunstancias el cuerpo representa a la capa de datos, que puede ser manejada desde la capa cliente a través de un manejador adecuado. En cualquier aplicación concreta el manejador está representado por una instancia de la clase "Handle", que resulta así un objeto de acceso a datos, por lo que es frecuente que la técnica manejador-cuerpo sea denominada DAO ("Data Access Objecs") cuando se utiliza en este tipo de aplicaciones.

§3 Ventajas

El sistema presenta algunas ventajas (e inconvenientes) respecto al diseño tradicional de una clase única, así como una variedad de posibilidades de aplicación en diferentes contextos que resultan muy interesantes:

§3.1 Mayor independencia entre los módulos de la aplicación

En efecto: es sabido que cuando se modifica un elemento de una aplicación, todos los demás módulos que lo utilizan deben ser recompilados de nuevo. En el diseño de software suele ocurrir que la interfaz de las clases se mantenga relativamente estable, aunque sean frecuentes las modificaciones y perfeccionamientos del su diseño interior. Si trabajamos en un entorno compartido, en el que más usuarios manejan nuestra clase, la técnica cuerpo-manejador permite que las modificaciones en el cuerpo no obliguen a los usuarios a recompilar todos los módulos que la utilizan, ya que el acceso se realiza a través del manejador y este no sufre modificaciones. Todo lo que deben hacer es enlazar de nuevo sus aplicaciones utilizando la última versión del objeto (body.obj).

§3.2 Modularidad

La independencia entre los módulos representa diversas ventajas en las aplicaciones. Por ejemplo, en las que manejan datos, cuando la capa cliente utiliza objetos DAO, no necesita conocer los detalles de la capa de datos, de forma que es posible reemplazar esta última sin necesidad de cambiar ni una línea de código en la programación de los clientes. Por ejemplo, es posible sustituir una capa de datos que utiliza una base de datos relacional por un servicio Web sin necesidad de cambiar absolutamente nada en el cliente.

Los beneficios de esta modularidad funcionan en ambas direcciones. Si tenemos una capa de datos determinada (que puede ser un motor de base de datos), es posible construir DAOs (manejadores) para diversos tipos de clientes. Por ejemplo navegadores Web, tareas en background, etc.

§3.3 Ocultación de la tecnología

Cuando el resto de usuarios no pertenecen al entorno del mantenedor de la clase, son simplemente "clientes" de una librería comercial, es frecuente que sus creadores deseen mantener el máximo sigilo posible sobre los detalles de la implementación y de la tecnología utilizada (el "Know how").

Como siempre es necesario proporcionar la interfaz de la clase a sus usuarios, para que puedan disponer de la información pertinente a su manejo, es inevitable proporcionar cierta información sobre los métodos privados y protegidos contenidos en los ficheros de cabecera.

Nota: recuerde que el diseño de C++ permite que todos los detalles de la implementación puedan estar incluidos en el fichero de cabecera. El hecho de declarar privados ciertos miembros solo garantiza que el usuario no tendrá acceso a ellos, aunque pueda conocer todos los pormenores de su diseño.


Para evitar este inconveniente, se recurre a la técnica del Gato de Cheshire, denominada así en referencia al personaje Lewis Carroll de "Alicia en el País de las Maravillas", que se difumina, y del que solo queda su sonrisa [2].  Consiste en una variación del esquema de módulos del párrafo anterior ; en este caso se proporcionan al cliente los módulos handle.obj, body.obj y la interfaz del manejador contenido en un fichero <handle.h> con el siguiente diseño:

// handle.h
 
class Body; // declaración adelantada 
 
class Handle {  // Manejador
  public:
    Handle();
    ~Handle();
    void funcH();
  private:
    Body* bPtr;
};

En este caso, la cabecera <body.h> con los detalles de la implementación es considerada restringida, y no es distribuida a los usuarios.

§3.4 Manejo de objetos diversos

La técnica "Handle-body" también permite utilizar un único manejador para acceder a objetos de tipos distintos, siempre que presenten la misma intefaz. Para esto se recurre a que los objetos deriven de una clase polimórfica ( 4.11.8) que puede ser abstracta ( 4.11.8c). Siguiendo con nuestro esquema, el diseño tendría el siguiente aspecto:

class Body {         // Superclase abstracta
  friend class Handle;
  protected:
  virtual void funcB() =0;
  virtual ~Body();   // destructor virtual!!
};
 
class B1 : public Body {
  friend class Handle;
  void funcB();       // Implementación-1
};
class B2 : public Body {
  friend class Handle;
  void funcB();       // Implementación-2
};
 
class Handle {        // Manejador
  public:
    Handle();
    ~Handle();
    void funcH();     // un método público
  private:
    Body* bPtr;       // puntero a la superclase
};
...
void Handle::funcH() {
  bPtr->funcB();
}


En este caso, el manejador Handle puede acceder a dos tipos de objetos: B1 y B2, que representan dos implementaciones distintas de la superclase Body.  Recuerde que el puntero bPtr se define como puntero-a-la-superclase, de forma que en la práctica puede señalar a objetos de cualquiera de las subclases B1, B2, ... ( 4.2.1f). Las solicitudes al manejador, tales como invocación del método funcH, son pasados mediante el puntero bPtr a la función funcB del tipo Bn correspondiente, por acción de su tipo de enlazado (enlazado retrasado 1.4.4).

Observe que el destructor de la superclase se declara virtual a fin de que los objetos Bn puedan ser destruidos desde el manejador ( 4.11.2d2). En la página adjunta se muestra una versión ejecutable del esquema anterior ( ejemplo-1).

Al tratar de la internacionalización ( 5.2) veremos que el sistema de localismos del nuevo Estándar C++ utiliza esta técnica; de forma que la clase locale es en realidad un "Handle" a través del que se manejan unas clases denominadas facets, que son las que realmente contienen la funcionalidad de internacionalización.

§3.5 Economía de operación

En muchas ocasiones, los objetos manejados mediante la técnica handle-body, no son realmente copiados. Cuando se necesita disponer de una copia del objeto (cuerpo) lo que se hace realmente es realizar una copia del manejador, lo que resulta mucho más económico en términos de proceso.

§3.6 Constructores virtuales

La técnica letra-carta es también la base de los denominados constructores virtuales. No se trata por supuesto de constructores virtuales en el sentido literal de la palabra (los constructores no pueden ser declarados virtuales), sino de un tecnicismo que trataremos más detenidamente en el siguiente capítulo ( 4.13.5).

  Inicio.


[1] James O. Coplien "Advanced C++: Programming Styles and Idioms". Addison-Wesley, Reading, MA. 1994 (la primera edición es de 1992).

[2]  Reproducimos este Gato de Cheshire por cortesía de Jeffrey Hoffman   www.webslingerz.com