Ziirish's Home :: Blog

Ziirish's Pub

 
 

KISS pour Keep It Simple, Stupid! J'espère que la plupart de mes lecteurs (merci public) qui font du développement connaissent ce principe de développement.

Je ne reviendrai pas sur la définition de ce principe, Internet regorge d'infos là-dessus.

Je vais me contenter de vous présenter un cas concret. Vous vous souvenez peut-être de cet article ou bien de celui-là. Le principe KISS était déjà appliqué. Le résultat, c'est un code très étalé, comportant de nombreuses fonctions, mais on gagne énormément en maintenabilité/ajout de fonctionnalités et surtout généricité. Mais cela implique plusieurs choses. Tout d'abord, il faut pensé à l'avance au besoin d'évolution. Ensuite, il faut documenter/commenter un minimum pour s'y retrouver.

Alors vous me direz, "ouay, 'fin, sans se faire chier avec ton principe, si on commente/documente ça revient au même".

C'est presque vrai. De toute façon, un code non commenté ne devrait même pas avoir appellation d'origine contrôlée code. Cependant, les fonctions de 5000 lignes qui font 3000 choses, ça me donne la nausée. Le code de CID comporte une fonction de plus de 1000 lignes, et je n'ose pratiquement pas y mettre les mains dedans. Elle est "splitable", et je compte effectuer ce split un jour, mais elle a bien un rôle unique qui nécessite un code conséquent.

Mais passons à autre chose de plus concret. Dernièrement, j'ai eu besoin de fonctions de manipulation de chaînes de caractère, mais je n'avais pas envie de m'encombrer avec des expressions régulières complexes et "trop puissante" par rapport à mon besoin. J'ai donc réutilisé mon implémentation de "l'ArrayList" :


static void
datacase_replace (DataCase *pCase, gpointer *pData)
{
    if (pCase != NULL)
    {
        gchar **c_tmp = pData[2];
        if (GPOINTER_TO_INT(pData[0]) < GPOINTER_TO_INT(pData[1]))
        {
            gchar *tmp = g_strdup(*c_tmp);
            g_free (*c_tmp);
            *c_tmp = NULL;
            *c_tmp = g_strdup_printf ("%s%s%s",tmp,pCase->content->string,pData[3]);
            g_free (tmp);
        } 
        else
        {
            gchar *tmp = g_strdup(*c_tmp);
            g_free (*c_tmp);
            *c_tmp = NULL;
            *c_tmp = g_strdup_printf ("%s%s",tmp,pCase->content->string);
            g_free (tmp);
        }
    }
}

void
str_replace_all (gchar **string, const gchar *sFrom, const gchar *sTo)
{
    g_return_if_fail (*string != NULL && sFrom != NULL && sTo != NULL);
    
    gchar **tmp = g_strsplit(*string,sFrom,0);
    DataTable *t_temp = datatable_new();
    while (*tmp != NULL)
    {
        datatable_append(&t_temp,datacontent_new_string(*tmp));
        g_free (*tmp);
        tmp++;
    }
    size_t size = datatable_length(t_temp);
    if (size < 2)
    {
        free_datatable(&t_temp);
        return;
    }
    int length = (strlen(*string)+((strlen(sTo)-strlen(sFrom))*size))*sizeof(gchar)+1;
    g_free (*string);
    *string = NULL;
    *string = g_malloc0(length);
    gpointer *pData = g_new(gpointer,5);
    pData[0] = GINT_TO_POINTER(0);
    pData[1] = GINT_TO_POINTER(size);
    pData[2] = string;
    pData[3] = (gchar *)g_strdup(sTo);
    datatable_foreach(t_temp,(DataAction)datacase_replace,pData);
    free_datatable(&t_temp);
    g_free (pData[3]);
    g_free (pData);
}

Cela implique d'avoir une chaîne allouée dynamiquement (à l'aide de malloc), mais on peut faire des choses intéressantes :


$ cat test.c
#include <stdlib.h>
#include <stdio.h>
#include "arraylist.h"

