C++ Classes V

[forCode]

El concepto de encapsulación en orientación a objetos hace referencia a que la clase contiene tanto los datos (variables) como el código encargado de operar con estos datos (funciones). Pero esto serviría de poco si no se puede garantizar que dichos datos no podrán ser accedidos desde ningún otro lugar. Para ello en orientación a objetos hablamos de niveles de visibilidad.

Visibilidades en C++

C++ define tres niveles de visibilidad:

  • Público (public):Una variable pública podrá ser accedida desde cualquier función perteneciente a cualquier clase (incluídas funciones globales que no pertenecen a ninguna clase). Una función pública podrá ser invocada desde cualquier función perteneciente a cualquier clase (incluídas funciones globales).
  • Protegido (protected): Una variable protegida solo podrá ser accedida desde cualquier función que pertenezca a la propia clase que declara la variable o bien desde cualquier función que esté en cualquier clase derivada. Una función protegida podrá ser invocada por cualquier función que pertenezca a la propia clase o a cualquier clase derivada,
  • Privado (private): Una variable privada solo podrá ser accedida desde cualquier función que pertenezca a la propia clase. Una función privada podrá ser llamada sólo desde cualquier otra función de la misma clase.

La  norma que debemos seguir siempre es usar el nivel de visibilidad más restrictivo posible. Si algo puede ser privado, no lo hagas protegido. Si algo puede ser protegido, no lo hagas público.

Variables públicas… ¿sí o no?

En lenguajes como C# está casi universalmente extendida la práctica de no hacer nunca ninguna variable pública. Todas las variables deben ser privadas (pese a que C# no obliga a ello) y el acceso a dichas variables se debe realizar mediante propiedades. Una propiedad es un par de métodos get/set que permiten leer o establecer el valor de una variable. Lo que diferencia una propiedad de un par de métodos get/set tradicionales es que las propiedades se invocan con la misma sintaxis que usaríamos para acceder a una variable. Es decir, nos parece que accedemos a una variable pública, pero realmente accedemos a un método que nos devuelve el valor de dicha propiedad. Otros lenguajes como Swift van más allá y obligan al uso de propiedades.

C++, al igual que Java, no tiene soporte para propiedades y eso hace que el debate «variables públicas si o no» sea más encendido. En Java se opta mucho por las variables privadas y un par de métodos get/set porque existe la convención de Java beans que obliga a ello. Pero en C++ no hay una convención específica que se haya adoptado y te vas a encontrar de todo. Partamos de la idea de que, por lo general, las variables públicas rompen la encapsulación. Tu clase expone una variable que se puede establecer en cualquier momento y a cualquier valor. No puedes validar valores de dicha variable, no puedes enterarte cuando se ha cambiado dicha variable (y realizar acciones adicionales). Pero si eso es correcto, entonces no hay problema en usar variables públicas. Eso sí: es una decisión sin marcha atrás. Una vez empiezas a usar una variable pública ya no puedes «echarte atrás» y meter un par de métodos get/set en su lugar (sin romper código existente, se entiende).

Mucha gente opta por usar, siempre, métodos get/set:

class Beer
{
	std::string _name;
	float _abv;
public:
	std::string getName();
	void setName(std::string);
	float getAbv();
	void setAbv(float);
};
// Fichero cpp
std::string Beer::getName() {
	return _name;
}
void Beer::setName(std::string newName) {
	_name = newName;
}
float Beer::getAbv() {
	return _abv;
}
void Beer::setAbv(float newValue) {
	_abv = newValue;
}

El uso es muy sencillo:

auto beer = Beer();
beer.setName("Punk IPA");
beer.setAbv(4.5f);
std::cout << "Beer Name is " << beer.getName();

Este mecanismo es muy seguro en el sentido de que si en un «futuro» se requiere colocar código de validación (como p. ej. que el valor de Abv no sea negativo) puede hacerse en el método Beer::setAbv(). Por supuesto el precio a pagar es multitud de métodos get/set. A ti te toca valorar si vale la pena pagar este precio o no.

Si vienes de Java, seguramente te sentirás cómodo con esa nomenclatura. Pero que los métodos se llamen getXXX y setXXX a mi, no me convence para nada en absoluto. Lo encuentro muy «forzado». Prefiero tener algo como el estilo:

class Beer
{
	std::string _name;
	float _abv;
public:
	std::string name();
	void name(std::string);
	float abv();
	void abv(float);
};
// Fichero cpp
std::string Beer::name() {
	return _name;
}
void Beer::name(std::string newName) {
	_name = newName;
}
float Beer::abv() {
	return _abv;
}
void Beer::abv(float newAbv) {
	_abv = newAbv;
}

Observa que hemos declarado dos métodos llamados abv y dos métodos llamados name. Eso se llama sobrecarga. C++ permite la sobrecarga de métodos, es decir declarar dos o más métodos con el mismo nombre pero distinto número o tipo de parámetros. El tipo de retorno no se tiene en cuenta, por lo que no se puede declarar dos métodos con mismo nombre, mismos argumentos y que se diferencien solo en el tipo del valor de retorno.

El uso entonces queda como sigue:

auto beer = Beer();
beer.name("Punk IPA");
beer.abv(4.2);
std::cout << "Beer name is " << beer.name();

A mi me parece mucho más natural, y de hecho la STL (Standard Template Library, una de las librerías que forman parte del estándard de C++) sigue ese patrón.

Pero, amigos, ya sabéis que estamos en C++ y lo bonito de este lenguaje es que las cosas se pueden hacer de varias maneras. Hay otra alternativa al uso de un par de métodos get/set que es el uso de un método que devuelva una referencia a la variable privada:

class Beer
{
	std::string _name;
	float _abv;
public:
	float& abv();
	std::string& name();
};
// Fichero cpp
std::string& Beer::name() {
	return _name;
}
float& Beer::abv() {
	return _abv;
}

Ahora el uso quedaría tal y como sigue:

auto beer = Beer();
beer.name() = "Punk IPA";
beer.abv() = 4.2;
std::cout << "Beer name is " << beer.name();

De esta manera el método actúa como un getter pero al obtener una referencia podemos modificar el valor de la variable. Con este método puedes verificar condiciones antes de devolver la referencia, pero no puedes realizar validaciones del valor que haya establecido el usuario. En según que condiciones es interesante, y no deja de ser otra alternativa.

Bueno… lo dejamos aquí en este post. En el siguiente post veremos asignación y copia de objetos así como el tema de objetos constantes…