Le sujet de cet article est de présenter une solution technique en C pour avoir une interface et son implémentation.

Pas d'objet en C, donc ... WTF ?

Alors, évidemment, on n'a pas d'objet en C, donc parler d'interface est un abus de langage. Le problème qui servira à exposer notre solution est celui ci : on veut définir une interface qui pose les opérations de base sur une base de données : connexion, exécution d'un ordre, et récupération des résultats. Cette interface est implémentée par du code qui permet l'usage d'une base de données Postgresql.

 

On espère écrire du code comme suit :

struct database * db= psqldatabase_new(host,port,username,password,schema);

Où la fonction psqldatabase_new(host,port,username,password,schema) renvoie une instance opérationnelle vers une database postgresql. Cet objet lui même va renvoyer une connexion postgresql, et quand on fera un select, le resultset que la connexion créé sera évidemment adapté au postgresql. Finalement, c'est un "tout en un" qui part de la database et qui utilise l'implémentation postgresql définie. Le but est que tout cela soit complètement transparent, de sorte que l'on puisse utiliser normalement le code :

struct dbconnection * connection = database_connect(db);
struct dbresultset * rs = dbconnection_select(connection,"select continent_name from continents");
// check meta data 
unittest_assert(counter, 1 == dbresultset_columns(rs));
unittest_assert(counter, strcmp(dbresultset_name(rs,0),"continent_name") == 0 );
//counter of lines 
int lines = 0 ; 
while(dbresultset_hasnext(rs)) {
   ++lines; 
   dbresultset_next(rs);
}
unittest_assert(counter, lines > 0 );
// clean them all 
dbresultset_delete(rs);
dbconnection_delete(connection);
database_delete(db);

Sans avoir à se soucier si la base derrière est effectivement du postgresql, ou un mariadb, ou etc. Finalement, on voit que le seul couplage est à la création de la database, le reste du code ne devant pas du tout changer si on passe à une autre base de donnée.

Solution proposée

La solution choisie, qui sera commentée dans la suite de l'article, est celle ci  :

Le principe est d'utiliser :

  1. Au niveau de la définition générale de databases, un void * pour l'implémentation, et des pointeurs de fonctions qui utilisent cette implémentation.
  2. Au niveau de chaque implémentation (par exemple pour une base de données postgresql), les structures fournies par la librairie tierce. Et le code en question va crééer la database en utilisant le void * pour la structure et les pointeurs de fonctions pour les fonctions propres à cette base

Le lien entre les fonctions exposées par database et les pointeurs de fonction de sa structure est de faire "passe plat" pour

Coté database

Le code du header :

/**
 * A generic database definition, that is 
 * a name 
 * an implementation provided by the developers 
 * a way to clean the implementation
 * a way to create a connection  
 * 
 */
struct database {
    /** description or name of the database (for instance "sqlite implementation" */
    char * dbname ; 
    /** actual implementation by the developers */
    void * implementation ; 
    /** a special free implementation to clean the database. 
     * @param the database implementation to close  */
    void (*database_freeimplementation)(void * implementation);
    /** a way to open a connection for that database
     * @param a generic pointer to the implementation of the database connection parameters  
     * @return null for invalid parameters, a valid open connection otherwise */
    struct dbconnection * (*database_openconnection)(void * implementation );
};

/**
 * Open a connection to the database 
 * @param database the database to connect to 
 * @return NULL for NULL or errors, the valid connection if no error occured
 */
struct dbconnection * database_connect(struct database * database); 

/**
 * Delete the database and free the underlying memory 
 * @param the database to delete 
 */
void database_delete(struct database * );

Chaque implémentation va pouvoir utiliser le void * comme elle le souhaite, et devra par contre alimenter les pointeurs de fonction de database pour que le code marche coté database :

/**
 * Information to access the POSTGRESQL database
 * That is : 
 * host to connect to 
 * port that listens on connections
 * username to connect with 
 * password to connect with
 * default schema to connect to 
 */
struct psqldbinformations {
    char * host; // machine to connect to 
    char * port ; // port to connect to 
    char * username ;  // username for the connection 
    char * password;   // password for the connection 
    char * defaultSchema; // schema to connect to 
};
/**
 * Converts all data in parameter to an instance of psqldbinformations 
 * @param host the machine to connect to the database
 * @param port the port to connect to the database
 * @param username user name to connect to the database
 * @param password the password of the uer to connect to the database
 * @param schema the default schema to connect to 
 * @return if a parameter is null, then null, else the correct filled instance 
 */
static struct psqldbinformations * psqldbinformations_new(const char * host, int port,
        const char * username, const char * password,const char * schema) {
  // just copy the stuff
    return result;
}

/**
 * Delete the connection informations. 
 * Has no effect on NULL 
 * @param informations the informations to delete 
 */
static void psqldbinformations_delete(struct psqldbinformations * informations) {
  // basic free calls
}

/**
 * Constructs a new database instance 
 * @param host machine to connect to 
 * @param port host to connect to 
 * @param username user name to connect with
 * @param password password to connect with 
 * @param schema schema to connect to 
 * @return an instance of the dabase or null for null parameters 
 */
struct database * psqldatabase_new(const char * host, int port,
        const char * username, const char * password,const char * schema) {
    if (!host || port < 0 || !username || !password || !schema) return NULL ; 
    struct database * result = malloc(sizeof(struct database));
    result->dbname = stringutils_copy(PSQL_DBNAME);
    result->implementation = (void *) psqldbinformations_new(host,port,username,password,schema); 
    result->database_freeimplementation = &psqldbinformations_genericdelete;
    result->database_openconnection = &psqldbconnection_genericnew;
    return result; 
}

On voit bien du coup que notre implémentation sera remplie avec un struct psqldbinformations qui contient essentiellement de quoi se connecter au serveur postgresql. Les fonctions psqldb sont définies dans un psqldatabases.h qui va, lui, manipulier les PGConn * (les connexions postgresql définies par l'API mises à disposition par la librairie postgresql), et tout ce qui est fourni en général par cette API.

Coté implémentation

Prenons l'exemple central de la gestion des connexions. Par définition, l'implémentation doit répondre à ce contrat, défini coté databases.h

/** A generic db connection is 
 * an implementation 
 * a way to pass a query (execute for non select orders, select for selections) 
 * and a way to clean it */
struct dbconnection {
    /** implementations will use this generic part of the memory */
    void * implementation; 
    /** implementations will provide a way to close and clean the implementation. 
     * Free the implementation, no effect on NULL *
     * @param the implementation to free 
     */
    void (*dbconnection_freeimplementation)(void * implementation);
    /** execute a select query with the connection. To be defined by the implementation. 
     * @param the implementation pointer, that is the actual connection to execute a query on 
     * @param the query to run on the database (for example select count(*) from countries)
     * @return NULL for null connections or errors, a valid iteator otherwise
     */
    struct dbresultset * (*dbconnection_select)(void * connection, const char * query);
    /** execute a query with the connection. To be defined by the implementation. 
     * @param the implementation pointer, that is the actual connection to execute a query on 
     * @param the query to run on the database 
     * @return 1 on success, 0 for error
     */    
    int ( * dbconnection_execute)(void * connection, const char * query);
};

Voilà comment le faire :

////////////////////////////
// CONNECTIONS MANAGEMENT //
////////////////////////////

/**
 * Creates a postgresql connection to the database
 * @param informations the informations to connect with (user, server, schema)
 * @return a connection to the psql database, or null for errors
 */
static PGconn * psqldbconnection_new(struct psqldbinformations * informations) {
    const char * dbhost = informations->host; 
    const char * login = informations->username; 
    const char * password = informations->password; 
    const char * port = informations->port; 
    const char * schema = informations->defaultSchema ; 
    PGconn * connection = PQsetdbLogin(dbhost, port, NULL, NULL, schema, login, password);
    // test the status of the connection 
    ConnStatusType status = PQstatus(connection);
    if (status != CONNECTION_OK) {
        fprintf(stderr, "Connection creation failed : STATUS IS \t\t%i\n", status);
        if (connection) PQfinish(connection);
        return NULL ; 
    }
    return connection; 
}

/**
 * Run a non select query on the connection 
 * @param connection the connection to run the query on 
 * @param query the query to run 
 * @return 1 for success, 0 for error
 */
static int psqldbconnection_execute(PGconn * connection, const char * query) {
    if (!connection || !query) return 0 ; 
    PGresult * result = PQexec(connection,query);
    if (PQresultStatus(result) != PGRES_COMMAND_OK) {
        char * error = PQcmdStatus(result);
        fprintf(stderr,"Error for the sql execute method : %s\n",error);
        PQclear(result);
        return 0 ; 
    }
    // no need to go further
    PQclear(result);
    return 1; 
}

// generic version
int psqldbconnection_genericexecute(void * connection, const char * query) {
    if (!connection || !query) return 0 ; 
    return psqldbconnection_execute((PGconn * ) connection,query);
}

/**
 * Closes a connection and delete the memory.
 * @param connection the postgresql connection to delete 
 */
static void psqldbconnection_delete(PGconn * connection) {
    if (connection) {
        PQfinish(connection);
    }
}

/**
 * Closes a connection and delete the memory. 
 * This is the generic version of the psqldbconnection_delete
 * @param connection the connection to delete
 */
void psqldbconnection_genericdelete(void * connection) {
    if (connection) psqldbconnection_delete((PGconn *) connection); 
}

/**
 * Constructs a new instance of a generic connection 
 * @param db a generic pointer to the postgresql connection informations 
 * @return a valid connection to the database 
 */
struct dbconnection * psqldbconnection_genericnew(void * implementation) {
    struct psqldbinformations * informations = (struct psqldbinformations * ) implementation;
    PGconn * connection = psqldbconnection_new(informations);
    struct dbconnection * result = malloc(sizeof(struct dbconnection));    
    result->implementation = (void *) connection;
    result->dbconnection_select = &psqldbresultset_genericnew;
    result->dbconnection_execute = &psqldbconnection_genericexecute; 
    result->dbconnection_freeimplementation = &psqldbconnection_genericdelete;
    return result; 
}

Dans cette partie du code, on voit qu'on a du code dédié à la gestion de postgresql, qui utilise bien une instance de PGConn pour la connexion à la base. Le code exécute son rôle, et il en existe à chaque fois une version générique, qui utilise un void * qu'elle va caster ensuite en PGConn * , faisant effectivement le lien avec l'implémentation propre à postgresql. On voit dans la dernière partie du code qu'on va utiliser ces fonctions génériques pour les mettre dans les pointeurs de fonctions. On renvoie bien une dbconnection, qui est définie coté database. Il reste à boucler la boucle coté databases.c en cachant les appels aux pointeurs de fonctions :

// just call the underlying function and check for nulls
struct dbresultset * dbconnection_select(struct dbconnection * connection, const char * query) {
    struct dbresultset * result = NULL ; 
    if (connection && connection->implementation && query && connection->dbconnection_select) {
        result = (connection->dbconnection_select)(connection->implementation,query);
    }
    return result; 
}

// delete the caller too 
void dbconnection_delete(struct dbconnection * connection) {
    if (connection) {
        if (connection->implementation && connection->dbconnection_freeimplementation) {
            (connection->dbconnection_freeimplementation)(connection->implementation);
        }
        free(connection);
    }
}

Conclusion

Oui, c'est un article technique. Le but était de présenter comment on peut découpler fortement une interface (donc un contrat général) avec une implémentation. Sur le principe, ce contrat se matérialise par un void * qui servira à contenir les structures nécessaires à l'implémentation, ainsi que les pointeurs de fonction renvoyant un résultat générique et prenant l'implémentation en paramètre. Et le code qui va se charger de l'implémentation va utiliser le void * à sa guise, avec du code "bas niveau" qui va faire le lien avec la vraie implémentation choisie. Il va surtout cacher les "détails techniques" de son code en exposant des fonctions qui utilisent le void * comme implémentation et qui renvoient les objets génériques. Voilà. Sur le principe, c'est "simple", mais l'implémentation nécessite un peu de pratique.

 

 

Comments are closed.

Set your Twitter account name in your settings to use the TwitterBar Section.