int
main (int argc, char **argv)
{
    gchar *string = g_strdup("bonjour %u");

    str_replace_all (&string, "%u", g_getenv("USER");

    fprintf (stdout, "%s\n", string);

    g_free (string);

    return 0;
}
$ ./test
bonjour ziirish

Bien entendu on peut faire ça sans les datatables, mais ici je voulais vous illustrer la réutilisation de code. Le principe, on découpe la chaîne de départ en plusieurs sous-chaînes stockées dans une datatable, puis on réassemble le tout en incluant la chaîne de substitution entre chaque sous-chaîne.

Et voici un exemple de modularité du code. J'avais besoin de permettre aux utilisateurs de mon application d'entrer n motifs de chaînes. Une liste variable ? Utilisons les datatables !

On modifie donc légèrement le code existant, et on utilise un vilain hack pour générer un nouveau GType :


/// Structure représentant un couple regex/replacement
typedef struct _Substitute
{
    /// la regex recherchée
    gchar *regex;
    /// par quoi on remplace
    gchar *replacement;
} Substitute;

/// Structure représentant le contenu d'une case
struct _CidDataContent {
    /// structure anonyme servant à encapsuler un seul type de donnée dans une case
    union {
        /// type chaine de caractère
        gchar *string;
        /// type entier
        gint iNumber;
        /// type booléen
        gboolean booleen;
        /// type #Substitute
        Substitute *sub;
    };
    /// type contenu dans la case
    GType type;
};

/// Définition d'un nouveau type 'SUBSTITUTE'
#define TYPE_SUBSTITUTE G_TYPE_MAKE_FUNDAMENTAL (49)

DataTable *
cid_create_datatable (GType iDataType, ...)
{
    DataTable *res = datatable_new();
    GType iCurrType = iDataType;
    va_list args;
    va_start(args,iDataType);
    void *current;
    while ((GType) (current = va_arg(args,gpointer)) != G_TYPE_INVALID) {
        DataContent *tmp = NULL;
        if ((GType) current == G_TYPE_BOOLEAN || (GType) current == G_TYPE_INT 
            || (GType) current == G_TYPE_STRING || (GType) current == TYPE_SUBSTITUTE)
        {
            iCurrType = (GType) current;
            continue;
        }
        switch (iCurrType) 
        {
            case G_TYPE_BOOLEAN:
                tmp = datacontent_new_boolean(current);
                break;
            case G_TYPE_STRING:
                tmp = datacontent_new_string(current);
                break;
            case G_TYPE_INT:
                tmp = datacontent_new_int(current);
                break;
            case TYPE_SUBSTITUTE:
                tmp = datacontent_new_substitute(current);
                break;
            default:
                iCurrType = (GType) current;
        }
        datatable_append(&res,tmp);
    }
    va_end(args);
    return res;
}

void
free_datacontent_full (DataContent *pContent, gpointer *pData)
{
    if (pContent != NULL)
    {
        if (pContent->type == G_TYPE_STRING && pContent->string != NULL)
            g_free(pContent->string);
        if (pContent->type == TYPE_SUBSTITUTE)
            free_substitute (pContent->sub);
        g_free(pContent);
    }
}

DataContent *
datacontent_new (GType iType, gpointer value)
{
    DataContent *ret = g_new0(DataContent,1);
    if (ret != NULL)
    {
        ret->type = iType;
        switch (iType) 
        {
            case G_TYPE_STRING:
                ret->string = NULL;
                int iLength = (strlen((gchar *) value)+1)*sizeof(gchar);
                ret->string = g_malloc0(iLength);
                strncpy(ret->string, (gchar *) value, iLength);
                break;
            case G_TYPE_INT:
                ret->iNumber = (gint)(long) value;
                break;
            case G_TYPE_BOOLEAN:
                ret->booleen = (gboolean)(long) value;
                break;
            case TYPE_SUBSTITUTE:
                ret->sub = (Substitute *) value;
                break;
            default:
                g_free(ret);
                return NULL;
        }
    }
    return ret;
}

Il ne nous reste plus qu'à implémenter nos nouvelles fonctions :


static void
proceed_substitute (DataCase *pCase, gpointer *pData)
{
    str_replace_all (pData[1],pCase->content->sub->regex,pCase->content->sub->replacement);
}

void 
substitute_user_params (gchar **cPath)
{
    DataTable *table = create_datatable (CID_TYPE_SUBSTITUTE, 
                                                new_substitute ("%artist%",musicData.playing_artist ? 
                                                                               musicData.playing_artist :
                                                                               ""),
                                                new_substitute ("%album%",musicData.playing_album ?
                                                                              musicData.playing_album :
                                                                              ""),
                                                new_substitute ("%home%",g_getenv ("HOME")),
                                                new_substitute ("%user%",g_getenv ("USER")),
                                                G_TYPE_INVALID);

    gpointer *pData = g_new0(gpointer, 2);
    pData[0] = GINT_TO_POINTER(0);
    pData[1] = cPath;
    datatable_foreach (table, (DataAction) proceed_substitute, pData);
    free_datatable (&table);
}

Substitute *
new_substitute (const gchar *regex, const gchar *replacement)
{
    Substitute *ret = g_new0 (Substitute, 1);
    if (ret != NULL)
    {
        ret->regex = g_strdup (regex);
        ret->replacement = g_strdup (replacement);
    }
    return ret;
}

void
free_substitute (Substitute *pSub)
{
    if (pSub == NULL)
        return;
    g_free (pSub->regex);
    g_free (pSub->replacement);
    g_free (pSub);
}


On voit toujours ici qu'il y a des fonctions qui ne font pas grand chose : KISS.

Cela nous permet de remplacer plusieurs motifs dans la chaîne en une seule commande. Et en poussant le bouchon encore plus loin, l'utilisateur entrant une liste de patterns, j'encapsule cette dernière dans une datatable.

On voit donc ici qu'avec un principe de base assez simple : "l'ArrayList", on arrive à l'encapsuler récursivement pour arriver à différents résultats.

Cela aura nécessité quelques mises au point sur les fonctions "de base", mais chaque nouvelle fonctionnalité n'aura demandé que l'implémentation d'une ou deux fonctions supplémentaires. On économise donc de la réécriture de code. Mais cela nécessite une écriture ultra généraliste à la base.