Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace http://www.chuidiang.org

Ocultación y Encapsulamiento en C++

Todos hemos oido hablar de la encapsulación de información en los lenguajes orientados a objetos y en C++. Vamos a ver aquí en qué consiste y algunos "trucos" que podemos hacer en el caso concreto de C++ y que no suelen venir en los libros de este lenguaje (aunque sí en libros sobre patrones de diseño).

Los puntos que veremos son:

Encapsulamiento de los atributos de una clase

Antes de nada, debe quedar claro que el encapsulamiento, igual que cualquier buen hábito de programación (como no poner goto, comentar, etc) es útil para código que más adelante se puede querer reutilizar o modificar, por otras personas o por uno mismo. Si yo hago un programa de marcianos y nunca jamas pienso volver a tocarlo, da igual que lo haga con gotos y sin comentar mientras me entere yo mismo mientras lo estoy haciendo y funcione. Pagaré este "pecado" si dentro de dos meses se me ocurre mejorarlo o quiero reaprovechar algo de su código para otro programa.

Comento esto porque el encapsulamiento, llevado a su extremo, como es el caso del punto final de interfaces, hace la programación un poco más complicada (hay que hacer más clases). Este esfuerzo sólo se ve recompensado si el código es muy grande (evitando recompilados innecesarios) o se va a reutilizar en un futuro (podremos extraer clases con menos dependencias de otras clases). Dicho esto, vamos al tema.

Cualquier curso de orientación a objetos nos dice que es mejor poner los atributos de una clase protegidos o privados (nunca públicos) y acceder a ellos a través de métodos públicos que pongamos en la clase. Veamos el motivo. Supongamos, por ejemplo, que nos piden un programa que permita llevar una lista de gente con sus fechas de nacimiento. Entre otras cosas, decidimos hacernos nuestra clase Fecha con varios métodos maravillosos de la siguiente manera.

class Fecha
{
   public:
      int anho; // El anho con cuatro cifras, ej. 2004
      int mes;  // El mes, de 1 a 12
      int dia;    // El dia, de 1 a 31
      void metodoMaravilloso1();
      void metodoMaravilloso2();
};

Ya hemos hecho la clase. Ahora hacemos el resto del código y en unos varios miles de líneas de código usamos directamente cosas como esta.

Fecha unaFecha;
unaFecha.anho = 2004;
unaFecha.mes = 1;
unaFecha.dia = 25;

Finalmente acabamos nuestro programa y todo funciona de maravilla. Unos días después nos dicen que el programa va a guardar tropecientas mil personas y que ocupan mucho los ficheros, que a ver si podemos hacer algo para remediarlo. ¡Vaya!, almacenamos una fecha con tres enteros. Si usamos el formato de la mayoría de los ordenadores, en el que la fecha es el número de segundos transcurridos desde el 1 de Enero de 1970 (lo que nos devuelve la función time()), basta con un entero.

Total, que manos a la obra, cambiamos nuestra clase para que tenga lo siguiente:

class Fecha
{
   public:
      /* Comentado por ineficiente
      int anho; 
      int mes; 
      int dia;   */

      long numeroSegundos;

      void metodoMaravilloso1();
      void metodoMaravilloso2();
};

Ya está hecho lo fácil. Ahora sólo hay que ir por las tropecientas mil líneas de código cambiando nuestras asignaciones y lecturas a los tres enteros anteriores por el nuevo long.

Hubiera sido mucho mejor si hubieramos hecho estos tres enteros protegidos y unos métodos para acceder a ellos. Algo como esto

class Fecha
{
   public:
      void tomaFecha (int anho, int mes, int dia);
      int dameAnho ();
      int dameMes ();
      int dameDia ();
      void metodoMaravilloso1();
      void metodoMaravilloso2();
   protected:
      int anho; // El anho con cuatro cifras, ej. 2004
      int mes;  // El mes, de 1 a 12
      int dia;    // El dia, de 1 a 31
};

Si ahora tenemos que hacer el mismo cambio, basta con cambiar los atributos protegidos. Los métodos tomaXXX() y dameXXX() se mantienen en cuanto a parámetros y valor devuelto, pero se modifica su código interno para que conviertan el año,mes y dia en un long de segundos y al revés. El resto del código no hay que tocarlo en absoluto.

