AUG
13
Programacin y Diseo de Software Orientado A Objetos. Tema
4: Correccin y Robustez en C++
El camino hasta aqu nos ha enseado la base terica de la programacin orientada a
objetos, que son los tipos abstractos de datos, y cmo emplear el diseo por contrato, a
partir de una especificacin algebraica o en su ausencia, para implementar
correctamente una clase. Aunque hemos utilizado Java, obviamente no slo se pueden
aplicar en este lenguaje estos conceptos. Otros, como C++ o C#, pueden beneficiarse
igualmente de la adopcin de los mismos.
En este tema, vamos a analizar el caso de C++, partiendo de la base de que no contamos
con una biblioteca que permita poner en prctica el diseo por contrato en este
lenguaje. Vamos a partir de dos clases ya implementadas, la clase Persona que modela a
una persona cualquiera, y la clase Cuenta que modela una cuenta del banco. El cdigo es
el siguiente:
#ifndef PERSONA_H
#define PERSONA_H
#include <string>
#include <iostream>
using namespace std;
class Persona {
public:
Persona(string = "", string = "", string = "", int = 0);
Persona(const Persona&);
virtual ~Persona();
Persona& operator =(const Persona&);
friend ostream& operator <<(ostream& stream, Persona&);
friend istream& operator >>(istream& stream, Persona&);
Persona* clon() const;
int getEdad() const;
string getNif() const;
string getNombreCompleto() const;
private:
string nif, nombre, apellidos;
int edad;
};
#endif /* PERSONA_H */
==================================================
#include "Persona.h"
Persona::Persona(string _nif, string _nombre, string _apellidos,
int _edad) :
nif{_nif}, nombre{_nombre}, apellidos{_apellidos},
edad{_edad} {
}
Persona::Persona(const Persona& orig) {
this->nif = orig.nif;
this->nombre = orig.nombre;
this->apellidos = orig.apellidos;
this->edad = orig.edad;
}
Persona::~Persona() {
}
Persona& Persona::operator =(const Persona& orig) {
this->nif = orig.nif;
this->nombre = orig.nombre;
this->apellidos = orig.apellidos;
this->edad = orig.edad;
return *this;
}
int Persona::getEdad() const {
return this->edad;
}
string Persona::getNif() const {
return this->nif;
}
string Persona::getNombreCompleto() const {
return this->nombre + " " + this->apellidos;
}
Persona* Persona::clon() const {
Persona* p = new Persona(*this);
return p;
}
ostream& operator <<(ostream& stream, Persona& p) {
stream << p.nif << endl;
stream << p.nombre << " " << p.apellidos << endl;
stream << "Edad: " << p.edad << endl;
return stream;
}
istream& operator >>(istream& stream, Persona& p) {
cout << "NIF: ";
stream >> p.nif;
cout << "Nombre: " << endl;
stream.ignore();
getline(stream, p.nombre);
cout << "Apellidos: " << endl;
getline(stream, p.apellidos);
cout << "Edad: ";
stream >> p.edad;
return stream;
}
==================================================
#ifndef CUENTA_H
#define CUENTA_H
#include <string>
#include <iostream>
#include "Persona.h"
using namespace std;
class Cuenta {
public:
Cuenta(string = "", string = "", string = "", int = 0, double =
0.0, string = "");
Cuenta(const Cuenta&);
virtual ~Cuenta();
Cuenta& operator =(const Cuenta&);
friend ostream& operator <<(ostream& ostream, Cuenta& c);
friend istream& operator >>(istream& istream, Cuenta& c);
Cuenta* clon() const;
double getSaldo() const;
Persona* getTitular() const;
bool puedoSacar(double cantidad) const;
void ingreso(double cantidad);
void reintegro(double cantidad);
private:
Persona* titular;
double saldo;
string codigo;
};
#endif /* CUENTA_H */
==================================================
#include "Cuenta.h"
Cuenta::Cuenta(string _nif, string _nombre, string _apellidos, int
_edad, double _saldo, string _codigo) :
saldo{_saldo}, codigo{_codigo} {
this->titular = new Persona(_nif, _nombre, _apellidos, _edad);
}
Cuenta::Cuenta(const Cuenta& orig) {
this->codigo = orig.codigo;
this->saldo = orig.saldo;
this->titular = orig.titular;
}
Cuenta::~Cuenta() {
delete this->titular;
this->titular = nullptr;
}
Cuenta& Cuenta::operator =(const Cuenta& orig) {
this->codigo = orig.codigo;
this->saldo = orig.saldo;
delete this->titular;
this->titular = orig.titular;
return *this;
}
Cuenta* Cuenta::clon() const {
Cuenta* c = new Cuenta(*this);
return c;
}
ostream& operator <<(ostream& stream, Cuenta& c) {
stream << "Titular: " << endl;
stream << *(c.titular) << endl;
stream << "Numero de cuenta: " << c.codigo << endl;
stream << "Saldo: " << c.saldo << endl;
return stream;
}
istream& operator >>(istream& stream, Cuenta& c) {
cout << "Titular: " << endl;
stream >> *(c.titular);
cout << "Codigo: ";
stream >> c.codigo;
cout << "Saldo: ";
stream >> c.saldo;
return stream;
}
void Cuenta::reintegro(double cantidad) {
if (puedoSacar(cantidad)) {
saldo = saldo - cantidad;
}
}
void Cuenta::ingreso(double cantidad) {
saldo = saldo + cantidad;
}
bool Cuenta::puedoSacar(double cantidad) const {
return (saldo >= cantidad);
}
double Cuenta::getSaldo() const {
return this->saldo;
}
Persona* Cuenta::getTitular() const {
return this->titular;
}
Como hemos dicho, en C++ no tenemos una biblioteca que nos permita aplicar el diseo
por contrato, pero el lenguaje presenta la siguiente macro predefinida:
assert(proposicin);
Provoca la terminacin inmediata del programa e imprime el diagnstico si la proposicin
es falsa -o es cero- en tiempo de ejecucin. Es necesario incluir el fichero de
cabecera <cassert>. Este mecanismo es semejante a la instruccin assert de Java.
Por supuesto, ejecutar las aserciones conlleva un coste en tiempo de ejecucin. Por ello,
hay quienes abogan por utilizarlas nicamente durante la fase de depuracin y pruebas
de la aplicacin. De este modo, una vez se considera que el programa est
suficientemente probado, se desactivan las aserciones. Cmo? Existe otra macro
predefinida,NDEBUG, que deshabilita las aserciones, provocando que ya no se ejecuten;
simplemente, hay que definir NDEBUGjusto antes de incluir <cassert>:
#define NDEBUG
#include <cassert>
Tenemos as un modo para activar y desactivar aserciones en distintos puntos del
programa.
Otra manera de definir NDEBUG es hacerlo una sola vez y que afecte al programa entero,
pasndolo como opcin al compilador a travs de las opciones de proyecto del entorno de
desarrollo visual o de la lnea de comandos, como por ejemplo:
mycc -DNDEBUG myfile.cpp
La mayora de los compiladores usan la marca -D para definir nombres de macro. En la
instruccin anterior, habra que sustituir mycc por el nombre correcto del ejecutable del
compilador.
Por el contrario, yo soy de la opinin de que las aserciones han de mantenerse inclusive
cuando la aplicacin est en produccin. Fueron un mecanismo esencial para detectar
errores cuando el sistema se estaba probando y lo siguen siendo una vez el sistema ha
sido puesto en produccin; si una asercin falla en produccin, sin duda existe un
problema mucho mayor que una degradacin en el rendimiento. Tony Hoare lo explic de
una manera muy clara:Desactivar las aserciones en tiempo de ejecucin es similar a un
entusiasta de la navegacin que lleva un chaleco salvavidas mientras entrena en
tierra y luego se deshace de l cuando va al mar.
Cmo se aplican las aserciones en C++? Siguiendo los siguientes criterios.
Las precondiciones de mtodos privados se comprueban mediante aserciones. Las
postcondiciones y los invariantes de clase tambin se verifican mediante aserciones. Pero
las precondiciones de los mtodos pblicos se comprueban explcitamente y, si no se
cumplen, se lanza la excepcin correspondiente.
El mecanismo de excepciones de C++ es semejante a Java pero, sinceramente, ms
confuso. No obstante, la disciplina del programador puede poner las cosas en su sitio. A
diferencia de Java, una excepcin en C++ no tiene por qu ser un objeto de una clase: Se
puede lanzar un entero o una cadena de caracteres, por ejemplo. Naturalmente, no es
una prctica que vayamos a llevar a cabo en un programa orientado a objetos.
La biblioteca estndar de C++ presenta un conjunto de excepciones predefinidas en el
fichero de cabecera<stdexcept> y en el espacio de nombres std. Todas ellas heredan de
la clase std::exception, que est en el fichero de cabecera <exception>, y disponen del
mtodo what() que devuelve una cadena de caracteres con el informe de error.
La clase exception se puede utilizar como raz de una jerarqua de excepciones definida
por el programador. En caso de hacerlo, se debe redefinir el mtodo what() para que
lance el mensaje de error ms conveniente.
Dos de las excepciones que heredan de exception son las
clases runtime_error y logic_error, y de sta ltima a su vez heredan las
clases out_of_range, length_error, invalid_argument y domain_error. Precisamente, para
el control de las precondiciones se lanzan excepciones compatibles con logic_error: O
bien la propia logic_error, alguna de sus subclases, o bien alguna subclase suya creada a
propsito con tal fin.
Para lanzar una excepcin, se utiliza la palabra reservada throw:
throw exception_object;
C++ no obliga a declarar las excepciones pero se puede especificar el conjunto de
excepciones que puede lanzar un mtodo. As, por ejemplo, la siguiente declaracin:
void f (int a) throw (E1, E2);
significa que f puede lanzar excepciones de tipo E1 y E2 pero no otras. Se puede indicar
que un mtodo no lanzar ninguna excepcin:
int f() throw();
Por ltimo, si no se dice nada, significa que el mtodo podra lanzar cualquier excepcin
-incluyendo ninguna-:
int f();
A diferencia de Java, el compilador ignora la declaracin de las excepciones pero, si en
tiempo de ejecucin se intenta lanzar una excepcin que no pertenece al conjunto de las
declaradas, se detecta la violacin y termina la ejecucin del programa. El
compilador tampoco obliga al cdigo cliente a manejar las excepciones que puede lanzar
un mtodo pero, si ocurre una excepcin y el programador no ha definido cmo
manejarla, la excepcin escapara del mtodo.
Lo que s controla el compilador es la concordancia entre las declaraciones de
excepciones de la clase padre e hija, de modo que un mtodo redefinido no puede lanzar
ms excepciones que las especificadas en el mtodo de la clase padre.
C++ emplea bloques try-catch para manejar excepciones. Al igual que en Java, se define
un manejador -catch- por cada excepcin que se espera que pueda lanzar la ejecucin
del bloque de cdigo que se encuentra dentro del try.
try {
...
} catch (exception_class1 object1) {
...
} catch (exception_class2 object2) {
...
}
Las excepciones se pueden pasar al manejador por valor o por referencia. Cuando las
excepciones son objetos -como por otra parte debe ser en nuestros diseos-, se deben
pasar por referencia para asegurar el comportamiento polimrfico. De este modo, un
manejador puede capturar no slo una determinada excepcin sino tambin instancias de
excepciones derivadas de ella.
Cuando ocurre una excepcin, se evalan los tipos definidos en los manejadores y se
ejecuta el que sea compatible.
Se puede definir un manejador para cualquier tipo de excepcin:
catch (...)
Es posible relanzar la misma excepcin que se est manejando dentro de un
bloque catch, tan slo ejecutando
throw;
Es importante tener en cuenta que, si ningn bloque catch captura la excepcin, bien en
el propio mtodo o fuera de l, el resultado ser la terminacin abrupta del programa.
Hecha esta exposicin, veamos una aplicacin prctica.
Consideremos el mtodo ingreso() de la clase Cuenta. Vamos a aadirle una precondicin
y una postcondicin. Por un lado, la precondicin exigir que el argumento no sea
negativo, puesto que no tiene sentido ingresar una cantidad negativa de dinero en una
cuenta. Por otro, la postcondicin se asegurar que efectivamente el saldo de la cuenta
ha sido actualizado, incrementndose en el valor del argumento.
En el fichero Cuenta.h incluimos los siguientes ficheros:
#include <cassert>
#include <stdexcept>
Y en el fichero Cuenta.cpp incluimos la nueva implementacin del mtodo:
void Cuenta::ingreso(double cantidad) throw (invalid_argument) {
if (cantidad < 0) {
throw invalid_argument("no se puede ingresar una cantidad
negativa");
} else {
double oldSaldo = this->saldo;
saldo = saldo + cantidad;
assert(saldo == oldSaldo + cantidad);
}
}
Tambin ser necesario modificar Cuenta.h para que el prototipo del mtodo refleje que
la declaracin de la excepcin:
void ingreso(double cantidad) throw (invalid_argument);
Ahora vamos a fijarnos en el mtodo reintegro(). Sin duda, el mtodo puedoSacar() es
parte de su precondicin, a la cual habra que aadir la condicin de que la cantidad de
dinero a sacar de la cuenta no puede ser negativa. La postcondicin, de nuevo, se
asegura de que efectivamente el saldo de la cuenta ha sido actualizado,
decrementndose en el valor del argumento.
Actualizamos el prototipo en el fichero Cuenta.h:
void reintegro(double cantidad) throw (invalid_argument);
La nueva implementacin del mtodo en Cuenta.cpp es:
void Cuenta::reintegro(double cantidad) throw (invalid_argument) {
if (cantidad < 0) {
throw invalid_argument("no se puede sacar una cantidad
negativa");
} else if (!puedoSacar(cantidad)) {
throw invalid_argument("no hay suficiente saldo para sacar
una cantidad tan grande");
} else {
double oldSaldo = this->saldo;
saldo = saldo - cantidad;
assert(saldo = oldSaldo - cantidad);
}
}
Para finalizar, el siguiente cdigo prueba estos mtodos:
#include "Persona.h"
#include "Cuenta.h"
#include <limits>
int main() {
Cuenta* c = new Cuenta("1234567A", "Jose Maria", "Canales
Romero", 37, 500.0, "246813579");
double Saldo;
try {
Saldo = c->getSaldo();
cout << "Saldo: " << Saldo << endl;
c->ingreso(200);
Saldo = c->getSaldo();
cout << "Saldo: " << Saldo << endl;
c->ingreso(-150);
} catch (invalid_argument& ex) {
cout << ex.what() << endl;
try {
c->reintegro(400);
Saldo = c->getSaldo();
cout << "Saldo: " << Saldo << endl;
c->reintegro(500);
} catch (invalid_argument& ex) {
cout << ex.what() << endl;
Saldo = c->getSaldo();
cout << "Saldo: " << Saldo << endl;
}
}
cin.ignore(numeric_limits<int>::max(), '\n');
cout << "Press ENTER to continue...";
cin.ignore();
return 0;
}