Documentos de Académico
Documentos de Profesional
Documentos de Cultura
SESIÓN N° 08:
I
OBJETIVOS
II
TEMAS A TRATAR
❖ Introducción.
❖ Clases abstractas
❖ ¿Qué es una clase Completamente Abstracta?
❖ ¿Qué es un método virtual?
❖ Añadir, Reemplazar y Refinar
❖ Resumen
III
MARCO TEORICO
1. INTRODUCCIÓN
En la programación orientada a objetos (POO), existen varios conceptos clave que nos
permiten diseñar sistemas más flexibles, modulares y fáciles de mantener. En este capítulo,
exploraremos cuatro de estos conceptos fundamentales: Clases Abstractas, Interfaces,
Funciones Virtuales y Remplazo/Refinamiento.
Comenzaremos por comprender qué son las Clases Abstractas y las Interfaces, y cómo se
utilizan para establecer una base común en la jerarquía de clases y definir contratos para
las clases derivadas. Luego, nos adentraremos en el mundo de las Funciones Virtuales y
cómo permiten el polimorfismo, brindando la capacidad de redefinir comportamientos en las
clases hijas. Por último, exploraremos los conceptos de Remplazo y Refinamiento, y cómo
nos ayudan a mantener la coherencia y flexibilidad en la evolución de nuestros sistemas.
A lo largo de este capítulo, analizaremos ejemplos prácticos y estudiaremos cómo estos
conceptos se aplican en diferentes lenguajes de programación orientados a objetos, como
C++, Java, Python y JavaScript. Veremos cómo cada uno de estos conceptos se utiliza para
resolver problemas del mundo real y cómo nos permiten escribir código más modular,
reutilizable y mantenible.
2. CLASES ABSTRACTAS
Una clase abstracta es la que no instancia objetos, para tal caso se debe evitar que
funcionalmente una clase así no pueda hacerlo, esto se consigue creando métodos virtuales
puros, para convertir a un método en un método virtual puro se le iguala a cero en la
declaración, esto hace que no pueda ser definida en la clase donde se declara,
El concepto de métodos virtuales está asociado al polimorfismo, las clases abstractas
también están asociadas a este concepto.
#ifndef UNACLASEABSTRACTA_H
#define UNACLASEABSTRACTA_H
class UnaClaseAbstracta
{
public:
UnaClaseAbstracta(int,char,float);
void ingreso();
void salida();
// método virtual puro
virtual void metodoVirtualPuro() = 0;
int getAtributoEntero();
float getAtributoFloat();
char getAtributoChar();
private:
int atributoEntero;
char atributochar;
float atributoFloat;
};
#endif
#include "unaclaseabstracta.h"
#include <iostream>
using namespace std;
UnaClaseAbstracta::UnaClaseAbstracta(int a, char b, float c)
{
atributoEntero = a;
atributochar = b;
atributoFloat = c;
}
int UnaClaseAbstracta::getAtributoEntero()
{
return atributoEntero;
}
float UnaClaseAbstracta::getAtributoFloat()
{
return atributoFloat;
}
char UnaClaseAbstracta::getAtributoChar()
{
return atributochar;
}
void UnaClaseAbstracta::ingreso()
{
cout << "Ingreso de objeto de una clase: " << endl;
cout << "atributoEntero = ";
La clase UnaClaseAbstracta, abstracta ahora, las clases que deriven de ella deben
implementar el método virtual puro: virtual void metodoVirtualPuro() = 0.
#include "unaclaseabstracta.h"
class UnaClaseConcreta : public UnaClaseAbstracta
{
public:
UnaClaseConcreta(int = 0, char = ' ', float = 0);
void ingreso();
void salida();
// El método virtual puro debe ser definido aqui
void metodoVirtualPuro();
private:
int atributoEnteroDerivada;
char atributocharDerivada;
float atributoFloatDerivada;
};
#endif
A. REGLAS DE USO:
Las clases abstractas se usan en el contexto siguiente:
▪ Una clase abstracta no puede ser utilizada como argumento o como retorno de una
función.
▪ Al no instanciar objetos tampoc puede ser usada como argumento ni valor de retorno
de un método o función.
▪ Se pueden declarr punteros ya que puede apuntar a objetos de clases derivadas.
▪ Tambié se pueden crear instancias si el objeto que inicializa la referencia no es
necesario.
UnaClaseConcreta objUnaClaseConcreta(1,'a',3);
UnaClaseConcreta *ptrUnaClaseConcreta = new UnaClaseConcreta(2,'b',6);
// Se pueden crear punteros del tipo de clase abstracta
UnaClaseAbstracta *ptrObjUnaClaseAbstracta;
objUnaClaseConcreta.metodoVirtualPuro();
ptrUnaClaseConcreta -> metodoVirtualPuro();
system("pause");
return 0;
}
Si una clase derivada de una clase abstracta no implementa todos los métodos virtuales
puros definidos en las clases de donde deriva esta también se vuelve abstracta.
Las funciones-miembro pueden ser llamadas desde el constructor de una clase abstracta,
pero la llamada directa o indirecta de una función virtual pura desde tal constructor puede
provocar un error en tiempo de ejecución. Sin embargo, son permitidas disposiciones como
la siguiente:
#ifndef UNACLASE_H
#define UNACLASE_H
class UnaClase
{
public:
UnaClase(int,char,float);
void ingreso();
void salida();
int getAtributoEntero();
float getAtributoFloat();
char getAtributoChar();
private:
int atributoEntero;
char atributochar;
float atributoFloat;
};
#endif
La clase anterior será usada como clase base convencional, aquí no existen métodos
virtuales puros, por lo que dicha clase no es abstracta. Veamos ahora cómo queda el
archivo "unaclase.cpp":
char UnaClase::getAtributoChar()
{
return atributochar;
}
void UnaClase::ingreso()
{
cout << "Ingreso de objeto de una clase: " << endl;
cout << "atributoEntero = ";
cin >> atributoEntero;
cout << "atributochar = ";
cin >> atributochar;
cout << "atributoFloat = " ;
cin >> atributoFloat;
}
void UnaClase::salida()
{
cout << "Objeto de una clase: " << endl;
cout << "atributoEntero = " << atributoEntero << endl;
cout << "atributochar = " << atributochar << endl;
cout << "atributoFloat = " << atributoFloat << endl;
}
La herencia múltiple hace que la clase resultante reciba características de ambas clases, el
especificador de acceso era public, por lo cual se recibió la herencia de forma pública,
teniendo en cuenta que de la clase UnaInterface solo se reciben los métodos virtuales
puros que tendrán que ser implantados en esta clase o de lo contrario también se volverá
abstracta.
#include "unaclasederivada.h"
#include <iostream>
using namespace std;
UnaClaseDerivada::UnaClaseDerivada(int a, char b, float c) :
atributoEnteroDerivada(a),atributocharDerivada(b),
atributoFloatDerivada(c), UnaClase(a,b,c)
{
}
void UnaClaseDerivada::ingreso()
{
cout << "Ingreso de objeto de una clase derivada: " << endl;
cout << "atributoEnteroDerivada = ";
cin >> atributoEnteroDerivada;
cout << "atributocharDerivada = ";
cin >> atributocharDerivada;
cout << "atributoFloatDerivada = " ;
cin >> atributoFloatDerivada;
UnaClase::ingreso();
}
void UnaClaseDerivada::salida()
{
cout << "Objeto de una clase: " << endl;
cout << "atributoEnteroDerivada = " << atributoEnteroDerivada << endl;
cout << "atributocharDerivada = " << atributocharDerivada << endl;
cout << "atributoFloatDerivada = " << atributoFloatDerivada << endl;
UnaClase::salida();
}
// Definición de los métodos virtuales puros heredados
void UnaClaseDerivada::operacion1()
{
cout << "operacion1: " << atributoEnteroDerivada + getAtributoEntero()
<< endl;
}
void UnaClaseDerivada::operacion2()
{
cout << "operacion2: " << getAtributoFloat() + atributoFloatDerivada
<< endl;
}
void UnaClaseDerivada::operacion3()
{
cout << "operacion3: " << atributocharDerivada << getAtributoChar()
<< endl;
}
#include <iostream>
using namespace std;
#include "unaclasederivada.h"
#include "unaclase.h"
int main()
{
UnaClaseDerivada *ptrUnaClaseDerivada = new UnaClaseDerivada(2,'b',6);
UnaClaseDerivada objUnaClaseDerivada(1,'a',3);
objUnaClaseDerivada.operacion1();
objUnaClaseDerivada.operacion2();
objUnaClaseDerivada.operacion3();
ptrUnaClaseDerivada -> operacion1();
ptrUnaClaseDerivada -> operacion2();
ptrUnaClaseDerivada -> operacion3();
system("pause");
return 0;
}
#ifndef CLASEBASE_H
#define CLASEBASE_H
class ClaseBase
{
public:
ClaseBase(int i) : atributoEntero(i)
{
}
// Métodos virtuales
virtual void metodo1();
virtual int metodo2();
virtual int metodo3(int);
virtual void metodo4(int);
private:
int atributoEntero;
};
#endif
#include "clasebase.h"
#include <iostream>
using namespace std;
void ClaseBase::metodo1()
{
cout << "metodo vitual ClaseBase::metodo1(). . ." << endl;
}
int ClaseBase::metodo2()
{
cout << "metodo vitual ClaseBase::metodo2(). . ." << endl;
return 0;
}
int ClaseBase::metodo3(int a)
{
cout << "metodo vitual ClaseBase::metodo3(). . ." << endl;
return 0;
}
void ClaseBase::metodo4(int a)
{
cout << "metodo vitual ClaseBase::metodo4(). . ." << endl;
};
#include <iostream>
using namespace std;
#include "clasebase.h"
#include "clasederivada.h"
int main()
{
ClaseBase objClaseBase(0);
ClaseBase* ptrClaseBase = new ClaseBase(5);
ClaseDerivada objptrClaseDerivada(3);
ClaseDerivada *ptrClaseDerivada = new ClaseDerivada(6);
objClaseBase.metodo1();
ptrClaseBase -> metodo1();
objptrClaseDerivada.metodo1();
objptrClaseDerivada.metodo2();
objptrClaseDerivada.ClaseBase::metodo1();
ptrClaseDerivada -> metodo1();
ptrClaseDerivada -> ClaseBase::metodo1();
system("pause");
return 0;
}
Se puede observar que el mismo método metodo1() es ejecutado por dos objetos diferentes
en las llamadas: objClaseBase.metodo1() y
objptrClaseDerivada.ClaseBase::metodo1(), en cambio, en
objptrClaseDerivada.metodo1() y objptrClaseDerivada.metodo2(), el mismo
objeto ejecuta métodos diferentes.
#include <iostream>
using namespace std;
#include "clasebase.h"
#include "clasederivada.h"
int main()
{
ClaseBase *ptrClaseBase;
ClaseDerivada objClaseDerivada(3);
ClaseDerivada *ptrClaseDerivada = new ClaseDerivada(6);
objClaseDerivada.metodo1();
ptrClaseDerivada -> metodo1();
ptrClaseBase -> metodo1();
refClaseBase.metodo1();
system("pause");
return 0;
}
#ifndef OTRACLASEBASE_H
#define OTRACLASEBASE_H
class OtraClaseBase
{
public:
OtraClaseBase(int i) : atributoEntero(i)
{
}
private:
int atributoEntero;
};
#endif
Derivemos de la anterior:
#ifndef OTRACLASEDERIVADA_H
#define OTRACLASEDERIVADA_H
#include "otraclasebase.h"
class OtraClaseDerivada : public OtraClaseBase
{
public:
OtraClaseDerivada(int a) : atributoEntero(a), OtraClaseBase(a)
{
}
private:
int atributoEntero;
};
#endif
Ahora crearemos dos clases en otra jerarquía aparte para probar lo anterior, estas tendrán
métodos virtuales.
#ifndef CLASEBASE_H
#define CLASEBASE_H
#include "otraclasebase.h"
class ClaseBase
{
public:
ClaseBase(int i) : atributoEntero(i)
{
}
virtual void metodo1();
virtual int metodo2();
virtual int metodo3(int);
virtual void metodo4(int);
virtual OtraClaseBase* metodo5();
virtual OtraClaseBase& metodo6();
private:
int atributoEntero;
};
#endif
#include "clasebase.h"
#include <iostream>
using namespace std;
void ClaseBase::metodo1()
{
cout << "metodo vitual ClaseBase::metodo1(). . ." << endl;
}
int ClaseBase::metodo2()
{
cout << "metodo vitual ClaseBase::metodo2(). . ." << endl;
return 0;
}
int ClaseBase::metodo3(int a)
{
cout << "metodo vitual ClaseBase::metodo3(). . ." << endl;
return 0;
}
void ClaseBase::metodo4(int a)
{
cout << "metodo vitual ClaseBase::metodo4(). . ." << endl;
};
OtraClaseBase* ClaseBase::metodo5()
{
OtraClaseBase objOtraClaseBase(5);
OtraClaseBase* ptrOtraClaseBase = &objOtraClaseBase;
#ifndef CLASEDERIVADA_H
#define CLASEDERIVADA_H
#include "clasebase.h"
#include "otraclasederivada.h"
class ClaseDerivada : public ClaseBase
{
public:
ClaseDerivada(int a) : atributoEntero(a), ClaseBase(a)
{
}
void metodo1();
int metodo2();
int metodo3(int);
void metodo4(int);
OtraClaseDerivada* metodo5();
OtraClaseDerivada& metodo6();
private:
int atributoEntero;
};
#endif
#include "clasederivada.h"
#include <iostream>
using namespace std;
void ClaseDerivada::metodo1()
{
cout << "metodo vitual ClaseDerivada::metodo1(). . ." << endl;
}
int ClaseDerivada::metodo2()
{
cout << "metodo vitual ClaseDerivada::metodo2(). . ." << endl;
return 0;
}
int ClaseDerivada::metodo3(int a)
{
cout << "metodo vitual ClaseDerivada::metodo3(). . ." << endl;
return 0;
}
void ClaseDerivada::metodo4(int a)
{
cout << "metodo vitual ClaseDerivada::metodo4(). . ." << endl;
};
OtraClaseDerivada* ClaseDerivada::metodo5()
{
OtraClaseDerivada objOtraClaseDerivada(5);
OtraClaseDerivada* ptrOtraClaseDerivada = &objOtraClaseDerivada;
cout << "metodo vitual ClaseDerivada::metodo5(). . ." << endl;
return ptrOtraClaseDerivada;
}
OtraClaseDerivada& ClaseDerivada::metodo6()
{
OtraClaseDerivada objOtraClaseDerivada(2);
OtraClaseDerivada& refOtraClaseDerivada = objOtraClaseDerivada;
cout << "metodo vitual ClaseDerivada::metodo6(). . ." << endl;
return refOtraClaseDerivada;
}
#include <iostream>
using namespace std;
#include "clasebase.h"
#include "clasederivada.h"
#include "otraclasebase.h"
#include "otraclasederivada.h"
int main()
{
ClaseBase objClaseBase(0);
ClaseBase* ptrClaseBase = new ClaseBase(5);
ClaseDerivada objptrClaseDerivada(3);
ClaseDerivada *ptrClaseDerivada = new ClaseDerivada(6);
objClaseBase.metodo1();
objClaseBase.metodo2();
objClaseBase.metodo5();
objClaseBase.metodo6();
ptrClaseBase -> metodo1();
ptrClaseBase -> metodo2();
ptrClaseBase -> metodo5();
ptrClaseBase -> metodo6();
objptrClaseDerivada.metodo1();
objptrClaseDerivada.metodo2();
objptrClaseDerivada.metodo5();
objptrClaseDerivada.metodo6();
ptrClaseDerivada -> metodo1();
ptrClaseDerivada -> metodo2();
ptrClaseDerivada -> metodo5();
ptrClaseDerivada -> metodo6();
system("pause");
return 0;
}
#ifndef CENTERO_H
#define CENTERO_H
#include "cnumero.h"
CEntero::CEntero()
{
valor = 0;
}
CEntero::CEntero(int numero)
{
valor = numero;
}
CEntero* CEntero::sumar(CEntero* A,CEntero* B)
{
valor = A->valor + B->valor;
return this;
}
CEntero* CEntero::restar(CEntero* A,CEntero* B)
{
this->valor = A->valor - B->valor;
return this;
}
void CEntero::escribir()
{
cout << "Entero: " << valor << endl;
}
Para hacer una jerarquía de clase más interesante hacemos una segunda derivación de
CNumero:
float valor;
};
#endif
Es claro que un método de una clase derivada como CEntero que tiene métodos con el
mismo nombre que los métodos de su clase base anulan los métodos de la clase
antecesora. Durante el proceso de búsqueda de un método que se invoca como respuesta
a un mensaje, se encontrará lógicamente al método de la clase derivada antes que al
método de la clase base.
A. REEMPLAZO
Cuando se llevan a cabo implantaciones de reemplazo y refinamiento se choca con la
dificultad de poder preservar las características del tipo de relación es-un. Esto es, al anular
un método, generalmente no se tiene garantía alguna de que el comportamiento exhibido
por la clase derivada tendrá alguna relación con el de la clase base.
La anulación en C++ se complica por la aplicación concurrente de los conceptos de
anulación, sobrecarga, funciones virtuales (o polimórficas) y constructores.
Los remplazos simples ocurren cuando los argumentos del método de la clase derivada se
acoplan en forma idéntica al tipo y número al método de la clase base y el modificador
virtual se usa en la declaración del método de la misma.
No es necesario incluir virtual en las clases derivadas, probaremos con el código siguiente:
#include <iostream>
using namespace std;
#include "cnumero.h"
#include "centero.h"
#include "cflotante.h"
int main()
{
CNumero *objPtr,objB(3.00),objC(1.00),objR;
objPtr =&objR;
CEntero *objPtrEntero, objEnteroB(5), objEnteroC(2);
objPtr = new CEntero;
CFlotante objPtrFlotante, objFlotanteB(5.1), objFlotanteC(2.6);
objPtr = &objPtrFlotante;
objPtr -> sumar(0.75,3.9);
objPtr -> escribir();
El resultado muestra ahora todo como 0, ¿Qué fue lo que sucedió?, es simple el único
método virtualizado es escribir(), debemos tener en cuenta que cuando se hace el
ocultamiento se debe tener las mismas firmas para los métodos, los métodos presentados
están en una suerte de sobrecarga, pero debido a que el puntero que apunta al objeto es
de la clase base entonces después de buscar un método adecuado se acaba buscando al
método dela clase superior en la jerarquía. Para arreglar esto agregaremos en la clase base
el método siguiente:
Como los métodos en ambas clases tienen la misma firma entonces se hace la llamada en
función al objeto que envía el mensaje y no en función de la lista de parámetros. Para el
caso del método escribir(), siempre se llamo en función del objeto apuntado ya que la
firma del método en las tres clases era la misma.
B. REFINAMIENTO
Un método es anulado por una clase derivada con la misma frecuencia tanto para añadir
una mayor funcionalidad como para sustituirla por una funcionalidad nueva. La adición de
la nueva funcionalidad se podría lograr con sólo copiar el código anulado de la clase base.
Sin embargo, semejante enfoque viola algunos de los principios importantes del diseño OO;
por ejemplo, reduce el compartimiento de código, bloquea la ocultación de información y
reduce la confiabilidad debido a que los errores corregidos en la clase base pueden no
transmitirse a las clases derivadas. Así resulta útil que haya algún mecanismo desde dentro
de un método anulado que invoque el mismo método y que "reutilice" de este modo el código
del método anulado. Cuando un método invoca el método anulado de la clase base de esta
manera, se dice que el método refina el de la clase base.
Existen dos técnicas para proporcionar la capacidad de encontrar un método de una
superclase: la primera, usada en Object Pascal, simplemente cambia la clase inicial para la
búsqueda del método, de la clase del receptor a la superclase de la clase del método que
se está ejecutando en el momento; la segunda, usada en C++, nombra en forma explícita
la clase de la cual se va a tomar un método, eliminándose por completo el mecanismo de
búsqueda en tiempo de ejecución y permitiendo que la llamada al procedimiento realice el
paso de mensajes. La segunda técnica es menos elegante que la primera, especialmente
si algún método se copia de una clase a otra no relacionada, o más poderosa al poder
designarse los métodos de ancestros no inmediatos (aun si son anulados en clases
intermedias), y los conflictos entre múltiples superclases pueden resolverse con el uso de
la herencia múltiple. C++ usa la técnica de nombres calificados para dar acceso a métodos
de las clases base en métodos anulados. Un nombre calificado se escribe como un nombre
de clase seguido por un par de dos puntos y el nombre de un procedimiento. El nombre
calificado elimina el mecanismo virtual de paso de mensajes y asegura que el método se
tome de la clase nombrada. Modificaremos el método escribir() en la clase CFlotante
para demostrar este principio.
void CFlotante::escribir()
{
CNumero::escribir();
cout << "Flotante: " << valor << endl;
}
Fácilmente se aprecia que el método heredado de la clase base que está oculto se llama
explícitamente y se ejecuta antes que la impresión en el método de la clase derivada, es
decir la funcionalidad heredada se "refina" agregando características a las ya existentes.
6. RESUMEN
En este capítulo, exploramos cuatro conceptos fundamentales en la programación orientada
a objetos: Clases Abstractas, Interfaces, Funciones Virtuales y Remplazo/Refinamiento.
Las Clases Abstractas nos permiten definir una clase base que no puede ser instanciada
directamente, pero que establece una base común para sus clases derivadas. Las
Interfaces, por otro lado, nos permiten definir un conjunto de métodos que deben ser
implementados por las clases que las implementan.
Las Funciones Virtuales nos brindan la capacidad de redefinir comportamientos en las
clases derivadas, lo que nos permite lograr polimorfismo y escribir código más genérico y
flexible. Por último, el Remplazo y Refinamiento nos ayudan a mantener la coherencia y
flexibilidad en la evolución de nuestros sistemas, permitiéndonos reemplazar o mejorar
IV
(La práctica tiene una duración de 4 horas) ACTIVIDADES
V
EJERCICIOS
1. Diseña una jerarquía de clases para una aplicación de dibujo. Crea una clase abstracta
llamada "Figura" con métodos abstractos para dibujar y calcular el área de la figura. Luego,
crea clases derivadas como "Círculo", "Rectángulo" y "Triángulo" que implementen estos
métodos de acuerdo con la fórmula correspondiente para cada figura. Crea instancias de
cada figura y prueba los métodos implementados.
2. Define una interfaz llamada "Reproductor" con métodos para reproducir, pausar y detener
la reproducción de un archivo de audio. Luego, crea una clase llamada "ReproductorMP3"
que implemente esta interfaz. Asegúrate de que los métodos de la clase "ReproductorMP3"
realicen las acciones correspondientes. Por último, crea una instancia de "ReproductorMP3"
y prueba los métodos definidos en la interfaz.
3. Crea una clase base llamada "Empleado" con un método virtual llamado "calcularSalario()"
que devuelve el salario mensual del empleado. Luego, crea dos clases derivadas:
"EmpleadoTiempoCompleto" y "EmpleadoMedioTiempo". En cada clase derivada, redefine
el método "calcularSalario()" para calcular el salario de acuerdo a las políticas
correspondientes. Crea instancias de cada tipo de empleado y muestra su salario llamando
al método correspondiente.
VI
CUESTIONARIO
VII
BIBLIOGRAFIA Y REFERENCIAS
• Deitel, Paul J., Deitel, Harvey M., "Cómo Programar en C++", 6ta Edición, Ed. Pearson
Educación, México 2009.
• Stroustrup, Bjarne, "El Lenguaje de Programación C++", 3ra Edición, Adisson Pearson
Educación S.A., Madrid 2002.
• Eckel, Bruce, "Thinking in C++", 2da Edición, Prentice Hall, 2000.