Es incluso mejor hacer los atributos privados que protegidos. Haciéndolos protegidos, las clases hijas (las que heredan de Fecha) pueden acceder directamente a estos atributos. Cuando hagamos el cambio por un long, debemos cambiar también el código de las clases hijas. Si los atributos son privados y obligamos a las clases hijas a acceder a ellos a través de métodos, tampoco tendremos que cambiar el código de estas clases hijas.

El acceso a través de métodos es menos eficiente que hacerlo directamente, así que aunque siguiendo el principio de ocultación es mejor hacer atributos privados, por eficiencia en algunos casos quizás sea mejor hacerlos protegidos (o incluso públicos) a riesgo de tener que cambiar más líneas de código en caso de cambio.

CONSEJO:
Siempre que sea posible hacer los atributos de una
clase privados.

Importancia de la encapsulación en C++

Con lo contado hasta ahora evitamos tener que cambiar código en caso de cambiar parámetros.

En el caso concreto de C++ hay un pequeño problema adicional. Es bastante normal hacer que las clases se definan por medio de dos ficheros. En el caso de la clase Fecha tendríamos un Fecha.h con la definición de la clase y un Fecha.cc (o .cpp) con el código de los métodos de la clase. Cuando queremos usar la clase Fecha, solemos hacer nuestro #include <Fecha.h>.

Cualquier proceso de compilado eficiente (como la utilidad make de linux y supongo que el Visual C++) es lo suficientemente listo como para recompilar sólo aquellos ficheros que es necesario recompilar. Es decir, si ya tenemos nuestro proyecto compilado y tocamos un fichero, el compilador sólo compilará ese fichero y todos los que dependen de él. Esta características es muy importante en proyectos grandes (con muchos ficheros y muchas líneas de código), para ahorrar tiempo de compilado cada vez que hacemos una modificación (He trabajado en proyectos que tardaban en compilar desde cero alrededor de 4 horas).

¿Cual es el problema?. El problema es que si decidimos, por ejemplo, cambiar nuevamente el atributo privado de la clase Fecha por otra cosa, necesitamos tocar el fichero Fecha.h. Esto hará que se recompilen todos los ficheros que hagan #include <Fecha.h> y todos los ficheros que hagan #include de algun fichero que a su vez haga #include de Fecha.h y así sucesivamente.

La solución es evidente, colocar lo menos posible en el fichero Fecha.h, en concreto los #define y variables globales que no sea necesario ver desde otras clases.

Por ejemplo, nuestra clase Fecha podía tener unos #define para indicar cual es el número mínimo y máximo de mes. Es mejor colocar estos #define en Fecha.cc en vez de en Fecha.h, salvo que alguien tenga que verlos.

// Esto mejor en el .cc que en el .h
#define MES_MINIMO 1
#define MES_MAXIMO 12

CONSEJO:
Siempre que sea posible, poner los #define, definición de tipos,
constantes globales, etc, dentro del fichero .cc

Encapsulamiento a través de interfaces

Nos queda una cosa. ¿Por qué tenemos que recompilar muchas cosas si cambiamos un atributo privado de la clase?. Lo ideal sería poder cambiar las cosas internas de la clase sin que haya que recompilar nada más, a fin de cuentas, el atributo es privado y nadie lo utiliza directamente.

Es bastante habitual en programación orientada a objetos el uso de interfaces para hacer que las clases no dependan entre sí. En el caso de C++ el uso de interfaces es útil además para evitar recompilados innecesarios.

Una interface no es más que una clase en la que se definen los métodos públicos necesarios, pero no se implementan. Luego la clase concreta que queramos hacer hereda de esa interface e implementa sus métodos.

En nuestro caso, podemos hacer una clase InterfaceFecha, con los métodos públicos virtuales puros (sin código). Luego la clase Fecha hereda de InterfaceFecha e implementa esos métodos.

En el fichero InterfaceFecha.h tendríamos

class InterfaceFecha
{
   public:
      virtual void tomaFecha (int anho, int mes, int dia) = 0;
      virtual int dameAnho () = 0;
      virtual int dameMes () = 0;
      virtual int dameDia () = 0;
      virtual void metodoMaravilloso1() = 0;
      virtual void metodoMaravilloso2() = 0;
};

