1. Al pasar una clase como parámetro, mejor con referencia
Es muy común ver la gente, en las prácticas, presenta una función del tipo
Código
void func( std::string parametro );
El problema que presenta este código es que se va a crear un string temporal, copia del que se ha pasado como parámetro, y que se eliminará al salir de la función.
La forma natural de evitar este problema es pasar las clases como referencias constantes. De esta forma aseguramos, si se aplican buenas prácticas, que el objeto pasado como parámetro no va a ser modificado dentro de nuestra función:
Código
void func( const std::string& parametro );
Además, su uso dentro de la función no va a cambiar, ya que al ser referencia el acceso a sus miembros se sigue haciendo con el operador '.'.
2. Al pasar un tipo básico como parámetro, evita referencias constantes.
Mucha gente se aprende lo indicado en el primer punto y lo aplica de forma indiscriminada. Esto lleva a encontrarse funciones del tipo:
Código
void func( const bool& parametro );
No hay que olvidar que una referencia es un puntero "encubierto". Punteros, referencias y la inmensa mayoría de tipos básicos ocupan lo mismo en la pila, por lo que no se obtiene ninguna ventaja al pasar un tipo básico como referencia constante.
Además, pasar un tipo como referencia provoca que en la pila se almacene la referencia, lo que provoca que el acceso al parámetro conlleve dos instrucciones en vez de una...
3. Los miembros de una clase, mejor privados
Es costumbre, supongo que por comodidad, el declarar algunos miembros de una clase en la parte pública para ahorrarnos los getters y los setters correspondientes.
Esta práctica es bastante mala, ya que rompe con el principio de encapsulación que es uno de los pilares de la programación orientada a objetos. Además, presenta el problema de que, si en algún momento, una necesidad obliga a implementar un getter y/o un setter, tendremos que reemplazar todas las llamadas correspondientes... y eso no suele ser algo trivial.
Además, utilizar getters y setters no suele provocar problemas de rendimiento, ya que los compiladores son capaces de optimizar el código de forma que el acceso sea tan eficiente como llamar directamente a la variable miembro.
Código
class VersionMala { public: std::string nombre; int edad; }; class VersionBuena { public: std::string GetNombre( ) const; void SetNombre( const std::string nombre ); int GetEdad( ) const; void SetEdad( int edad ); private: std::string nombre; int edad; };
4. Los destructores, preferiblemente virtuales.
Declarar un constructor como virtual permite que ese destructor sea llamado cuando estamos eliminando una instancia de una subclase.
Parece una tontería, pero no es un fallo obvio y descubrir el origen de una laguna de memoria provocada por este motivo puede ser complicada de detectar.
5. Acostúmbrate a usar Forward declarations.
Cuando se compila un código fuente, el compilador necesita "explotar" los includes para poder realizar su tarea correctamente. Explotar los includes significa que ha de sustituir un include por el contenido del archivo al que apunta... y todo esto para cada archivo a compilar.
Hay que tener en cuenta que el proceso de explotar los includes es recursivo... los includes que se encuentran dentro del archivo apuntado por el include también son explotados.
Como es de imaginar, este trabajo incrementa el tiempo de compilación y los recursos de memoria necesarios para compilar un programa o librería.
Hay una forma de reducir esta carga y es mediante las forward declarations.
A continuación se explican los casos en los que se pueden usar forward declarations en vez de includes:
* Cuando el miembro de una clase es un puntero:
Código
class VehiculoImpl; class Vehiculo { private: VehiculoImpl* impl; };
* Clases pasadas como argumento, se pase la clase por valor, referencia o puntero:
Código
class VehiculoImpl; class Persona; class Vehiculo { private: VehiculoImpl* impl; public: void SetConductor( Persona conductor ); };
* Clases retornadas en un método:
Código
class VehiculoImpl; class Persona; class Vehiculo { private: VehiculoImpl* impl; public: void SetConductor( Persona conductor ); Persona GetConductor( ) const; };
*Lo anterior es perfectamente aplicable cuando nos encontramos con namespace:
Código
class VehiculoImpl; namespace prueba { class Persona; } class Vehiculo { private: VehiculoImpl* impl; public: void SetConductor( prueba::Persona conductor ); prueba::Persona GetConductor( ) const; };
Hay una excepción, y se encuentra al tratar con templates... una clase definida por un template no puede aprovecharse de esta característica del lenguaje, mala suerte.
6. Evitar defines, usar enums
Usar enumerados en vez de defines tiene numerosas ventajas:
- Los valores, por defecto, siempre van a ser consecutivos.
- Los valores se mantienen agrupados.
- A la hora de revisar el código, se sabe qué valores están relacionados.
- Se evitan sustituciones inesperadas en el código.
- A partir de C++11 se pueden añadir opciones de tipado fuertes, lo que mejora su usabilidad.
7. Nombra las clases, variables, métodos, miembros y argumentos con sentido.
un código tal que:
Código
class clase { public: void func( int a, int b, std::string c ) { if ( a != this->a && b != this->b && a < b ) { this->a = a; this->b = b; this->c = c; } private: int a; int b; std::string c; };
seguro que es menos claro que:
Código
class Filtro { public: void SetValores( int minimo, int maximo, std::string mensaje ) { if ( minimo != _minimo && maximo != _maximo && minimo < _maximo ) { _minimo = minimo; _maximo = maximo; _mensaje = mensaje; } } private: int _minimo; int _maximo; std::string _mensaje; }
A la larga usar nombres que aporten información sobre la función a cumplir por el elemento en cuestión proporciona una cantidad enorme de beneficios.
8. No tengas miedo a los contenedores
Yo creo que la práctica totalidad de la gente que está aprendiendo a programar no es consciente de la potencia y versatilidad que proporcionan los contenedores de la STL.
Muchos, al venir de una experiencia previa con C, siguen aplicando los mismos mecanismos de arreglos a la hora de trabajar con colecciones de elementos.
Lo que sucede es que los contenedores están específicamente diseñados para trabajar con colecciones. Gracias a ello suponen una herramienta muy útil a la par que potente... incluso si se elige el contenedor adecuado a nuestras necesidades podemos ahorrarnos bastante código.
* vector: Es el contenedor por defecto. Tiene la característica de que el orden de sus elementos permanece inalterado y permite el acceso a sus elementos a través de índices.
* set: Este contenedor almacena sus elementos ordenados y no admite duplicados. Cuando añadimos un elemento nuevo no podemos saber, a priori en qué posición se va a almacenar. No admite el acceso a través de índices.
Una característica poco explotada de este elemento y que creo que a todos nos acaban pidiendo alguna vez en las prácticas es la siguiente: coger una lista de elementos y presentar un listado sin duplicados:
Código
int lista[10] = { 5, 2, 3, 1, 6, 5, 4, 2, 5, 1 }; std::set< int > sinDuplicados( lista, lista + 10 ); for ( auto it = sinDuplicados.begin( ); it != sinDuplicados.end( ); ++it ) std::cout << *it << " "; std::cout << std::endl;
El código anterior sacará por lo siguiente:
Código:
1 2 3 4 5 6
Fácil, no?
* map: Este contenedor permite añadir elementos asociándoles una clave; los registros se almacenan ordenados en base a su clave y no admite dos claves iguales, en esto se parece al set. No admite el acceso por índice, pero si por clave:
Código
std::map< std::string, int > items; items[ "uno" ] = 1; items[ "dos" ] = 1; items[ "tres" ] = 3; items[ "dos" ] = 2; for ( auto it = items.begin( ); it != items.end( ); ++it ) std::cout << it->first << " " << it->second << std::endl;
Resultado:
Código:
dos 2
tres 3
uno 1
* stack: Implementa una pila LIFO.
* queue: Implementa una pila FIFO.
* array: Es similar a un vector en cuanto a que los elementos se almacenan de la misma forma y que admite duplicados. Es de tamaño fijo y dicho tamaño hay que especificarlo al crear el objeto.
9. Olvida los cast de C
A primera vista, puede parecer mucho más cómodo realizar una conversión utilizando los cast propios de C. Sin embargo, este tipo de conversiones pueden llegar a ser peligrosas, ya que no hacen ningún tipo de chequeo previo.
Los cast de C++ proporcionan más seguridad al respecto. Además, también es más sencillo localizar cast propios de C++ por su sintaxis particular.
10. Diseña funciones cortas.
Como norma general, las funciones no deberían tener más de 20 - 30 líneas.
Tener funciones con este tamaño permite tener funciones sencillas, con una traza fácil de seguir y con un mantenimiento bastante sencillo. Además, podrás ver toda la función en la pantalla de una vez, lo que supone una gran ventaja.
Normalmente cuando una función se hace demasiado grande ( y las he llegado a ver de 5.000 líneas y más ) es debido, bien a un mal diseño por parte del programador, bien a que la función está asumiendo más de una responsabilidad. En cualquier caso lo ideal sería revisar esa función y reducir su tamaño.
Esta norma es sobretodo orientativa... pueden darse casos en los que no es recomendable aplicarla... pero ya adelanto que en un buen diseño son una minoría.
11. Evitar miembros privados y estáticos
Código
class POO { public: POO( ); // ... private: static bool _algo; };
Los miembros privados y estáticos no aportan absolutamente nada a la interfaz de una clase en C++... no influyen en el tamaño de la clase, no permiten el acceso a información nueva...
Y no contentos con esto aportan un problema, y es que, al añadir, modificar o eliminar alguno de estos miembros se obliga a recompilar todos los fuentes dependientes de esta clase.
Es más práctico declarar esos elementos en el cpp, ya que así, en caso de sufrir cambios, solo se recompila un archivo.
Para evitar problemas con nombres duplicados, lo recomendable es definirlos dentro de un espacio de nombres anónimo. Esto es:
Código
namespace { bool _algo = false; } POO::POO( ) { _algo = true; }
Al incluir la declaración en un namespace anónimo convertimos ese código en innacesible desde fuera del archivo en el que se encuentra, por lo que no habrá problemas si en otro cpp declaramos otra variable _algo, sea o no del mismo tipo.
Como se puede ver, acceder a la variable estática es algo totalmente trivial y exento de complicaciones.
Este truco se puede aplicar también a métodos que estén definidos como privados y estáticos, lo cual es igualmente aconsejable.
12. Evita a toda costa usar using namespace en las cabeceras.
Quizás por comodidad, la gente se acostumbra a usar esta sintaxis en los archivos de cabecera, así el acceso a las clases incluidas en ese espacio de nombres es más corto y limpio.
El problema es que al incluir un using en un archivo de cabecera automáticamente se propaga a todos los archivos que tengan dependencias de dicha cabecera.
En caso de usar esta característica, añádela únicamente a los cpp, aunque mi consejo personal es no usar "using namespace" como norma general. La razón es que al perder la clase su espacio de nombres se desvirtúa el código. "std::vector" te da mucha más información que "vector" a secas... además evitas colisiones por coincidencia de nombres.
13. Una clase o un método = una responsabilidad
A veces a los programadores se nos empiezan a ocurrir mil ideas que acabamos conjugando en un único sitio, obteniendo como resultado una clase o método gigantesco que más bien parece sacado de la segunda parte de godzilla.
Hay que procurar que cada clase y cada método tenga una única responsabilidad. Esto es, si tenemos una clase "Alumno"... esta clase sólo ha de preocuparse de almacenar los datos de un alumno, única y exclusivamente.
* si necesitamos gestionar una colección de alumnos de forma especial debemos crear una clase tipo "ListaAlumnos"
* Para rellenar los datos de un alumno, el código que interacciona con el usuario / base de datos / socket / ... ha de estar, necesariamente, en cualquier otro sitio.
Tener clases y métodos con una única responsabilidad facilita la comprensión del código y dificulta la aparición de errores.... tener un método que se conecte a un servidor, le pida información, la muestre por pantalla y le pregunte al usuario su nombre no parece que vaya a resultar agradable a la hora de depurarlo.
Definir correctamente las responsabilidades de cada clase y cada método puede costar bastante al principio, pero al final es sobretodo práctica y experiencia... no digo que después sea coser y cantar... siempre hay situaciones en las que la decisión no es clara, pero no por ello nos vamos a rendir a las primeras de cambio.
14. Evita el acoplamiento entre clases
Se dice que dos clases están acopladas cuando resulta imposible trabajar con una de ellas sin tener que depender de la otra. Obviamente siempre va a existir un cierto acople entre las clases... es ley de vida, sin embargo hay que reducir esa dependencia al mínimo y evitar situaciones absurdas.
El problema que crea el acoplamiento es que te limita la versatilidad del código.
A continuación tenemos un ejemplo de acoplamiento.
Código
class Usuario { public: void GuardarUsuario( std::ostream& stream ); };
La clase "Usuario" incluye un método para almacenar en un stream de salida los datos del usuario. ¿Qué sucede si después se decide también volcar esa información a una base de datos? O bien optas por crear un conector de bases de datos que se enlace al ostream o te toca crear un nuevo método para esta nueva tarea... ambas soluciones no son, desde luego, las ideales.
En este caso el acoplamiento se produce porque no se ha respetado lo indicado en el punto anterior acerca de las responsabilidades. Es decir, la clase "Usuario" no solo almacena los datos de un usuario, sino que además se encarga de volcar los datos a un stream...
Da la sensación de que tenemos miedo a crear clases, es como si se nos fuese a ir de las manos. Nada más lejos de la realidad. La programación ha de ser como construir con Lego... tienes fichas pequeñas pero eso no te impide crear estructuras de varias decenas de metros y varias decenas de kilos de peso... en su sencillez radica su potencia... en la programación sucede lo mismo.
15. Evita utilizar const_cast
C++, al igual que C, permite hacer muchas perrerías. Lo siguiente por ejemplo es totalmente válido:
Código
class POO { public: void Func( ) const; private: int _dato; }; void POO::Func( ) const { // instruccion no valida _dato = 5; // este codigo no da error, compila y funciona. POO* ptr = const_cast< POO* >( this ); ptr->_dato = 5; }
Obviamente todos nos podemos imaginar que no es la mejor solución a elegir... para cosas de estas existe el modificador "mutable" o, directamente, darle un par de pensadas al diseño del sistema.
Como norma general, si en una parte del código tenemos un valor o clase constante... dejémoslo así... si resulta que es necesario modificarlo entonces deberíamos quitarle el atributo const, ya que este tipo de códigos complican la lectura del código.
16. Valida SIEMPRE las entradas del usuario
Una gran cantidad, por poner un ejemplo, de portales Web son sensibles a ataques mediante un método conocido como "inyección SQL". Esta vulnerabilidad permite hacer casi cualquier escabechina en el portal.
Otro fallo bastante común, este caso en aplicaciones de escritorio, es el de desbordamiento de buffer... y permite ejecutar código totalmente aleatorio con los mismos privilegios que la aplicación... imagínate si la aplicación tiene privilegios de administrador la que te pueden liar.
Problemas como estos se producen porque un programador "presupone" que el usuario es bueno y siempre va a facilitar la información que se le pide sin intentar tocar las narices. ERROR!!!
Las interfaces de usuario hay que programarlas con la idea en mente de que el usuario va a ser un cab*** despiadado que te va a buscar las cosquillas hasta en el carnet de conducir. Cada entrada a la aplicación, ya sea por teclado, archivo, sockets, ... debe ser validada para evitar problemas.
Por ejemplo, si un dato a pedir es la edad debemos verificar, en primer lugar, que la información facilitada es, efectivamente, numérica... luego ya puede que interese comprobar que está dentro de un rango determinado, pero siempre hemos de asegurar que la información que entra a nuestra aplicación es válida... nos ahorraremos muchos disgustos después.
17. No escribas código en los .h
Puede parecer muy tentador implementar una función tipo getter o setter directamente en la cabecera para ahorrarnos tiempo y código.
El problema que subyace en esta práctica es que si el futuro requiere un cambio en esta función habremos de recompilar todos y cada uno de los archivos dependientes del archivo donde se encuentre... como os podéis imaginar el tiempo empleado puede ser considerable.
En cambio, si el código se encuentra en los cpp solo será necesario recompilar dicho archivo, con el ahorro de tiempo que ello conlleva.
Los compiladores actuales son capaces de hacer optimizaciones que ni nos imaginamos, por lo que no debemos preocuparnos en pensar que poner la implementación en el cpp va a implicar más instrucciones que dejar el código en la cabecera.
Además se consigue una cabecera más limpia... que es lo que normalmente se usa como referencia.
18. Implementa grupos de operadores completos
Si en una clase te ves obligado a implementar, por ejemplo, el operador '>', procura implementar también los operadores '<', '==', '<=' y '>='.
El motivo es que, si te ves obligado a hacer una comparación... no tiene sentido a que te ates a usar una única comparación... puedes encontrarte con situaciones en las que el código sea más cómodo si usas otra comparación diferente. E incluso puede suceder que estos operadores estén ya implementados en una clase padre... no sobrescribirlos en la hija no te va a dar error... pero si puede dar resultados no esperados.
19. Evita códigos "inusuales"
Código
bool resultado = Func( ); resultado && Func2( );
Código
bool resultado = Func( ); if ( resultado ) Func2( );
¿Qué opción queda más clara? Espero que al menos la inmensa mayoría digáis la segunda... poco más que añadir al respecto.
El código claro y sencillo facilita su lectura y comprensión y eso reduce el número de horas necesarias para arreglar un problema. No hay que olvidar que en este mundo las horas tienen un coste económico.
20. No uses friend
El uso de 'friend' rompe con cualquier principio relacionado con la programación orientad a objetos que te puedas imaginar.
Si necesitas usar 'friend' es porque la arquitectura elegida es mejorable.
Entre otras cosas, 'friend' se encarga de hacer un acoplamiento bastante fuerte de las clases y eso va a dificultar la aplicación de tests unitarios, por ejemplo.
21. Acostúmbrate a usar repositorios
La época en la que el historial de cambios se almacenaba guardando las copias de seguridad en archivos comprimidos pasó a mejor vida.
En la actualidad dudo que encuentres una sola empresa en la que se trabaje así. La gran mayoría sino todas usan algún tipo de repositorio ( SVN, Mercurial, GIT, ... ). Lo mejor es que te vayas familiarizando con su uso. Lo vas a agradecer.
Como recomendación personal, creo que empezar con Mercurial es una buena opción.
* Te permite crear fácilmente repositorios nuevos
* No tiene un juego de instrucciones especialmente complicado.
* Dispones de portales web ( como BitBucket ) que te permiten almacenar repositorios privados de forma gratuita.
La idea luego sería utilizar repositorios más conocidos, como SVN o git, pero claro, es sólo mi opinión.
Y bueno, si tiene buena acogida este hilo lo extenderé con el tiempo... por supuesto, se aceptan comentarios y aportes.