8.2. Herencia de interfaces

Un uso no controvertido de la herencia múltiple es la herencia de interfaz. En C++, toda herencia lo es de implementación, dado que todo en una clase base, interface e implentación, pasa a formar parte de la clase derivada. No es posible heredar solo una parte de una clase (es decir, la interface únicamente). Tal como se explica en el [FIXME:enlace en la versión web] Capítulo 14 del volumen 1, es posible hacer herencia privada y protegida para restringir el acceso a los miembros heredados desde las clases base cuando se usa por clientes de instancias de una clase derivada, pero esto no afecta a la propia clase derivada; esa clase sigue conteniendo todos los datos de la clase base y puede acceder a todos los miembros no-privados de la clase base.

La herencia de interfaces. por otra parte, sólo añade declaraciones de miembros a la interfaz de la clase derivada, algo que no está soportado directamente en C++. La técnica habitual para simular la herencia de interfaz en C++ es derivar de una clase interfaz, que es una clase que sólo contiene declaraciones (ni datos ni cuerpos de funciones). Estas declaraciones serán funciones virtuales puras, excepto el destructor. Aquí hay un ejemplo:

//: C09:Interfaces.cpp
// Multiple interface inheritance.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;

class Printable {
public:
  virtual ~Printable() {}
  virtual void print(ostream&) const = 0;
};

class Intable {
public:
  virtual ~Intable() {}
  virtual int toInt() const = 0;
};

class Stringable {
public:
  virtual ~Stringable() {}
  virtual string toString() const = 0;
};

class Able : public Printable, public Intable,
             public Stringable {
  int myData;
public:
  Able(int x) { myData = x; }
  void print(ostream& os) const { os << myData; }
  int toInt() const { return myData; }
  string toString() const {
    ostringstream os;
    os << myData;
    return os.str();
  }
};

void testPrintable(const Printable& p) {
  p.print(cout);
  cout << endl;
}

void testIntable(const Intable& n) {
  cout << n.toInt() + 1 << endl;
}

void testStringable(const Stringable& s) {
  cout << s.toString() + "th" << endl;
}

int main() {
  Able a(7);
  testPrintable(a);
  testIntable(a);
  testStringable(a);
} ///:~

Listado 8.1. C09/Interfaces.cpp


La clase Able «implementa» las interfaces Printable, Intable y Stringable dado que proporciona implementaciones para las funciones que éstas declaran. Dado que Able deriva de las tres clases, los objetos Able tienen múltiples relaciones «es-un». Por ejemplo, el objeto a puede actuar como un objeto Printable dado que su clase, Able, deriva públicamente de Printable y proporciona una implementación para print(). Las funciones de prueba no necesitan saber el tipo más derivado de su parámetro; sólo necesitan un objeto que sea substituible por el tipo de su parámetro.

Como es habitual, una plantilla es una solución más compacta:

//: C09:Interfaces2.cpp
// Implicit interface inheritance via templates.
#include <iostream>
#include <sstream>
#include <string>
using namespace std;

class Able {
  int myData;
public:
  Able(int x) { myData = x; }
  void print(ostream& os) const { os << myData; }
  int toInt() const { return myData; }
  string toString() const {
    ostringstream os;
    os << myData;
    return os.str();
  }
};

template<class Printable>
void testPrintable(const Printable& p) {
  p.print(cout);
  cout << endl;
}

template<class Intable>
void testIntable(const Intable& n) {
  cout << n.toInt() + 1 << endl;
}

template<class Stringable>
void testStringable(const Stringable& s) {
  cout << s.toString() + "th" << endl;
}

int main() {
  Able a(7);
  testPrintable(a);
  testIntable(a);
  testStringable(a);
} ///:~

Listado 8.2. C09/Interfaces2.cpp


Los nombres Printable, Intable y Stringable ahora no son mas que parámetros de la plantilla que asume la existencia de las operaciones indicadas en sus respectivos argumentos. En otras palabras, las funciones de prueba pueden aceptar argumentos de cualquier tipo que proporciona una definición de método con la signatura y tipo de retorno correctos. Hay gente que encuentra más cómoda la primera versión porque los nombres de tipo garantizan que las interfaces esperadas están implementadas. Otros están contentos con el hecho de que si las operaciones requeridas por las funciones de prueba no se satisfacen por los argumentos de la plantilla, el error puede ser capturado en la compilación. Esta segunda es una forma de comprobación de tipos técnicamente más débil que el primer enfoque (herencia), pero el efecto para el programador (y el programa) es el mismo. Se trata de una forma de comprobación débil de tipo que es aceptable para muchos de los programadores C++ de hoy en día.