De momento, ni siquiera existiría un InterfaceFecha.cc

La clase Fecha sigue igual, pero hereda de InterfaceFecha.

#include <InterfaceFecha.h>

class Fecha : public InterfaceFecha
{
   public:
      void tomaFecha (int anho, int mes, int dia);
      int dameAnho ();
      int dameMes ();
      int dameDia ();
      void metodoMaravilloso1();
      void metodoMaravilloso2();
   protected:
      int anho; // El anho con cuatro cifras, ej. 2004
      int mes;  // El mes, de 1 a 12
      int dia;    // El dia, de 1 a 31
};

Ahora, todo el que necesite una Fecha, tiene que tener un puntero a InterfaceFecha en vez de a Fecha. Alguien instanciará Fecha y lo guardará en ese puntero. Es decir, podríamos hacer algo como esto

#include <Fecha.h>
#include <InterfaceFecha.>
...
InterfaceFecha *unaFecha = NULL;
...
unaFecha = new Fecha();
unaFecha->tomaFecha (2004, 1, 27);
...
delete unaFecha;
unaFecha = NULL;

Si nos fijamos un poco, todavía no hemos arreglado nada, salvo complicar el asunto. El que haga este código necesita hacer ahora #include tanto de InterfaceFecha.h como de Fecha.h. Si tocamos algo en Fecha.h, este código se recompilará.

Este código necesita #include <Fecha.h> para poder hacer el new de Fecha. Hay que buscar la forma de evitar ese new. Suele ser también bastante habitual hacer una clase (o utilizar la misma Interface si el lenguaje lo permite, como es el caso de C++) para poner un método estático que haga el new y nos lo devuelva.

En el caso de Java, al poner este método, ya no tendríamos una interface, sino una clase. Hacer que Fecha herede de InterfaceFecha nos limita a no heredar de otra cosa (Java no admite herencia múltiple). Si esto es admisible, podemos hacerlo así. Si necesitamos que Fecha herede de otra clase, en vez de poner el método estático en la interface, debemos hacer una tercera clase aparte GeneradorFecha con este método estático.

En nuestro ejemplo de C++, la clase InterfaceFecha quedaría.

class InterfaceFecha
{
   public:

      static InterfaceFecha *dameNuevaFecha();

      virtual void tomaFecha (int anho, int mes, int dia) = 0;
      virtual int dameAnho () = 0;
      virtual int dameMes () = 0;
      virtual int dameDia () = 0;
      virtual void metodoMaravilloso1() = 0;
      virtual void metodoMaravilloso2() = 0;
};

Ahora sí necesitamos un InterfaceFecha.cc. Dentro de él tendriamos

#include <InterfaceFecha.h>
#include <Fecha.h>

InterfaceFecha *InterfaceFecha::dameNuevaFecha()
{
   return new Fecha();
}

El código que antes utilizaba el puntero a InterfaceFecha quedaría ahora

#include <InterfaceFecha.>
...
InterfaceFecha *unaFecha = NULL;
...
unaFecha = InterfaceFecha::dameNuevaFecha();
unaFecha->tomaFecha (2004, 1, 27);
...
delete unaFecha;
unaFecha = NULL;

Como vemos, sólo es necesario el #include de InterfaceFecha.h y este no incluye a Fecha.h (lo hace InterfaceFecha.cc, no el .h). Hemos hecho que este código no vea en absoluto a Fecha.h. Ahora podemos tocar sin ningún miramiento el fichero Fecha.h, que este código no necesita ser recompilado.

Una ventaja adicional es que se puede cambiar la clase Fecha por otra clase Fecha2 en tiempo de ejecución. Bastaría con poner un atributo estático en InterfaceFecha para indicar qué clase Fecha queremos y hacer que el método dameNuevaFecha() instancie y devuelve una u otra en función de ese atributo.

CONSEJO:
Utilizar interfaces para aquellas clases que preveamos que
pueden cambiar durante el desarrollo del proyecto o
que creamos que podemos cambiar más adelante por otra.

Este mecanismo de obtener una instancia de una clase a través de un método estático y de una interface, para no depender de la clase concreta, creo que dentro del mundo de los patrones de diseño es el patrón Factoria.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007: