elhacker.net cabecera Bienvenido(a), Visitante. Por favor Ingresar o Registrarse
¿Perdiste tu email de activación?.

 

 


Tema destacado: Guía rápida para descarga de herramientas gratuitas de seguridad y desinfección


  Mostrar Temas
Páginas: [1]
1  Programación / Programación C/C++ / [APORTE] C++ y bases de datos en: 9 Septiembre 2014, 16:42 pm
Introducción

En este documento intento plasmar un poco mi experiencia a la hora de tratar con bases de datos. Se que está muy de moda usar herramientas que automatizan la conexión y hacen gran parte del trabajo por nosotros. Sin embargo ésta es una de las cosas que prefiero hacer por mi cuenta, ya que he visto fracasar bastantes proyectos debido a una mala gestión de la base de datos.

Ojo, no me considero un experto sobre el tema, de hecho, mi preparación es ingeniero de telecomunicaciones y en la carrera he visto tanta teoría sobre bases de datos como la que pueden dar unos niños en la guardería.

Se aceptan críticas constructivas, comentarios, anotaciones, mejoras... en fín, lo de siempre.

Como también se pone de moda esto de las licencias, este código se presenta tal cual, sin garantía de ningún tipo. Sois libres de copiar y/o modificar el código a vuestro antojo, sin embargo declino toda responsabilidad que su mal uso pueda conllevar. También sois libres de coger el código y guardarlo en algún rincón oscuro como vuestro tesoro o usarlo para hechizos y brujería, eso si, siempre bajo vuestra responsabilidad.

Nota adicional: No puedo garantizar que el código esté 100% libre de errores, ya que la mayor parte de los ejemplos no son ejecutables y los he tenido que ir retocando (con cuidado, eso sí) para adaptarlos a la guía. Aún así, me parece importante recalcar que el código está escrito bajo el estándar C++11, así que cuidado con usar compiladores no compatibles.

Bueno, dicha la parte aburrida vamos al tema. Antes o después, todo el que se dedica al mundo de la programación, ya sea por hobby o profesionalmente, acaba haciendo uso de bases de datos. Esto suele ser debido a que tenemos la mala costumbre de querer guardar todo tipo de información... con lo divertido que es reescribir los documentos una y otra vez.

Debido a que no suele ser demasiado eficiente eso de resetear la base de datos y rellenarla de nuevo cada vez que queremos almacenar algún cambio, lo lógico es que nos veamos obligados a guardar determinada información sobre los objetos que se encuentran en la base de datos:

  • Necesitamos saber si el objeto es nuevo o no (no es lo mismo CREATE que UPDATE).
  • Necesitamos saber si el objeto ha sido modificado. Si no lo hacemos tendremos que actualizar TODOS, con el tiempo que ello pueda conllevar.
  • En el caso de tablas con campo autoincremental, necesitamos almacenar dicho OID para futuras consultas.

La primera aproximación que yo creo que hemos hecho todos es meter toda esta información a piñón en el propio objeto. Algo del tipo:

Código
  1. class User
  2. {
  3.  public:
  4.  
  5.    // Interfaz para la base de datos
  6.  
  7.    // Devuelve el OID ( en el caso de objetos nuevos vale 0 )
  8.    int OID( ) const;
  9.  
  10.    // Permite modificar el OID
  11.    void SetOID( int oid );
  12.  
  13.    // Indica si el objeto es nuevo (no esta aun en la BD )
  14.    bool IsNew( ) const;
  15.  
  16.    // Indica si el objeto ha sido modificado
  17.    bool IsModified( ) const;
  18.  
  19.    // Reinicia los flags 'IsNew' e 'IsModified'
  20.    void ObjectSaved( );
  21.  
  22.    // Interfaz propia del objeto
  23.  
  24.    std::string Name( ) const;
  25.  
  26.    std::string Surname( ) const;
  27.  
  28.    // ...
  29. };

Como queda patente de un primer vistazo, esta solución no parece demasiado elegante. Estamos introduciendo acoplamiento al introducir en la interfaz elementos propios de la gestión de la base de datos. Además, este diseño plantea otros problemas:

  • Si se usan plugins, exponemos información sensible de la base de datos a terceros.
  • Si nuestra aplicación trabaja en red nos obliga a enviar información innecesaria en el lado del cliente.

El diseño, al fin y al cabo, es feo, ya puestos, también podemos poner en 'User' los métodos para leer y escribir en la base de datos, un par de métodos también para poder almacenar la información en un fichero y, si nos quedan más ganas de marcha, también podemos meter en la clase el código que gestione la interfaz gráfica...(espero que se entienda la ironía).

Después de un rato pensando, a alguien podría decir... "bueno, creo una clase base que tenga los datos de la BD y soluciono el problema con herencia:

Código
  1. class DBItem
  2. {
  3.  public:
  4.  
  5.    // Interfaz para la base de datos
  6.  
  7.    // Devuelve el OID ( en el caso de objetos nuevos vale 0 )
  8.    int OID( ) const;
  9.  
  10.    // Permite modificar el OID
  11.    void SetOID( int oid );
  12.  
  13.    // Indica si el objeto es nuevo (no esta aun en la BD )
  14.    bool IsNew( ) const;
  15.  
  16.    // Indica si el objeto ha sido modificado
  17.    bool IsModified( ) const;
  18.  
  19.    // Reinicia los flags 'IsNew' e 'IsModified'
  20.    void ObjectSaved( );
  21. };
  22.  
  23. class User : public DBItem
  24. {
  25.  public:
  26.  
  27.    // Interfaz propia del objeto
  28.  
  29.    std::string Name( ) const;
  30.  
  31.    std::string Surname( ) const;
  32.  
  33.    // ...
  34. };

No voy a negarlo, es una posible solución, aunque este diseño sigue teniendo los mismos problemas de acoplamiento que el diseño anterior. La ventaja, eso sí, es que podremos reutilizar código de 'DBItem', no iban a ser todo malas noticias. Sin embargo, como comentaba, este diseño sigue sin convencer.

La idea entonces parece que pasa por separar físicamente la información relativa a la base de datos de los datos propios del objeto. El problema que aparece entonces es cómo conseguir esto de la forma más limpia posible, intentando además que sea reutilizable. Los requisitos que ha de cumplir este sistema son los siguientes:

  • Debe ser posible separar la lógica de negocio de la capa de acceso a datos.
  • Debe permitir la gestión de elementos eliminados.
  • Debe ser capaz de detectar elementos nuevos y elementos modificados.
  • La solución debe ser reutilizable.



Separar la lógica de negocio de la capa de acceso a datos.

Una posible solución, la que trataré en este tema, pasa por crear un contenedor a modo de caché.

El contenedor va a estar dividido en 2 niveles:

  • Nivel 1: El contenedor. Representa la interfaz accesible por la aplicación. Debe permitir realizar las operaciones habituales sobre los objetos que gestiona: creación, borrado, edición, etc.
  • Nivel 2: Información de base de datos. El contenedor almacenará instancias de un objeto que almacenará la información relevante de la base de datos. Este objeto únicamente será accesible por el contenedor.

De acuerdo con lo expuesto, la implementación de la clase interna del contenedor debería tener un aspecto similar al siguiente:

Código
  1. /*
  2.  * Gestiona información sobre un elemento almacenado en una caché.
  3.  * Los objetos se guardan indizados por clave única.
  4.  */
  5. class UserCacheItem final
  6. {
  7.    UserCacheItem( ) = delete;
  8.  
  9.    UserCacheItem(
  10.              const UserCacheItem& ar_other ) = delete;
  11.  
  12.    UserCacheItem& operator=(
  13.              const UserCacheItem& ar_other ) = delete;
  14.  
  15.  public:
  16.  
  17.    /*
  18.      * Constructor de la clase.
  19.      */
  20.    UserCacheItem(
  21.              User* ap_item,
  22.              int a_key )
  23.      : _item{ ap_item },
  24.        _key{ a_key }
  25.    {
  26.    }
  27.  
  28.    /*!
  29.      * Destructor de la clase.
  30.      */
  31.    virtual ~UserCacheItem( )
  32.    {
  33.    }
  34.  
  35.    /*!
  36.      * Método llamado por la caché cuando se han almacando los cambios en la base de datos.
  37.      */
  38.    void Commit( )
  39.    {
  40.      if ( _tempOid )
  41.      {
  42.        _key = *_tempOid;
  43.        _tempOid.reset( );
  44.      }
  45.    }
  46.  
  47.    /*!
  48.      * Indica si el objeto gestionado es nuevo o no.
  49.      */
  50.    inline bool IsNew( ) const
  51.    {
  52.      return 0 == _key;
  53.    }
  54.  
  55.    /*!
  56.      * Indica si el objeto gestionado ha sufrido cambios desde el último commit.
  57.      */
  58.    inline bool IsModified( ) const
  59.    {
  60.      return !IsNew( ) && !_tempOid && _hash != _item->Hash( );
  61.    }
  62.  
  63.    /*!
  64.      * Expone el puntero al objeto gestionado por la instancia.
  65.      */
  66.    inline User* Item( ) const;
  67.    {
  68.      return _item.get( );
  69.    }
  70.  
  71.    /*!
  72.      * Recupera la clave única utilizada para identificar el objeto gestionado.
  73.      */
  74.    int Key( ) const
  75.    {
  76.      if ( _tempOid )
  77.        return *_tempOid;
  78.      else
  79.        return _key;
  80.    }
  81.  
  82.    /*!
  83.      * Recupera la clave original.
  84.      */
  85.    inline int OriginalKey( ) const
  86.    {
  87.      return _key;
  88.    }
  89.  
  90.    /*!
  91.      * Permite modificar la clave del objeto.
  92.      */
  93.    void ChangeKey(
  94.              int a_key )
  95.    {
  96.      if ( ar_key == _key )
  97.      {
  98.        _tempOid.reset( );
  99.      }
  100.      else if ( ( !_tempOid ) || ( a_key != *_tempOid ) )
  101.      {
  102.        _tempOid.reset( new int{ a_key } );
  103.      }
  104.    }
  105.  
  106.  private:
  107.  
  108.    std::unique_ptr< User* > _item;
  109.    int _key;
  110.    std::unique_ptr< int > _tempKey
  111. };

Como se ve, internamente se usan punteros inteligentes. Desde mi punto de vista el uso de punteros inteligentes tiene dos ventajas básicas:

  • Evitar lagunas de memoria.
  • Queda claro que esta clase se encarga de controlar el ciclo de vida de los objetos de tipo 'User', lo cual evita malentendidos con los 'delete'.
  • Facilita el mantenimiento del código.

Además, ya se cómo funciona la memoria dinámica, no necesito practicarlo cada vez que tenga que crear objetos... y más cuando su ciclo de vida está tan delimitado. Prefiero centrar mis esfuerzos en problemas más serios.

En cuanto al control del oid, se puede apreciar que se usan dos claves: _key y _tempKey. Para almacenar el oid se ha optado por un mecanismo en dos pasos:

  • El valor inicial de _key se establece al crear el objeto.
  • Al llamar a ChangeKey se modifica _tempKey.
  • Al llamar a 'Commit' el valor de _key se modifica por _tempKey, suponiendo que tenga valor.

Este mecanismo en dos pasos permite "recordar" la clave que tenía inicialmente el objeto de cara a poder localizarlo en la base de datos en base a dicha clave. Recordemos que las actualizaciones a la base de datos suelen ser del tipo: "UPDATE users SET key=30 WHERE key=20". Si no guardamos ese '20', dificilmente podremos localizar el registro en la base de datos.

Entiendo que en este caso cambiar el oid puede no tener demasiado sentido, pero no siempre es así.

Hablando ahora del contenedor, su interfaz se detalla a continuacion:

Código
  1. /*!
  2.  * Contenedor de BD para objetos de tipo "User"
  3.  */
  4. class UserCache
  5. {
  6.    UserCache(
  7.              const UserCache& ar_other ) = delete;
  8.  
  9.    UserCache& operator=(
  10.              const UserCache& ar_other ) = delete;
  11.  
  12.  public:
  13.  
  14.    /*!
  15.      * Constructor por defecto.
  16.      */
  17.    UserCache( )
  18.    {
  19.    }
  20.  
  21.    /*!
  22.      * Destructor.
  23.      */
  24.    virtual ~UserCache( )
  25.    {
  26.    }
  27.  
  28.    /*!
  29.      * Inserta un nuevo elemento en la cache
  30.      */
  31.    bool Add(
  32.              User* ap_element,
  33.              int a_key )
  34.    {
  35.      if ( ap_element )
  36.      {
  37.        auto it = _items.find( a_key );
  38.        if ( it == _items.end( ) )
  39.        {
  40.          UserCacheItem* newItem = new UserCacheItem{ ap_element, a_key };
  41.          _items.insert( std::make_pair( a_key, std::unique_ptr< UserCacheItem >{ newItem } ) );
  42.          return true;
  43.        }
  44.      }
  45.  
  46.      return false;
  47.    }
  48.  
  49.    /*!
  50.      * Obtiene la clave para un usuario.
  51.      */
  52.    int Key(
  53.              const User* ap_element ) const
  54.    {
  55.      auto it = FindItem( *ap_element, _items );
  56.      if ( it != _items.end( ) )
  57.        return it->first;
  58.  
  59.      return 0;
  60.    }
  61.  
  62.    /*!
  63.      * Recupera la lista de usuarios.
  64.      */
  65.    std::vector< User* > Items( ) const
  66.    {
  67.      std::vector< User* > to_return;
  68.  
  69.      for ( auto it = _items.begin( ); it != _items.end( ); ++it )
  70.        to_return.push_back( it->second->Item( ) );
  71.  
  72.      return to_return;
  73.    }
  74.  
  75.    std::map< int, User* > MappedItems( ) const
  76.    {
  77.      std::map< int, User* > to_return;
  78.  
  79.      for ( auto it = _items.begin( ); it != _items.end( ); ++it )
  80.        to_return.insert( std::make_pair( it->first, it->second->Item( ) ) );
  81.  
  82.      return to_return;
  83.    }
  84.  
  85.    /*!
  86.      * Recupera un usuario en base a su oid.
  87.      */
  88.    User* Item(
  89.              int a_key ) const
  90.    {
  91.      auto it = _items.find( a_key );
  92.      if ( it != _items.end( ) )
  93.        return it->second->Item( );
  94.  
  95.      return nullptr;
  96.    }
  97.  
  98.    /*!
  99.      * Permite cambiar el oid de un usuario.
  100.      */
  101.    bool ChangeKey(
  102.              const User* ap_element,
  103.              int a_key );
  104.    {
  105.      if ( ap_element )
  106.      {
  107.        auto it = _items.find( a_key );
  108.        if ( it != _items.end( ) )
  109.          return ( it->second->Item( ) == ap_element );
  110.  
  111.        it = FindItem( *ap_element, _items );
  112.        if ( it != _items.end( ) )
  113.        {
  114.          // Si cambia la clave tenemos que actualizar el mapa de elementos
  115.          it->second->SetKey( ar_key );
  116.          _items.insert( std::make_pair( ar_key, std::move( it->second ) ) );
  117.          _items.erase( it );
  118.          return true;
  119.        }
  120.      }
  121.  
  122.      return false;
  123.    }
  124.  
  125.    /*!
  126.      * Marca todos los usuarios como "no modificados", además se encarga de
  127.      * descartar todos los elementos eliminados.
  128.      */
  129.    void Commit( )
  130.    {
  131.      for ( auto& item : _items )
  132.        item.second->Commit( );
  133.    }
  134.  
  135.    /*!
  136.      * Reinicia la caché. Elimina todo su contenido.
  137.      */
  138.    void Clear( )
  139.    {
  140.      _items.clear( );
  141.    }
  142.  
  143.    /*!
  144.      * Indica si hay cambios pendientes de llevar a la base de datos.
  145.      */
  146.    bool ChangesPending( ) const;
  147.    {
  148.      for ( auto& item : _items )
  149.      {
  150.        if ( item.second->IsModified( ) )
  151.          return true;
  152.      }
  153.  
  154.      return false;
  155.    }
  156.  
  157.  private:
  158.  
  159.    std::map< int, std::unique_ptr< UserCacheItem > > _items;
  160.  
  161.    std::map< int, User* >::iterator FindItem(
  162.              const User& ar_element ) const
  163.    {
  164.      return std::find_if( _items.begin( ),
  165.                           _items.end( ),
  166.                           [&ar_element]( const std::pair< int, User* >& pair )
  167.                           { return pair.second->Item( ) == &ar_element; } );
  168.    }
  169.  
  170. };

Bueno, con este diseño parece que hemos conseguido desacoplar el uso de la base de datos del objeto 'User'. No está mal para empezar. Sin embargo aún queda camino por recorrer.



Borrar usuarios

Normalmente, cuando en una aplicación se decide eliminar información, éste borrado no suele ser inmediato. En ocasiones hay que esperar a que el usuario confirme la operación dando la orden de "guardar cambios", mientras que en otras ocasiones interesa retrasar las eliminaciones con la intención de intentar agruparlas para optimizar las consultas a la base de datos.

Para satisfacer esta necesidad, nuestro contenedor va a encargarse de la gestión de los elementos borrados de la siguiente forma:

  • Cuando le indicamos que deseamos borrar un elemento que ya se encuentra gestionado por el contenedor lo introduce en un listado de elementos a eliminar. Los elementos no se eliminarán de forma efectiva hasta que no hagamos "commit".
  • Cuando le indicamos que deseamos borrar un elemento que NO se encuentra gestionado por el contenedor lo elimina directamente. Esto es así porque al no estar gestionado por el contenedor no es posible que otras partes del código conozcan este objeto.

Un ejemplo del código que permite cumplir con esta función lo encontramos a continuación:

Código
  1. class UserCache
  2. {
  3.  public:
  4.  
  5.    bool Delete(
  6.        int a_key )
  7.    {
  8.      auto it = _items.find( a_key );
  9.      if ( it != _items.end( ) )
  10.      {
  11.        _deletedItems.push_back( std::move( it->second ) );
  12.        _items.erase( it );
  13.        return true;
  14.      }
  15.  
  16.      return false;
  17.    }
  18.  
  19.    bool Delete(
  20.        User* ap_element )
  21.    {
  22.      if ( ap_element )
  23.      {
  24.        auto it = FindItem( *ap_element );
  25.  
  26.        if ( it != _items.end( ) )
  27.        {
  28.          _deletedItems.push_back( std::move( it->second ) );
  29.          _items.erase( it );
  30.          return true;
  31.        }
  32.      }
  33.  
  34.      return false;
  35.    }
  36.  
  37.    std::map< int, User* > ToDelete( ) const
  38.    {
  39.      std::map< int, User* > to_return;
  40.  
  41.      for ( auto& pair : _deletedItems )
  42.        to_return.insert( std::make_pair( pair.first, pair.second->Item( ) ) );
  43.  
  44.      return to_return;
  45.    }
  46.  
  47.    void Commit( )
  48.    {
  49.      _deletedItems.clear( );
  50.  
  51.      for ( auto& item : _items )
  52.        item.second->Commit( );
  53.    }
  54.  
  55.    void Clear( )
  56.    {
  57.      _items.clear( );
  58.      _deletedItems.clear( );
  59.    }
  60.  
  61.    bool ChangesPending( ) const
  62.    {
  63.      if ( !_deletedItems.empty( ) )
  64.        return true;
  65.  
  66.      for ( auto& item : _items )
  67.      {
  68.        if ( item.second->IsModified( ) )
  69.          return true;
  70.      }
  71.  
  72.      return false;
  73.   }
  74.  
  75.  private:
  76.  
  77.    std::vector< std::unique_ptr< User > > _deletedItems;
  78. };

Como se puede apreciar, el contenedor nos va a ofrecer en un cómodo listado todos los objetos que se han marcado como borrados. Si recogemos este listado podemos



Modificar usuarios

Cuando recurrimos al método 'feo' para interaccionar con bases de datos, a menudo caemos en la tentación de registrar las modificaciones con un código similar al siguiente:

Código
  1. class User
  2. {
  3.  public:
  4.  
  5.    User( )
  6.      : _modified{ false }
  7.    { }
  8.  
  9.    std::string Name( ) const
  10.    { return _name; }
  11.  
  12.    void SetName( const std::string& name )
  13.    {
  14.      if ( name != _name )
  15.      {
  16.        _name = name;
  17.        _modified = true;
  18.      }
  19.    }
  20.  
  21.    bool IsModified( ) const
  22.    { return _modified; }
  23.  
  24.  private:
  25.  
  26.    std::string _name;
  27.    bool _modified;
  28. };

Esta solución ya no nos sirve, ya que intentamos que la clase 'User' sera totalmente independiente de la base de datos.

Para poder marcar los objetos como modificados se puede optar por varias soluciones diferentes, os enumero tres de ellas:

  • Hacer una llamada del tipo 'UserCache::SetModified( usuario )' cada vez que modifiquemos un objeto. Este mecanismo tiene el inconveniente de ser complicado de gestionar, ya que obliga a meter esta instrucción en todas las funciones que modifiquen el objeto. Si el objeto se modifica en zonas muy concretas de la aplicación puede ser una solución bastante factible. PD.: no es recomendado si la clase puede ser modificada por plugins.
Código
  1. class UserCache
  2. {
  3.  public:
  4.    void SetModified( User* ap_element );
  5. };
  6.  
  7. class Something
  8. {
  9.  public:
  10.  
  11.    void UpdateUser( User* user );
  12.    {
  13.      user->SetName( anotherName );
  14.      user->SetSurname( anotherSurname );
  15.  
  16.      // El método realmente no es estático, lo pongo asi por legibilidad
  17.      UserCache::SetModified( ap_element );      
  18.    }
  19. };

  • Utilizar un hash para identificar los elementos que han cambiado. Este hash debe estar mapeado en el contenedor para poder detectar cambios. Este mecanismo puede implementarse en el contenedor o en la clase 'User'. Esta solución, aparte de ser más limpia ofrece código reutilizable, ya que el hash puede usarse, por ejemplo, en los operadores de igualdad.
Código
  1. class User
  2. {
  3.  public:
  4.    std::size_t hash( ) const
  5.    {
  6.      const std::size_t name{ std::hash< std::string >( )( _name ) };
  7.      const std::size_t surname { std::hash< std::string >( )( _surname ) };
  8.      return name ^ ( surname << 1 );
  9.    }
  10. };

  • Utilizar un "contador de versión". El contador de versión puede ser tan sencillo como un "unsigned int" que se va incrementando cada vez que se realiza un cambio en el objeto. Entonces, basta con que el contenedor realice un mapeo de la versión de cada objeto para poder identificar los elementos modificados. La ventaja de este sistema respecto al hash es que es más rápido, la desventaja es que requiere actualizar los setters. Este método se parece bastante al "método feo" que iniciaba este apartado, es cierto.
Código
  1. class User
  2. {
  3.  public:
  4.  
  5.    User( )
  6.      : _version{ 0 }
  7.    { }
  8.  
  9.    void SetName( const std::string& name )
  10.    {
  11.      if ( name != _name )
  12.      {
  13.        _name = name;
  14.        ++_version;
  15.      }
  16.    }
  17.  
  18.    unsigned int Version( ) const
  19.    { return _version; }
  20. };

Una vez que ya hemos elegido el mecanismo que vamos a implementar tenemos que adaptar el funcionamiento del contenedor para que sea capaz de identificar los elementos que han sufrido cambios. En este caso, el código de ejemplo presupone que se ha tomado como buena la opción de hash. Adaptar el código para que funcione con la opción del "contador de versiones" es bastante sencillo:

Clase UserCacheItem:
Código
  1. class UserCacheItem final
  2. {
  3.  public:
  4.  
  5.    /*!
  6.      * \brief Constructor de la clase.
  7.      */
  8.    UserCacheItem(
  9.        User* ap_item,
  10.        int a_key )
  11.      : _item{ ap_item },
  12.        _key{ ar_key },
  13.        _hash{ ap_item->Hash( ) }
  14.    {
  15.    }
  16.  
  17.    /*
  18.      * Método llamado por la caché cuando se han almacando los cambios en la base de datos.
  19.      */
  20.    void Commit( )
  21.    {
  22.      if ( _tempOid )
  23.      {
  24.        _key = *_tempOid;
  25.        _tempOid.reset( );
  26.      }
  27.  
  28.      _hash = ap_item->Hash( );
  29.    }
  30.  
  31.    /*!
  32.      * Indica si el objeto gestionado ha sufrido cambios desde el último commit.
  33.      */
  34.    inline bool IsModified( ) const
  35.    { return !IsNew( ) && !_tempOid && _hash != _item->Hash( ); }
  36.  
  37.  private:
  38.  
  39.    std::unique_ptr< User > _item;
  40.    int _key;
  41.    std::unique_ptr< int > _tempOid;
  42.    std::size_t _hash;
  43.  
  44. };

Clase UserCache:
Código
  1. class UserCache
  2. {
  3.  public:
  4.  
  5.    std::set< User* > ToCreate( ) const
  6.    {
  7.      std::set< User* > to_return;
  8.  
  9.      for ( auto& item : _items )
  10.      {
  11.        if ( item.second->IsNew( ) )
  12.          to_return.insert( item.second->Item( ) );
  13.      }
  14.  
  15.      return to_return;
  16.    }
  17.  
  18.    std::map< int, User* > ToUpdate( ) const
  19.    {
  20.      std::map< int, User* > to_return;
  21.  
  22.      for ( auto& item : _items )
  23.      {
  24.        if ( item.second->IsModified( ) )
  25.          to_return.insert( std::make_pair( item.first, item.second->Item( ) ) );
  26.      }
  27.  
  28.      return to_return;
  29.    }
  30.  
  31.    bool ChangesPending( ) const
  32.    {
  33.      for ( auto& item : _items )
  34.      {
  35.        if ( item.second->IsModified( ) )
  36.          return true;
  37.      }
  38.  
  39.      return false;
  40.    }
  41. };



Conectar el contenedor con la base de datos

Una de las primeras motivaciones que nos ha llevado a este punto ha sido la necesidad de reducir el nivel de acoplamiento en nuestras clases. Por este motivo vamos a delegar la responsabilidad de la comunicación con la base de datos en una nueva clase.

A mí se me ha ocurrido llamarla 'DBUsers', para gustos, los nombres de las clases. Bueno, al tema, el caso es que esta clase debería proporcionar métodos para leer y escribir en las cachés de datos. Personalmente, en esta clase no me preocupa demasiado que haya acoplamiento ya que, como norma general, cada aplicación tiene su propio diseño de base de datos, por lo que el código de esta clase dificilmente será reutilizable.

Código
  1. class DBUsers
  2. {
  3.    DBUsers( ) = delete;
  4.  
  5.    DBUsers(
  6.          const DBUsers& ar_other ) = delete;
  7.  
  8.    const DBUsers& operator=(
  9.          const DBUsers& ar_other ) = delete;
  10.  
  11.  public:
  12.  
  13.    DBUsers(
  14.           const UniqueKeyCache< User, int >& ar_cache )
  15.      : _cache{ &ar_cache }
  16.    {
  17.    }
  18.  
  19.    virtual ~DBUsers( )
  20.    {
  21.    }
  22.  
  23.    bool SaveData( )
  24.    {
  25.      bool ok = true;
  26.  
  27.      // Este primer chequeo no sería necesario, pero me gusta la simetría
  28.      // y además así aseguro que se elimina deletedItems
  29.      if ( ok )
  30.      {
  31.        // Primero es recomendable realizar la operación de borrado
  32.        auto deletedItems = _cache.ItemsToDelete( );
  33.        for ( auto& pair : deletedItems )
  34.        {
  35.          // Generación y ejecución de las consultas de eliminación.
  36.          // Si algúna consulta da error, actualizamos ok a false
  37.        }
  38.      }
  39.  
  40.      if ( ok )
  41.      {
  42.        // Después actualizamos los elementos modificados
  43.        auto updatedItems = _cache.ItemsToUpdate( );
  44.        for ( auto& pair : updatedItems )
  45.        {
  46.          // Generación y ejecución de las consultas de actualización
  47.          // Si algúna consulta da error, actualizamos ok a false
  48.        }
  49.      }
  50.  
  51.      // Finalmente damos de alta los nuevos registros
  52.      if ( ok )
  53.      {
  54.        auto newItems = _cache.ItemsToCreate( );
  55.        for ( auto& item : newItems )
  56.        {
  57.          // Generación y ejecución de las consultas de creación
  58.          // Si algúna consulta da error, actualizamos ok a false
  59.  
  60.          // No hay que olvidarse de recuperar el OID de cada elemento
  61.          // y actualizar la cache.
  62.          _cache->ChangeKey( &item, newOid );
  63.        }
  64.      }
  65.  
  66.      if ( ok )
  67.        _cache->Commit( );
  68.      else
  69.      {
  70.        // Si se produce algún error tenemos que decidir entre resetear la cache
  71.        // o permitir al usuario intentarlo más veces.
  72.      }
  73.    }
  74.  
  75.    void LoadUsers( )
  76.    {
  77.      // Consulta de selección sobre la tabla de usuarios
  78.      // ...
  79.      while ( row.next( ) ) // Para cada registro leído...
  80.      {
  81.        User* user = new User{ };
  82.        user->SetName( row.data( "name" ).toString( ) );
  83.        user->SetSurname( row.data( "surname" ).toString( ) );
  84.  
  85.        _cache->Add( user, row.data( "id" ).toInt( ) );
  86.      }
  87.    }
  88.  
  89.    // No hace falta leer toda la tabla... se puede leer bajo demanda para
  90.    // reducir los tiempos de acceso.
  91.    void LoadUsers( const std::string& name );
  92.  
  93.  private:
  94.  
  95.    UniqueKeyCache< User, int >* _cache;
  96.  
  97. };

Como se ve, la caché la almaceno por referencia. Si no tengo necesidad de eliminar un objeto en un contexto dado, y además necesito que ese objeto exista, prefiero usar una referencia que un puntero, así queda claro que no es competencia de ese contexto hacer un delete. Además, la idea de que la caché pueda utilizarse de forma independiente permite aislar la capa de acceso a base de datos, piensa que no es obligatorio que los datos también se pueden guardar en un fichero o enviar por red... este diseño permite tener una clase específica para cada uso y sin necesidad de que se conozcan entre ellas.



Reutilización del código

El código ya nos funciona para la clase 'User', pero la gracia es que todo este esfuerzo pueda ser reutilizado con otras clases. La mejor forma para hacer esto es convertir la caché en un template... el código puede que se ensucie un poco con esta conversión, pero creo que es un sacrificio razonable que se va a compensar con creces:

Template CacheItem (sustituye a UserCacheItem):
Código
  1. /*!
  2.  * Gestiona información sobre un elemento almacenado en una caché.
  3.  * Los objetos se guardan indizados por clave única.
  4.  */
  5. template< class _CLASS_, class _KEY_ >
  6. class CacheItem final
  7. {
  8.    CacheItem( ) = delete;
  9.  
  10.    CacheItem(
  11.        const CacheItem& ar_other ) = delete;
  12.  
  13.    CacheItem& operator=(
  14.        const CacheItem& ar_other ) = delete;
  15.  
  16.  public:
  17.  
  18.    /*!
  19.      * Constructor de la clase.
  20.      */
  21.    CacheItem(
  22.        _CLASS_* ap_item,
  23.        const _KEY_& ar_key );
  24.  
  25.    /*!
  26.      * Destructor de la clase.
  27.      */
  28.    virtual ~CacheItem( );
  29.  
  30.    /*!
  31.      * Método llamado por la caché cuando se han almacando los cambios en la base de datos.
  32.      */
  33.    void Commit( );
  34.  
  35.    /*!
  36.      * Indica si el objeto gestionado es nuevo o no.
  37.      */
  38.    inline bool IsNew( ) const
  39.    { return _KEY_{ } == _key; }
  40.  
  41.    /*!
  42.      * Indica si el objeto gestionado ha sufrido cambios desde el último commit.
  43.      */
  44.    inline bool IsModified( ) const
  45.    { return !IsNew( ) && !_tempOid && _hash != _item->Hash( ); }
  46.  
  47.    /*!
  48.      * Expone el puntero al objeto gestionado por la instancia.
  49.      */
  50.    inline _CLASS_* Item( ) const
  51.    { return _item.get( ); }
  52.  
  53.    /*!
  54.      * Recupera la clave única utilizada para identificar el objeto gestionado.
  55.      */
  56.    _KEY_ Key( ) const;
  57.  
  58.    /*!
  59.      * Recupera la clave original.
  60.      */
  61.    inline _KEY_ OriginalKey( ) const
  62.    { return _key; }
  63.  
  64.    /*!
  65.      * Permite modificar la clave del objeto.
  66.      */
  67.    void ChangeKey(
  68.        const _KEY_& ar_key )
  69.    {
  70.      if ( ar_key == _key )
  71.      {
  72.        _tempOid.reset( );
  73.      }
  74.      else if ( ( !_tempOid ) || ( ar_key != *_tempOid ) )
  75.      {
  76.        _tempOid.reset( new _KEY_{ ar_key } );
  77.      }
  78.    }
  79.  
  80.  private:
  81.  
  82.    std::unique_ptr< _CLASS_ > _item;
  83.    _KEY_ _key;
  84.    std::unique_ptr< _KEY_ > _tempOid;
  85.    std::size_t _hash;
  86.  
  87. };
  88.  
  89. template< class _CLASS_, class _KEY_ >
  90. CacheItem< _CLASS_,_KEY_ >::CacheItem(
  91.          _CLASS_* ap_item,
  92.          const _KEY_& ar_key )
  93.  : _item{ ap_item },
  94.    _key{ ar_key },
  95.    _hash{ ap_item->Hash( ) }
  96. {
  97. }
  98.  
  99. template< class _CLASS_, class _KEY_ >
  100. CacheItem< _CLASS_,_KEY_ >::~CacheItem( )
  101. {
  102. }
  103.  
  104. template< class _CLASS_, class _KEY_ >
  105. void
  106. CacheItem< _CLASS_,_KEY_ >::Commit( )
  107. {
  108.  if ( _tempOid )
  109.  {
  110.    _key = *_tempOid;
  111.    _tempOid.reset( );
  112.  }
  113.  
  114.  _hash = _item->Hash( );
  115. }
  116.  
  117. template< class _CLASS_, class _KEY_ >
  118. _KEY_
  119. CacheItem< _CLASS_,_KEY_ >::Key( ) const
  120. {
  121.  if ( _tempOid )
  122.    return *_tempOid;
  123.  else
  124.    return _key;
  125. }

Se pueden apreciar algúnos cambios en lo relativo al tratamiento de la clave primaria. Dado que ahora esta clave puede ser de cualquier tipo (un string por ejemplo), los métodos ahora gestionan la clave por referencia.

Clase UniqueKeyCache (sustituye a UserCache):
Código
  1. template< class _CLASS_, class _KEY_ >
  2. class UniqueKeyCache
  3. {
  4.    UniqueKeyCache(
  5.        const UniqueKeyCache& ar_other ) = delete;
  6.  
  7.    UniqueKeyCache& operator=(
  8.        const UniqueKeyCache& ar_other ) = delete;
  9.  
  10.  public:
  11.  
  12.    /*!
  13.      * Constructor por defecto.
  14.      */
  15.    UniqueKeyCache( )
  16.    { }
  17.  
  18.    /*!
  19.      * Destructor.
  20.      */
  21.    virtual ~UniqueKeyCache( )
  22.    { }
  23.  
  24.    /*!
  25.      * Inserta un nuevo elemento en la cache.
  26.      */
  27.    bool Add(
  28.        _CLASS_* ap_element,
  29.        const _KEY_& ar_key );
  30.  
  31.    /*!
  32.      * Devuelve la clave asociada a un elemento.
  33.      */
  34.    _KEY_ Key(
  35.        const _CLASS_* ap_element ) const;
  36.  
  37.    /*!
  38.      * Marca un elemento para ser eliminado.
  39.      */
  40.    bool Delete(
  41.        const _KEY_& ar_key );
  42.  
  43.    /*!
  44.      * Marca un elemento para ser eliminado.
  45.      */
  46.    bool Delete(
  47.        _CLASS_* ap_element );
  48.  
  49.    /*!
  50.      * Devuelve el listado de elementos.
  51.      */
  52.    std::set< _CLASS_* > Items( ) const;
  53.  
  54.    /*!
  55.      * Devuelve el listado de elementos mapeados por la clave primaria.
  56.      */
  57.    std::map< _KEY_, _CLASS_* > MappedItems( ) const;
  58.  
  59.    /*!
  60.      * Devuelve un elemento dada su clave primaria.
  61.      */
  62.    _CLASS_* Item(
  63.        const _KEY_& ar_key ) const;
  64.  
  65.    /*!
  66.      * Devuelve la lista de elementos que aún no están dados de alta en la base
  67.      * de datos.
  68.      */
  69.    std::set< _CLASS_* > ToCreate( ) const;
  70.  
  71.    /*!
  72.      * Devuelve la lista de elementos a actualizar en la base de datos.
  73.      */
  74.    std::map< _KEY_, _CLASS_* > ToUpdate( ) const;
  75.  
  76.    /*!
  77.      * Devuelve la lista de elementos a eliminar de la base de datos.
  78.      */
  79.    std::map< _KEY_, _CLASS_* > ToDelete( ) const;
  80.  
  81.    /*!
  82.      * Permite modificar la clave primaria de un elemento.
  83.      */
  84.    bool ChangeKey(
  85.        const _CLASS_* ap_element,
  86.        const _KEY_& ar_key );
  87.  
  88.    /*!
  89.      * Limpia la lista de elementos nuevos y/o modificados y elimina los
  90.      * elementos marcados para borrar.
  91.      */    
  92.    void Commit( );
  93.  
  94.    /*!
  95.      * Limpia la caché, eliminando todo su contenido.
  96.      */
  97.    void Clear( );
  98.  
  99.    /*!
  100.      * Indica si hay o no cambios pendientes de llevar a la base de datos.
  101.      */
  102.    bool ChangesPending( ) const;
  103.  
  104.  private:
  105.  
  106.    using _Item_              = CacheItem< _CLASS_, _KEY_ >;
  107.    using _ItemMap_           = std::map< _KEY_, std::unique_ptr< _Item_ > >;
  108.    using _ItemPair_          = std::pair< _KEY_, std::unique_ptr< _Item_ > >;
  109.    using _ItemMapIterator_   = typename _ItemMap_::iterator;
  110.    using _ItemList_          = std::vector< std::unique_ptr< _Item_ > >;
  111.  
  112.    _ItemMap_ _items;
  113.    _ItemList_ _deletedItems;
  114.  
  115.    /*!
  116.      * Utilidad para buscar en la lista de elementos.
  117.      */
  118.    _ItemMapIterator_ FindItem(
  119.        _CLASS_* ap_element ) const
  120.    {
  121.      return std::find_if( _items.begin( ),
  122.                           _items.end( ),
  123.                           [ap_element]( const _ItemPair_& pair )
  124.                           { return pair.second->Item( ) == ap_element; } );
  125.    }
  126. };
  127.  
  128. template< class _CLASS_, class _KEY_ >
  129. bool
  130. UniqueKeyCache< _CLASS_, _KEY_ >::Add(
  131.          _CLASS_* ap_element,
  132.          const _KEY_& ar_key )
  133. {
  134.  if ( ap_element )
  135.  {
  136.    auto it = _items.find( ar_key );
  137.    if ( it == _items.end( ) )
  138.    {
  139.      _Item_* newItem = new _Item_{ ap_element, ar_key };
  140.      _items.insert( std::make_pair( ar_key, std::unique_ptr< _Item_ >( newItem ) ) );
  141.      return true;
  142.    }
  143.  }
  144.  
  145.  return false;
  146. }
  147.  
  148. template< class _CLASS_, class _KEY_ >
  149. _KEY_
  150. UniqueKeyCache< _CLASS_, _KEY_ >::Key(
  151.          const _CLASS_* ap_element ) const
  152. {
  153.  auto it = FindItem( *ap_element );
  154.  if ( it != _items.end( ) )
  155.    return it->first;
  156.  
  157.  return _KEY_{ };
  158. }
  159.  
  160. template< class _CLASS_, class _KEY_ >
  161. bool
  162. UniqueKeyCache< _CLASS_, _KEY_ >::Delete(
  163.          const _KEY_& ar_key )
  164. {
  165.  auto it = _items.find( ar_key );
  166.  if ( it != _items.end( ) )
  167.  {
  168.    _deletedItems.push_back( std::move( it->second ) );
  169.    _items.erase( it );
  170.    return true;
  171.  }
  172.  
  173.  return false;
  174. }
  175.  
  176. template< class _CLASS_, class _KEY_ >
  177. bool
  178. UniqueKeyCache< _CLASS_, _KEY_ >::Delete(
  179.          _CLASS_* ap_element )
  180. {
  181.  if ( ap_element )
  182.  {
  183.    auto it = FindItem( *ap_element );
  184.    if ( it != _items.end( ) )
  185.    {
  186.      _deletedItems.push_back( std::move( it->second ) );
  187.      _items.erase( it );
  188.      return true;
  189.    }
  190.  }
  191.  
  192.  return false;
  193. }
  194.  
  195. template< class _CLASS_, class _KEY_ >
  196. std::set< _CLASS_* >
  197. UniqueKeyCache< _CLASS_, _KEY_ >::Items( ) const
  198. {
  199.  std::set< _CLASS_* > to_return;
  200.  
  201.  for ( auto& pair : _items )
  202.    to_return.insert( pair.second->Item( ) );
  203.  
  204.  return to_return;
  205. }
  206.  
  207. template< class _CLASS_, class _KEY_ >
  208. std::map< _KEY_, _CLASS_* >
  209. UniqueKeyCache< _CLASS_, _KEY_ >::MappedItems( ) const
  210. {
  211.  std::map< _KEY_, _CLASS_* > to_return;
  212.  
  213.  for ( auto& pair : _items )
  214.    to_return.insert( std::make_pair( pair.first, pair.second->Item( ) ) );
  215.  
  216.  return to_return;
  217. }
  218.  
  219. template< class _CLASS_, class _KEY_ >
  220. _CLASS_*
  221. UniqueKeyCache< _CLASS_, _KEY_ >::Item(
  222.          const _KEY_& ar_key ) const
  223. {
  224.  auto it = _items.find( ar_key );
  225.  if ( it != _items.end( ) )
  226.    return it->second->Item( );
  227.  
  228.  return nullptr;
  229. }
  230.  
  231. template< class _CLASS_, class _KEY_ >
  232. std::set< _CLASS_* >
  233. UniqueKeyCache< _CLASS_, _KEY_ >::ToCreate( ) const
  234. {
  235.  std::set< _CLASS_* > to_return;
  236.  
  237.  for ( auto& pair : _items )
  238.  {
  239.    if ( pair.second->IsNew( ) )
  240.      to_return.insert( pair.second->Item( ) );
  241.  }
  242.  
  243.  return to_return;
  244. }
  245.  
  246. template< class _CLASS_, class _KEY_ >
  247. std::map< _KEY_, _CLASS_* >
  248. UniqueKeyCache< _CLASS_, _KEY_ >::ToUpdate( ) const
  249. {
  250.  std::map< _KEY_, _CLASS_* > to_return;
  251.  
  252.  for ( auto& item : _items )
  253.  {
  254.    if ( item.second->IsModified( ) )
  255.      to_return.insert( std::make_pair( item.first, item.second->Item( ) ) );
  256.  }
  257.  
  258.  return to_return;
  259. }
  260.  
  261. template< class _CLASS_, class _KEY_ >
  262. std::map< _KEY_, _CLASS_* >
  263. UniqueKeyCache< _CLASS_, _KEY_ >::ToDelete( ) const
  264. {
  265.  std::map< _KEY_, _CLASS_* > to_return;
  266.  
  267.  for ( auto& pair : _deletedItems )
  268.    to_return.insert( std::make_pair( pair.first, pair.second->Item( ) ) );
  269.  
  270.  return to_return;
  271. }
  272.  
  273. template< class _CLASS_, class _KEY_ >
  274. bool
  275. UniqueKeyCache< _CLASS_, _KEY_ >::ChangeKey(
  276.          const _CLASS_* ap_element,
  277.          const _KEY_& ar_key )
  278. {
  279.  if ( ap_element )
  280.  {
  281.    auto it = _items.find( ar_key );
  282.    if ( it != _items.end( ) )
  283.      return ( it->second->Item( ) == ap_element );
  284.  
  285.    it = FindItem( *ap_element );
  286.    if ( it != _items.end( ) )
  287.    {
  288.      it->second->SetKey( ar_key );
  289.      _items.insert( std::make_pair( ar_key, std::move( it->second ) ) );
  290.      _items.erase( it );
  291.      return true;
  292.    }
  293.  }
  294.  
  295.  return false;
  296. }
  297.  
  298. template< class _CLASS_, class _KEY_ >
  299. void
  300. UniqueKeyCache< _CLASS_, _KEY_ >::Commit( )
  301. {
  302.  _deletedItems.clear( );
  303.  
  304.  for ( auto& item : _items )
  305.    item.second->Commit( );
  306. }
  307.  
  308. template< class _CLASS_, class _KEY_ >
  309. void
  310. UniqueKeyCache< _CLASS_, _KEY_ >::Clear( )
  311. {
  312.  _items.clear( );
  313.  _deletedItems.clear( );
  314. }
  315.  
  316. template< class _CLASS_, class _KEY_ >
  317. bool
  318. UniqueKeyCache< _CLASS_, _KEY_ >::ChangesPending( ) const
  319. {
  320.  if ( !_deletedItems.empty( ) )
  321.    return true;
  322.  
  323.  for ( auto& item : _items )
  324.  {
  325.    if ( item.second->IsModified( ) )
  326.      return true;
  327.  }
  328.  
  329.  return false;
  330. }



Posibles mejoras

La cache se puede heredar para añadir funcionalidad específica en función del tipo de objeto que estemos cacheando. Por ejemplo, en el caso de los usuarios podría ser util obtener un listado de los usuarios con la cuenta bloqueada. Básicamente lo único que tendríamos que modificar en la clase 'UniqueKeyCache' es poner el listado de elementos en la parte protegida y heredar:

Código
  1. class UsersCache : public UniqueKeyCache< User, int >
  2. {
  3.  public:
  4.  
  5.    std::set< User* > LockedAccounts( ) const
  6.    {
  7.      std::set< User* > to_return;
  8.  
  9.      for ( auto& pair : _items )
  10.      {
  11.        if ( pair->second->Item( )->IsLocked( ) )
  12.          to_return.insert( pair->second->Item( ) );
  13.      }
  14.  
  15.      return to_return;
  16.    }
  17. };

 Y bueno, dado que un template no es exactamente lo mismo que una clase base, lo mejor es también sustituir 'DBUsers' para que acepte un objeto de tipo 'UserCache':
 
Código
  1. class DBUsers
  2. {
  3.    DBUsers( ) = delete;
  4.  
  5.    DBUsers(
  6.          const DBUsers& ar_other ) = delete;
  7.  
  8.    const DBUsers& operator=(
  9.          const DBUsers& ar_other ) = delete;
  10.  public:
  11.  
  12.    DBUsers(
  13.           const UsersCache& ar_cache )
  14.      : _cache{ ar_cache }
  15.    {
  16.    }
  17.  
  18.    virtual ~DBUsers( )
  19.    {
  20.    }
  21.  
  22.    bool SaveData( )
  23.    {
  24.      bool ok = true;
  25.  
  26.      // Este primer chequeo no sería necesario, pero me gusta la simetría
  27.      // y además así aseguro que se elimina deletedItems cuando deja de ser necesario
  28.      if ( ok )
  29.      {
  30.        // Primero es recomendable realizar la operación de borrado
  31.        auto deletedItems = _cache.ItemsToDelete( );
  32.        for ( auto& pair : deletedItems )
  33.        {
  34.          // Generación y ejecución de las consultas de eliminación.
  35.          // Si algúna consulta da error, actualizamos ok a false
  36.        }
  37.      }
  38.  
  39.      if ( ok )
  40.      {
  41.        // Después actualizamos los elementos modificados
  42.        auto updatedItems = _cache.ItemsToUpdate( );
  43.        for ( auto& pair : updatedItems )
  44.        {
  45.          // Generación y ejecución de las consultas de actualización
  46.          // Si algúna consulta da error, actualizamos ok a false
  47.        }
  48.      }
  49.  
  50.      // Finalmente damos de alta los nuevos registros
  51.      if ( ok )
  52.      {
  53.        auto newItems = _cache.ItemsToCreate( );
  54.        for ( auto& item : newItems )
  55.        {
  56.          // Generación y ejecución de las consultas de creación
  57.          // Si algúna consulta da error, actualizamos ok a false
  58.  
  59.          // No hay que olvidarse de recuperar el OID de cada elemento
  60.          // y actualizar la cache.
  61.          _cache.ChangeKey( &item, newOid );
  62.        }
  63.      }
  64.  
  65.      if ( ok )
  66.        _cache.Commit( );
  67.      else
  68.      {
  69.        // Si se produce algún error tenemos que decidir entre resetear la cache
  70.        // o permitir al usuario intentarlo más veces.
  71.      }
  72.    }
  73.  
  74.    void LoadUsers( )
  75.    {
  76.      // Consulta de selección sobre la tabla de usuarios
  77.      // ...
  78.      while ( row.next( ) ) // Para cada registro leído...
  79.      {
  80.        User* user = new User{ };
  81.        user->SetName( row.data( "name" ).toString( ) );
  82.        user->SetSurname( row.data( "surname" ).toString( ) );
  83.  
  84.        _cache.Add( user, row.data( "id" ).toInt( ) );
  85.      }
  86.    }
  87.  
  88.    // No hace falta leer toda la tabla... se puede leer bajo demanda para
  89.    // reducir los tiempos de acceso.
  90.    void LoadUsers( const std::string& name );
  91.  
  92.  private:
  93.  
  94.    UsersCache& _cache;
  95.  
  96. };

Y esto es todo. Creo que el diseño final es elegante, ya que reduce enormemente la dependencia entre clases y permite crear rápidamente enlaces con bases de datos, ficheros XML, ficheros binarios, sockets, etc.

También hay que tener en cuenta que el diseño que he planteado se puede adaptar de forma más o menos sencilla para que funcione bajo diferentes escenarios: Registros sin clave primaria, Registros con clave primaria compuesta, Clave primaria repetida, etc. Yo, personalmente, diseñaría un template de cache específico para cada caso... no soy especialmente partidario de hacer un template que herede de otro.

Y poco más que contar. Espero que os haya gustado, que hayáis aprendido algo o, en el peor de los casos, me conformo con que no penséis que habéis perdido el tiempo al leer este artículo.

Un saludo.
2  Programación / Programación C/C++ / Aporte C++: Utilidades de la STL que tal vez no conoces en: 2 Julio 2014, 13:29 pm
Muchas veces, al hacer programas nos dedicamos a reinventar la rueda. Unas veces es para aprender, pero otras muchas es por simple desconocimiento de las herramientas de las que disponemos.

He aquí algunos ejemplos, aunque hay que tener en cuenta lo siguiente:

  • Algunos ejemplos solo funcionarán con compiladores compatibles con C++11.
  • Para entender los ejemplos es necesario tener una base mínima ( familiarizarse con contenedores e iteradores, por ejemplo )
  • Los ejemplos no intentan hacer uso del algoritmo más óptimo o más rápido o más ... son simplemente ilustrativos. La opción más óptima dependerá de los requisitos propios de cada aplicación.

Si el aporte os resulta interesante intentaré ampliarlo conforme tenga tiempo libre.

Ordenar un vector:

std::sort es el primero de los grandes desconocidos. Esta función, bien utilizada, nos permite ordenar con facilidad casi cualquier tipo de vector, lista, arreglo, ... para ello nos apoyamos en las funciones std::begin y std::end, nuevas en C++11, que permiten obtener iteradores incluso para arreglos.

Si usamos C++11, podemos también diseñar nuestras propias funciones de ordenación basándonos en funciones lambda, como se verá más adelante.

Apoyándonos en las funciones std::begin y std::end podemos
Código
  1. #include <algorithm>
  2. #include <functional>
  3. #include <iostream>
  4.  
  5. int main( )
  6. {
  7.  int numeros[] = {3, 5, 7, 2, 9, 1, 4, 8, 6 };
  8.  
  9.  std::cout << "Orden de mayor a menor" << std::endl;
  10.  std::sort ( std::begin(numeros), std::end(numeros), std::greater< int >( ) );
  11.  for ( auto it = std::begin( numeros ); it != std::end( numeros ); ++it )
  12.    std::cout << *it << ' ';
  13.  std::cout << std::endl;
  14.  
  15.  std::cout << "Orden de menor a mayor" << std::endl;
  16.  std::sort ( std::begin(numeros), std::end(numeros), std::less< int >( ) );
  17.  for ( auto it = std::begin( numeros ); it != std::end( numeros ); ++it )
  18.    std::cout << *it << ' ';
  19.  std::cout << std::endl;
  20.  return 0;
  21. }

Conteo de elementos en un vector

En el foro suele ser habitual que alguien pida ayuda saber cuántos elementos de un arreglo cumplen una serie de características...

Normalmente las soluciones pasan por poner un for con uno o varios ifs en su interior... con lo limpio que puede llegar a quedar usando las instrucciones adecuadas.

greater_equal es un template que implementa el operador función. Dicho operador requiere 2 parámetros... el primero es el valor a comprobar y el segundo el valor de referencia. El operador devuelve true si el primer parámetro es mayor o igual al segundo. Debido a que el parámetro que sirve de referencia es el segundo, se hace necesario usar la funcion bind2nd "bind second", que permite asignar un valor fijo al segundo parámetro a lo largo de todas las llamadas.

finalmente count_if se encarga de recorrer los iteradores y realiza un conteno de todas las comprobaciones que arrojan un resultado positivo.

El resultado es una comparación programada en un par de líneas.

Para comparaciones más complejas se puede usar una expresión lambda, una función propia o una clase que implemente el operador función, como puede verse en los ejemplos.

Código
  1. #include <algorithm>
  2. #include <functional>
  3. #include <iostream>
  4.  
  5. bool pares( int num )
  6. {
  7.  return num % 2 == 0;
  8. }
  9.  
  10. class DivisiblePorN
  11. {
  12.  public:
  13.  
  14.    DivisiblePorN( int divisor )
  15.      : _divisor( divisor )
  16.    { }
  17.  
  18.    bool operator( )( int numero )
  19.    {
  20.      return ( numero % _divisor == 0 );
  21.    }
  22.  
  23.  private:
  24.  
  25.    int _divisor;
  26. };
  27.  
  28. int main( )
  29. {
  30.  int numeros[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
  31.  int resultado = std::count_if( std::begin( numeros ), std::end( numeros ),
  32.                                 std::bind2nd( std::greater_equal< int >( ), 7 ) );
  33.  std::cout << "Numeros mayores o iguales a 7: " << resultado << std::endl;
  34.  
  35.  resultado = std::count_if( std::begin( numeros ), std::end( numeros ),
  36.                             []( int num ){ return num%2; } );
  37.  std::cout << "Numeros impares: " << resultado << std::endl;
  38.  
  39.  resultado = std::count_if( std::begin( numeros ), std::end( numeros ), pares );
  40.  std::cout << "Numeros pares: " << resultado << std::endl;
  41.  
  42.  DivisiblePorN divisible3( 3 );
  43.  resultado = std::count_if( std::begin( numeros ), std::end( numeros ), divisible3 );
  44.  std::cout << "Numeros divisibles entre 3: " << resultado << std::endl;
  45.  
  46.  return 0;
  47. }

Intercambiar el valor de dos variables

El titular lo dice todo... podemos hacer uso la opción larga que todo el mundo conoce ( en cualquiera de sus variantes ) o dejar que la stl haga el "trabajo sucio:

Código
  1. #include <algorithm>
  2. #include <iostream>
  3. #include <utility>
  4. #include <vector>
  5.  
  6. int main( )
  7. {
  8.  int x = 1;
  9.  int y = 2;
  10.  
  11.  std::swap( x, y );
  12.  
  13.  std::cout << "X = " << x << "; Y = " << y << std::endl << std::endl;
  14.  
  15.  int v1[] = { 1, 2, 3, 4 };
  16.  int v2[] = { 5, 6, 7, 8 };
  17.  std::swap( v1, v2 );
  18.  
  19.  std::cout << "v1 = {";
  20.  for ( auto i : v1 )
  21.    std::cout << " " << i;
  22.  std::cout << " }" << std::endl;
  23.  std::cout << "v2 = {";
  24.  for ( auto i : v2 )
  25.    std::cout << " " << i;
  26.  std::cout << " }" << std::endl << std::endl;
  27.  
  28.  std::vector< int > vector1 = { 10, 20, 30, 40 };
  29.  std::vector< int > vector2 = { 50, 60, 70, 80 };
  30.  std::swap( vector1, vector2 );
  31.  
  32.  std::cout << "vector1 = {";
  33.  for_each( vector1.begin( ), vector1.end( ), []( int num ){ std::cout << " " << num; } );
  34.  std::cout << " }" << std::endl;
  35.  
  36.  std::cout << "vector2 = {";
  37.  for_each( vector2.begin( ), vector2.end( ), []( int num ){ std::cout << " " << num; } );
  38.  std::cout << " }" << std::endl;
  39.  
  40.  return 0;
  41. }

Sumar valores

"Tengo que devolver la suma de todos los valores del arreglo"... ¿te suena esta frase? La primera solución que aparece en la mente de más de uno pasa por inicializar un contador, poner un bucle for e ir acumulando valores... no hace falta:

Código
  1. #include <functional>
  2. #include <iostream>
  3. #include <numeric>
  4.  
  5. int main( )
  6. {
  7.  int numeros[] = { 20, 45, 86, 91, 10, 25, 4, 17, 62 };
  8.  
  9.  std::cout << "Suma total: ";
  10.  std::cout << std::accumulate(std::begin( numeros ), std::end( numeros ), 0 );
  11.  std::cout << std::endl;
  12. }

¿Y si sólo queremos saber la suma de aquellos que cumplan una condición determinada? Sin problemas, únicamente tenemos que aplicar lo que hemos visto en apartados anteriores:

Código
  1. #include <functional>
  2. #include <iostream>
  3. #include <numeric>
  4.  
  5. class AddLessThanN
  6. {
  7.  public:
  8.  
  9.    AddLessThanN( int n )
  10.      : _limit( n )
  11.    {
  12.  
  13.    }
  14.  
  15.    int operator( )( int accum, int x )
  16.    {
  17.      if ( x < _limit )
  18.        accum += x;
  19.  
  20.      return accum;
  21.    }
  22.  
  23.  private:
  24.  
  25.    int _limit;
  26. };
  27.  
  28. int main( )
  29. {
  30.  int numeros[] = { 20, 45, 86, 91, 10, 25, 4, 17, 62 };
  31.  
  32.  AddLessThanN lessThan( 20 );
  33.  std::cout << "Sumar elementos menores que 20: ";
  34.  std::cout << std::accumulate(std::begin( numeros ), std::end( numeros ), 0, lessThan );
  35.  std::cout << std::endl;
  36. }

Eliminar elementos de un vector

La opciónes tradicionales pasan todas por usar un bucle for y luego, o hacer malabares con los iteradores, añadir los elementos a eliminar en una lista... o crear una lista nuevo con los elementos válidos...

Código
  1. #include <algorithm>
  2. #include <iostream>
  3. #include <vector>
  4.  
  5. int main( )
  6. {
  7.  std::vector< int > numeros = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  8.  
  9.  std::vector< int >::iterator it = std::remove_if ( numeros.begin( ), numeros.end( ), [](int num){ return num%2; } );
  10.  numeros.erase( it, numeros.end( ) );
  11.  
  12.  std::cout << "Solo pares: ";
  13.  for_each( numeros.begin( ), numeros.end( ), []( int num){ std::cout << num << " "; } );
  14.  std::cout << std::endl;
  15.  
  16.  return 0;
  17. }

PD.: también funciona con arreglos:

Código
  1. #include <algorithm>
  2. #include <iostream>
  3. #include <vector>
  4.  
  5. int main( )
  6. {
  7.  int numeros[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
  8.  
  9.  int* itEnd = std::remove_if ( std::begin( numeros ), std::end( numeros ), [](int num){ return num%2 == 0; } );
  10.  
  11.  std::cout << "Solo impares: ";
  12.  for_each( std::begin( numeros ), itEnd, []( int num){ std::cout << num << " "; } );
  13.  std::cout << std::endl;
  14.  
  15.  return 0;
  16. }

3  Programación / Programación C/C++ / Buenas prácticas con C++ en: 11 Febrero 2014, 12:46 pm
Visto que aquí entra mucha gente que está aprendiendo a programar, he creído conveniente indicar una serie de ideas que creo que mejoran la legibilidad, usabilidad e incluso el rendimiento del código.

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
  1. 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
  1. 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
  1. 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
  1. class VersionMala
  2. {
  3.  public:
  4.    std::string nombre;
  5.    int edad;
  6. };
  7.  
  8. class VersionBuena
  9. {
  10.  public:
  11.    std::string GetNombre( ) const;
  12.    void SetNombre( const std::string nombre );
  13.  
  14.    int GetEdad( ) const;
  15.    void SetEdad( int edad );
  16.  
  17.  private:
  18.  
  19.    std::string nombre;
  20.    int edad;
  21. };

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
  1. class VehiculoImpl;
  2.  
  3. class Vehiculo
  4. {
  5.  private:
  6.    VehiculoImpl* impl;
  7. };

* Clases pasadas como argumento, se pase la clase por valor, referencia o puntero:

Código
  1. class VehiculoImpl;
  2. class Persona;
  3.  
  4. class Vehiculo
  5. {
  6.  private:
  7.    VehiculoImpl* impl;
  8.  
  9.  public:
  10.    void SetConductor( Persona conductor );
  11. };

* Clases retornadas en un método:

Código
  1. class VehiculoImpl;
  2. class Persona;
  3.  
  4. class Vehiculo
  5. {
  6.  private:
  7.    VehiculoImpl* impl;
  8.  
  9.  public:
  10.    void SetConductor( Persona conductor );
  11.  
  12.    Persona GetConductor( ) const;
  13. };

*Lo anterior es perfectamente aplicable cuando nos encontramos con namespace:

Código
  1. class VehiculoImpl;
  2.  
  3. namespace prueba
  4. {
  5.  class Persona;
  6. }
  7.  
  8. class Vehiculo
  9. {
  10.  private:
  11.    VehiculoImpl* impl;
  12.  
  13.  public:
  14.    void SetConductor( prueba::Persona conductor );
  15.  
  16.    prueba::Persona GetConductor( ) const;
  17. };

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
  1. class clase
  2. {
  3.  public:
  4.    void func( int a, int b, std::string c )
  5.    {
  6.      if ( a != this->a && b != this->b && a < b )
  7.      {
  8.        this->a = a;
  9.        this->b = b;
  10.        this->c = c;
  11.    }
  12.  
  13.  private:
  14.    int a;
  15.    int b;
  16.    std::string c;
  17.  
  18. };

seguro que es menos claro que:

Código
  1. class Filtro
  2. {
  3.  public:
  4.  
  5.    void SetValores( int minimo, int maximo, std::string mensaje )
  6.    {
  7.      if ( minimo != _minimo && maximo != _maximo && minimo < _maximo )
  8.      {
  9.        _minimo = minimo;
  10.        _maximo = maximo;
  11.        _mensaje = mensaje;
  12.      }
  13.    }
  14.  
  15.  private:
  16.    int _minimo;
  17.    int _maximo;
  18.    std::string _mensaje;
  19. }

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
  1. int lista[10] = { 5, 2, 3, 1, 6, 5, 4, 2, 5, 1 };
  2. std::set< int > sinDuplicados( lista, lista + 10 );
  3.  
  4. for ( auto it = sinDuplicados.begin( ); it != sinDuplicados.end( ); ++it )
  5.  std::cout << *it << " ";
  6.  
  7. std::cout << std::endl;
  8.  

  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
  1. std::map< std::string, int > items;
  2. items[ "uno" ] = 1;
  3. items[ "dos" ] = 1;
  4. items[ "tres" ] = 3;
  5. items[ "dos" ] = 2;
  6.  
  7. for ( auto it = items.begin( ); it != items.end( ); ++it )
  8.  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
  1. class POO
  2. {
  3.  public:
  4.    POO( );
  5.  
  6.    // ...
  7.  private:
  8.    static bool _algo;
  9. };

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
  1. namespace
  2. {
  3.  bool _algo = false;
  4. }
  5.  
  6. POO::POO( )
  7. {
  8.  _algo = true;
  9. }
  10.  

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
  1. class Usuario
  2. {
  3.  public:
  4.    void GuardarUsuario( std::ostream& stream );
  5. };

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
  1. class POO
  2. {
  3.  public:
  4.    void Func( ) const;
  5.  
  6.  private:
  7.    int _dato;
  8. };
  9.  
  10. void POO::Func( ) const
  11. {
  12.  // instruccion no valida
  13.  _dato = 5;
  14.  
  15.  // este codigo no da error, compila y funciona.
  16.  POO* ptr = const_cast< POO* >( this );
  17.  ptr->_dato = 5;
  18. }

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
  1. bool resultado = Func( );
  2.  
  3. resultado && Func2( );

Código
  1. bool resultado = Func( );
  2.  
  3. if ( resultado )
  4.  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.
Páginas: [1]
WAP2 - Aviso Legal - Powered by SMF 1.1.21 | SMF © 2006-2008, Simple Machines