Diseño de interfaces en C++
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 61 de 104.
Una interfaz en C++ es una clase abstracta donde todas las funciones miembro son virtuales puras. A diferencia de las clases abstractas que pueden contener alguna implementación, las interfaces definen solo qué operaciones deben existir: especifican un contrato sin ningún comportamiento.
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};Esta interfaz Drawable garantiza que cualquier clase que la implemente tendrá los métodos draw() y resize(), pero no dice nada sobre cómo funcionan. Esta separación es poderosa: el código puede depender de la interfaz sin saber nada sobre los tipos concretos.
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};
class Button : public Drawable {
std::string label;
public:
Button(std::string l) : label(l) {}
void draw() override { std::cout << "Drawing button: " << label << std::endl; }
void resize(double factor) override { /* redimensionar botón */ }
};Una clase puede implementar múltiples interfaces, lo que permite diseños flexibles donde los objetos pueden cumplir diferentes roles:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};Las interfaces promueven el acoplamiento débil: su código depende de abstracciones en lugar de implementaciones concretas, lo que facilita su extensión y prueba.
Desafío
FácilVamos a construir un sistema de gestión de dispositivos que demuestre cómo las interfaces definen contratos para diferentes tipos de dispositivos. Crearás dos interfaces separadas que los dispositivos pueden implementar, luego construirás clases de dispositivos concretas que cumplan con uno o ambos contratos.
Organizarás tu código en tres archivos:
Interfaces.h: Define dos clases abstractas puras (interfaces) que representan diferentes capacidades de dispositivos:Powerable— cualquier dispositivo que pueda encenderse y apagarse:- Un método virtual puro
powerOn() - Un método virtual puro
powerOff() - Un método virtual puro
getPowerStatus()que devuelve unstd::string - Un destructor virtual
Connectable— cualquier dispositivo que pueda conectarse a una red:- Un método virtual puro
connect(const std::string& network) - Un método virtual puro
disconnect() - Un método virtual puro
getConnectionInfo()que devuelve unstd::string - Un destructor virtual
- Un método virtual puro
Devices.h: Implementa clases de dispositivos concretas que utilicen estas interfaces:Lamp— implementa soloPowerable:- Un miembro privado
bool isOn(comienza comofalse) - Un miembro privado
std::string name - Un constructor que recibe el nombre de la lámpara
powerOn()estableceisOna true e imprime:<name>: Light turned onpowerOff()estableceisOna false e imprime:<name>: Light turned offgetPowerStatus()devuelve"ON"o"OFF"según el estado
SmartTV— implementa tantoPowerablecomoConnectable:- Miembros privados:
bool isOn(comienza false),std::string brand,std::string currentNetwork(comienza vacío) - Un constructor que recibe la marca de la TV
powerOn()estableceisOna true e imprime:<brand> TV: Powered onpowerOff()estableceisOna false, limpia la red e imprime:<brand> TV: Powered offgetPowerStatus()devuelve"ON"o"OFF"connect()almacena el nombre de la red e imprime:<brand> TV: Connected to <network>disconnect()limpia la red e imprime:<brand> TV: DisconnectedgetConnectionInfo()devuelve"Connected to <network>"si está conectado, o"Not connected"si está vacío
- Un miembro privado
main.cpp: Lee tres entradas (cada una en una línea separada):- Nombre de la lámpara
- Marca de la TV
- Nombre de la red
Crea una
Lampy unaSmartTV. Demuestra cómo se puede usar la misma interfaz con diferentes dispositivos:Primero, trabaja con ambos dispositivos a través de la interfaz
Powerable. Almacena punteros a ambos en un arreglo dePowerable*, luego recorre el arreglo y llama apowerOn()en cada uno, seguido de la impresión de su estado como:Status: <powerStatus>Imprime una línea en blanco, luego trabaja con la SmartTV a través de la interfaz
Connectable. Crea un punteroConnectable*a tu SmartTV, llama aconnect()con el nombre de la red e imprime:Connection: <connectionInfo>Imprime otra línea en blanco, luego apaga ambos dispositivos a través del arreglo
Powerabley muestra su estado final.
Por ejemplo, con las entradas Desk Lamp, Samsung, y HomeWiFi:
Desk Lamp: Light turned on
Status: ON
Samsung TV: Powered on
Status: ON
Samsung TV: Connected to HomeWiFi
Connection: Connected to HomeWiFi
Desk Lamp: Light turned off
Status: OFF
Samsung TV: Powered off
Status: OFFObserva cómo la SmartTV puede ser tratada como un Powerable o como un Connectable dependiendo de qué puntero de interfaz utilices. Esta flexibilidad es el poder de implementar múltiples interfaces: tu código puede trabajar con cualquier dispositivo que cumpla con el contrato que necesita, sin conocer el tipo concreto.
Hoja de referencia
Una interfaz en C++ es una clase abstracta donde todas las funciones miembro son virtuales puras. Las interfaces definen solo qué operaciones deben existir sin ninguna implementación:
class Drawable {
public:
virtual void draw() = 0;
virtual void resize(double factor) = 0;
virtual ~Drawable() = default;
};Las clases implementan interfaces sobrescribiendo todas las funciones virtuales puras:
class Circle : public Drawable {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() override { std::cout << "Drawing circle" << std::endl; }
void resize(double factor) override { radius *= factor; }
};Una clase puede implementar múltiples interfaces utilizando herencia múltiple:
class Clickable {
public:
virtual void onClick() = 0;
virtual ~Clickable() = default;
};
class IconButton : public Drawable, public Clickable {
public:
void draw() override { std::cout << "Drawing icon" << std::endl; }
void resize(double factor) override { }
void onClick() override { std::cout << "Clicked!" << std::endl; }
};Las interfaces promueven el acoplamiento débil al permitir que el código dependa de abstracciones en lugar de implementaciones concretas.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include "Devices.h"
using namespace std;
int main() {
// Leer entradas
string lampName;
string tvBrand;
string networkName;
getline(cin, lampName);
getline(cin, tvBrand);
getline(cin, networkName);
// TODO: Crear un objeto Lamp y un objeto SmartTV
// TODO: Crear un arreglo de punteros Powerable* que contenga ambos dispositivos
// TODO: Recorrer el arreglo y llamar a powerOn() en cada dispositivo
// Después de cada powerOn(), imprimir: Status: <powerStatus>
// TODO: Imprimir una línea en blanco
// TODO: Crear un puntero Connectable* hacia la SmartTV
// Llamar a connect() con el nombre de la red
// Imprimir: Connection: <connectionInfo>
// TODO: Imprimir una línea en blanco
// TODO: Recorrer el arreglo Powerable y llamar a powerOff() en cada dispositivo
// Después de cada powerOff(), imprimir: Status: <powerStatus>
return 0;
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico