Modifier

Convention de codage dans mes projets WinDev®

Cette page détermine la convention de codage utilisée dans mes projets. Elle est amenée à évoluer régulièrement et il se peut que certains de mes projets ne soient pas à jour. Actuellement, elle se base sur la version 25 de WinDev.

Vous avez le droit de réutiliser et de modifier cette convention pour l'adapter à vos besoins. Elle n'a pas pour vocation à devenir la convention que vous devez utiliser, mais je l'ai conçue en prenant en compte les contraintes imposées par WinDev® tel que je l'utilise.

WLangage - Les contraintes

Quand on est habitué à travailler avec d'autres langages, on remarque que le WLangage est vraiment à part. Des choix très particuliers ont été faits et ont un impact très fort sur les habitudes de codage. Je pense personnellement que ces contraintes sont en place pour permettre à des codeurs débutants d'appréhender beaucoup plus facilement le langage. Mais une fois que l'on acquiert de l'expérience, il faut faire avec.

Voici les principales contraintes à prendre en compte :

La langue de codage

WinDev est le seul langage que je connaisse à pratiquer cela, on peut écrire dans la langue de son choix : en français ou en anglais (et peut-être en espagnol). Développant avec plusieurs langages différents, j'ai décidé de coder en anglais. Tout simplement parce que je trouve la langue anglaise plus pratique et plus rapide à coder.

La langue française présente plusieurs inconvénients :

Les accents et autres lettres spécifiques au français

Ces lettres posent problèmes car elles ont un encodage particulier. Cet encodage peut poser des contraintes dans certains cas (la sérialisation par exemple). On peut éviter ce problème en se forçant à ne pas les utiliser, mais la lecture du code devient plus compliquée. Par exemple, les mots téléchargé et telecharge n'ont pas la même signification. Il est compliqué de lire recu ou envoye pour envoyé et reçu. Les participes passés sont énormément utilisés dans le nommage, de ce fait, cette contrainte est vraiment pénible.

La longueur des mots

Le français est une langue très verbeuse. La plupart des mots anglais sont beaucoup plus courts que leur traduction en français.

Par exemple, les mots set et get se traduisent par définir et obtenir.

Le code est ainsi moins lourd à lire et à écrire.

Le masculin / féminin

La langue française aime bien modifier les mots suivant s'ils sont masculins ou féminins. L'anglais nous évite cette contrainte dans la plupart des cas.

La casse est ignorée

De mon point de vue, c'est la pire des contraintes. Ce n'est vraiment pas pratique. Un simple exemple permet de comprendre le problème. Dans les autres langages, je suis habitué à mettre une première lettre majuscule pour le nom des classes, et à utiliser le même nom, mais avec une première lettre minuscule pour manipuler l'objet instancié. Avec WinDev, on ne peut pas. La classe Customer et l'objet customer ne peuvent pas co-exister.

Beaucoup de mots réservés

Dans la plupart des langages, je pense qu'on a entre 20 et 50 mots réservés. La belle affaire, on peut se passer de ces mots. En WLangage, on a aussi des mots réservés (RETOUR ou RENVOYER par exemple), mais on a aussi une quantité phénoménale de fonctions qui sont chargées en permanence. On doit tenir compte de ces fonctions dans notre convention de codage, sinon, on risque d'avoir des warnings disant que tel mot est réservé. De plus, la coloration syntaxique a parfois du mal avec ces mots réservés. Ceux qui m'embêtent le plus, ce sont les fonctions Error et Message que je ne peux pas utiliser comme des variables.

Comment faire alors ?

J'étudie et je teste en permanence les différentes possibilités pour écrire un mot. Cette convention a évolué sur plusieurs années pour devenir celle que je vous propose. Elle évoluera encore, mais son objectif est d'être la plus simple à utiliser. Je vous invite à étudier minutieusement chacun des points abordés pour que vous puissiez vous faire votre propre idée.

Les dépendances / Trop de notions globales

WinDev contient énormément d'éléments globaux, directement disponibles dans l'espace de nom. Ces noms ne peuvent pas être réutilisés facilement sans créer d'ambiguïté.

Voici une liste non exhaustive de ces éléments :

Non seulement ces éléments appauvrissent l'espace de nom déjà limité, mais en plus, le simple fait de les utiliser n'importe où dans le code peut créer des liens de dépendance avec ces différents éléments (les dépendances mal gérées, c'est le mal absolu dans le développement). Pire, la création d'un élément avec un nom similaire (le nom d'un paramètre de procédure par exemple), peut créer une ambiguïté et provoquer des bugs.

La complétion automatique

Dans un IDE, la complétion automatique est un outil très puissant qui fait gagner beaucoup de temps. Il faut trouver une convention qui permet de maximiser l'emploi de cet outil. Vu qu'elle propose tous les éléments locaux, mais aussi les éléments globaux, il faut faire en sorte de trouver ce que l'on cherche en tapant le minimum de caractères.

Énormément de contraintes

WinDev nous apporte son lot de contraintes. Il faut malheureusement en tenir compte pour gagner en efficacité. J'ai donc fait des choix qui peuvent paraître peu intuitifs, mais j'ai pris ces décisions en me basant sur l'expérience que j'avais non seulement en WinDev, mais aussi dans d'autres langages, comme Java et Python (vous constaterez une très forte influence de ce langage).

La syntaxe de nommage

Notation hongroise

La notation hongroise est une technique de nommage qui consiste à rajouter un préfixe à un élément de programmation (variable, fonction, classe, etc...) pour indiquer son type (entier, chaîne de caractère, booléen) et parfois sa portée (globale, locale, issue d'un paramètre, etc...). (En savoir plus avec Wikipedia). Cette notation est très utilisée dans le monde WinDev. Celui-ci propose d'ailleurs un outil de préfixage automatique en fonction du type et de la portée. Pour ma part, je le désactive car j'utilise cette méthode de nommage dans des cas bien précis que j'expliquerai au fur et à mesure.

Je limite au maximum l'utilisation de la notation hongroise dans mon code pour plusieurs raisons (elles s'appliquent autant aux variables qu'aux procédures):

La première, c'est que je considère cela peu utile. La plupart du temps, la déclaration de mes variables est juste au dessus du code, je n'ai qu'à lever les yeux pour connaître son type et la portée est en règle générale locale.

La seconde, c'est que j'utilise beaucoup les structures, les classes, les interfaces et les énumérations. Je crée donc régulièrement de nouveaux types que je ne peux pas traduire en préfixe. La plupart de mes variables seraient donc préfixées par un o (pour objet).

La troisième, c'est tout simplement que cela rend le code plus verbeux et plus complexe à lire. J'aime pouvoir écrire et lire mon code comme s'il sortait d'un roman. Le préfixe est en général très compliqué à lire et on se force à l'ignorer mentalement pour relire le code. Essayez de lire un code à haute voix et dîtes moi si vous lisez les préfixes.

La quatrième raison, c'est qu'il est compliqué de se souvenir de chaque préfixe en permanence. On peut faire des erreurs et il n'existe aucun outil pour vérifier que l'on a utilisé le bon (sauf si on utilise l'outil de préfixage automatique).

La cinquième raison, c'est que cela complique la complétion automatique. La notation impose le plus souvent au moins deux caractères en préfixe. Pour obtenir la complétion automatique, il faut donc taper ces deux caractères avant de commencer à taper le nom de notre variable. On perd donc en productivité. Je souhaite trouver mon nom de variable en tapant deux ou trois caractères, pas quatre ou cinq.

La sixième raison, et la plus importante, c'est que je pratique énormément le refactoring et mon code évolue énormément. Il est fréquent que je change le type d'une variable et / ou que je la déplace. Avec la notation hongroise, je passerais mon temps à revoir le nom de chaque variable, et je ne vous parle pas des oublis.

Vous verrez tout de même dans cette convention de codage des utilisations de cette notation. Je le fais pour gérer des cas particuliers. La règle principale que je suis dans ce cas, c'est de n'utiliser un préfixe qu'à condition que cela n'empêche pas le refactoring du code.

CamelCase ou snake_case ?

Mon code alterne avec deux syntaxes particulières :

Si le CamelCase est massivement utilisé par les développeurs, le snake_case peut paraître surprenant. Mais c'est un choix que j'ai fait pour pallier un problème.

Tout dans WinDev est en CamelCase. Tout ! Les fonctions, les variables, les constantes, etc… Et tout ces éléments sont globaux et ne tiennent pas compte de la casse. Ce qui signifie que mon espace de nom (mais je l'ai déjà dit) est réduit. Je risque de créer des ambiguités à chaque variable que je crée, avec un warning disant qu'elle masque un nom déjà utilisé.

Mis à part les éléments d'un seul mot (qui sont assez rares dans mon code), je sais que chaque fois que j'en crée un nouveau, il aura un nom qui ne créera pas d'ambiguïté. Ce qui diminue le risque de bug.

Un autre avantage du snake_case, c'est que c'est assez agréable à lire et à écrire. Couplé avec le fait que je n'utilise pas de préfixe, mon code ressemble à de la prose (et se lit parfois comme un roman).

Les règles de ma convention de langage

Les fonctions

La règle

Les fonctions s'écrivent :

Par exemple PROCEDURE display_something() au lieu de PROCEDURE displaySomething()

Quand un développeur lit le nom d'une fonction, il ne doit pas ressentir le besoin d'aller lire le contenu de cette fonction. Le nom doit donc décrire exactement ce que fait la procédure. En revanche, lorsqu'il modifie quelque chose dans la fonction, il ne faut pas qu'il ait le besoin de modifier le nom de la fonction. Le nom ne donne donc aucune information sur le comment c'est fait. Un bon nom de fonction doit être abstrait et ne doit pas dépendre de son implémentation (mais le nom peut être lié aux types des paramètres).

J'applique une petite particularité sur les fonctions issues d'une collection de procédures globales. Elles sont la plupart du temps préfixées du nom de la collection de procédure ou alors, elles ont un nom suffisament long pour expliquer le contexte et limiter les ambiguités.

Si je devais créer une fonction dans le module mMath qui donne le max entre deux valeurs, je l'appelerais ainsi :

max_value is int = mMath.max(value_1, value_2)

Enfin, je ne me pose pas de question, j'utilise toujours le mot clé PROCEDURE pour mes procédures. Je n'utilise jamais le mot FONCTION car il ne peut pas être utilisé pour définir une procédure interne.

En ce qui concerne la longueur du code, mes procédures ne doivent pas faire plus d'une trentaine de lignes. Si c'est le cas, j'essaye d'extraire des sous-fonctions. En règle générale, elles font entre 2 et 10 lignes.

Les paramètres

Concernant le nombre de paramètres, hormis cas particulier, ils se situent entre 0 et 3, en favorisant le minimum de paramètre. Pour le passage de paramètres, j'use et j'abuse des paramètres nommés.

Par exemple :

let customer <- cCustomer.create(<first_name>: "Pierre", <last_name>: "Durand") 

Il arrive souvent qu'un booléen soit passé en paramètre. Dans ce genre de cas, je crée deux fonctions pour éviter de passer ce booléen.

Par exemple :

send(true) // or send(false)

PROCEDURE send(by_mail is boolean)
IF by_mail THEN
// send by mail
ELSE
// send by fax
END

devient :

send_by_mail() // or send_by_fax()
procedure send_by_mail()
// send by mail

PROCEDURE send_by_fax()
// send by fax

Les variables

Les variables s'écrivent :

Je fais en sorte que la définition de ma variable et son utilisation ne soit pas trop éloignées. En contrepartie, je fais des noms de variables très explicites, quitte à ce que le nom soit un peu long. Je préfère connaître l'objectif de ma variable plutôt que la forme de ce qu'elle contient.

Pour les tableaux, en général, je mets la variable au pluriel. C'est suffisant.

Exemples :

Les variables dans les méthodes de classe

Petite entorse à la règle des préfixes, je préfixe toujours mes membres par _. Cela me permet de les distinguer des variables locales et des paramètres. Du fait de mon développement avec la méthode TDD, il m'arrive parfois de rendre temporairement un membre public pour tester ou passer à l'étape GREEN le plus rapidement possible. Ce préfixe me signale qu'il y a un travail de refactoring à faire très rapidement, car en théorie, aucun des membres de mes classes ne doivent être publics (respect de l'encapsulation).

Exemples :

Les paramètres

Je n'utilise aucun préfixe pour distinguer un paramètre d'une variable. Par contre, je type toujours (sauf exception) mes paramètres (mais je retire le a qui n'est pas obligatoire).

PROCEDURE display(displayed_value is int)

Les variables globales fenêtres (ainsi que les états)

Lorsque j'ai besoin d'une variable fenêtre (ou état), je l'intègre dans une structure et je déclare une variable lv (Local Variable) du type de cette structure. Cela me permet d'avoir accès à toutes mes variables globales fenêtres et de les distinguer des variables locales de mes procédures. C'est aussi utile pour la complétion automatique : lv. me liste toutes les variables globales

Avec cette méthode, si j'ai besoin, je peux sérialiser / dé-sérialiser les variables de ma fenêtre, ce qui peut être pratique pour des tests.

PROCEDURE MyWindow(ids_of_selected_articles is array of strings)

GLOBAL
    cLocalVariable is Structure
        window_title                is string
        ids_of_selected_articles    is array of strings
    END

    lv is cLocalVariable

LOCAL
lv.window_title             = MyWindow..Title
lv.ids_of_selected_articles = ids_of_selected_articles

Les variables globales de collection de procédures

Je limite au maximum l'utilisation de variables globales de collection de procédures. Je n'en utilise pour ainsi dire jamais, car cela crée des dépendances.

Mais parfois, j'en ai tout de même besoin...

J'utilise aussi une structure. Il faut faire attention dans le nom de la structure, car WinDev signale les doublons si on utilise un même nom de structure dans deux collections. Pour cela, je suffixe le nom de ma structure par le nom de ma collection.

Et pour le nom, j'ai choisi gv (Global Variable).

Je peux sérialiser / dé-sérialiser mes variables, ce qui est très utile pour les tests unitaires.

Enfin, cela m'oblige à préfixer du nom de la collection car WinDev me signalera une anomalie sur l'utilisation de gv.

Par exemple, dans une collection de procédures nommée Collection1

cVariableCollection1 is structure
    parameter_path is string
FIN

gv is cVariableCollection1

Et pour l'utilisation :

Collection1.gv.parameter_path = "..."

Les variables globales projets

J'essaye de ne plus mettre de variables globales au niveau du projet. Si je dois en avoir, je les mets dans la collection de procédures adéquate, quitte à en créer une si besoin. La raison est qu'on ne peut pas réutiliser le code d'un projet.

Et si je dois vraiment en avoir, j'utilise le même système que pour les fenêtres et les collections de procédures mais j'utilise une variable que je nomme project_variable pour que l'utilisateur sente le malaise d'utiliser une telle variable.

Les constantes

Dans la plupart des langages, les constantes sont en majuscule. Mais vu les contraintes sur les majuscules vues plus haut, on ne peut pas se servir de cette notion. J'utilise donc la notation _SNAKECASE. Elle me permet de distinguer les constantes des variables. C'est très pratique.

Exemples :

CONSTANT
    _LANGAGE_ = "fr"
    _PI_ = 3.14
    _DAY_IN_HOURS_ = 24
END

J'utilise aussi cette notation pour les énumérations ou les combinaisons (pour moi, ce sont aussi des constantes).

Enfin, j'applique aussi cette règle à certaines variables que je considère comme des constantes, mais que je ne peux pas initialiser comme des constantes. Dans ce cas, j'utilise le mot clé Let.

Exemple :

Let _PATH_FILE_PARAMETER_ = fExeDir() + ["\"] + "paramèter.ini"

Les définition de types

Toutes les définitions de types suivent les règles suivantes :

Les structures, les classes et les interfaces

Exemples :

J'utilise la même notation pour les structures, les classes et les interfaces. Pourquoi ? Tout simplement parce que j'essaye de manipuler tous ces éléments de la même manière. Il m'arrive souvent de commencer par une structure, puis de la transformer en classe pour enfin la remplacer par une interface. Ces différents éléments apparaissent suite aux différents refactoring que j'utilise, entre autre : extract method, extract class et extract interface. Le fait d'avoir un nommage constant me permet de gagner du temps lorsque je change la nature de l'élément. Je ne suis pas obligé de repasser partout pour le renommer.

Lorsque je manipule des objets, j'utilise toujours le mot clé dynamic. Cela permet d'utiliser le polymorphisme en WinDev. Lorsque je déclare une variable de structure ou de classe, j'utilise ce mot clé. Mais il est interdit avec les interfaces. Heureusement, le compilateur me signale une anomalie lorsque j'utilise le mot par erreur. En revanche, la transformation d'une interface en classe est plus risquée.

Par exemple :

 my_structure is cStructureName dynamic
 my_class is cClassName dynamic
 my_interface is cInterfaceName // On ne peut pas utiliser le mot clé dynamic

Pour initialiser une instance, j'utilise toujours l'opérateur d'affectation par référence <-. Il est obligatoire pour les interfaces et optionnel pour les structures et classes. J'utilise donc toujours cet opérateur d'affectation. Par contre, il ne fonctionne pas avec les types primitifs.

Afin d'améliorer la lisibilité du code, j'utilise le mot clé let. Cela m'évite de devoir spécifier le type de l'objet puisqu'il est indiqué par le constructeur. Si ce n'est pas le cas, j'indique explicitement le type de l'objet. Cela m'évite aussi d'avoir à indiquer le mot dynamic.

Par exemple :

let customer <- new cCustomer() // constructeur de cCustomer
let customer <- cCustomer.create() // constructeur depuis une méthode globale de cCustomer
customer is cCustomer dynamic <- create_customer() // fonction ou méthode

Enfin, j'essaye la plupart du temps de créer un constructeur sous forme de méthode globale. L'intérêt est de pouvoir utiliser les paramètres nommés qui sont interdits dans le constructeur standard d'une classe. Lors du refactoring, cela me permet aussi de détourner la création de l'objet et de retourner un autre objet sans avoir à retoucher tout mon code. Bien entendu, cette modification est toujours temporaire.

Pour une classe, le constructeur est global à la classe, mais pour les structures, je le mets dans une collection de procédure (mais cela peut parfois être une procédure locale). Quant aux interfaces, on les instancie avec des classes, on utilise donc le constructeur de la classe.

Les classes de tests

J'utilise le framework wxUnit pour effectuer mes tests unitaires et j'ai donc besoin de classe pour effectuer les différents tests. En les préfixant ainsi, je les distingue de mes classes standards et elles sont regroupées dans l'explorateur de projet.

Les énumérations

Les valeurs de l'énumération sont écrites comme des constantes.

Les combinaisons

Les valeurs des combinaisons sont écrites comme des constantes.

Les collections de procédures

Exemple :

mMath

Les fenêtres

Exemples :

Les champs

Exemples :

Les commentaires

J'utilise très peu de commentaires.

Je préfère mettre en place un code auto-documenté qu'un code avec beaucoup de commentaires. Pourquoi ? Tout simplement parce que les commentaires périment et il est très compliqué de les enlever lorsqu'on nettoie le code.

Pour commenter mon code, j'utilise des noms de fonctions explicites et s'il le faut, je découpe un code trop complexe avec des procédures internes. Et si je dois expliquer un calcul ou une condition, je passe par une ou plusieurs variables intermédiaires. Effectivement, je perds en performance (ridiculement faible) mais je gagne en lisibilité.

Le seul cas où j'utilise un commentaire, c'est pour expliquer le pourquoi d'un code (le code explique le comment mais pas le pourquoi) ou pour documenter mes fonctions (j'utilise pour cela les automatismes de WinDev).

ChaineConstruit

Dans certains cas, j'utilise des chaines de caractères qui doivent apparaître dans l'interface. J'utilise pour cela la fonction StringBuild. Et afin de gagner en lisibilité et pouvoir mettre en place un système de traduction indépendant de WinDev, j'utilise la variable _ que je définis dans le code du projet.

Exemple :

_ is procedure = translate_string
internal procedure translate_string(*)
result StringBuild(MyParameters)
END

Conclusion

Cette convention est vivante et continuera d'évoluer avec le temps. Elle n'est pas figée et je n'hésiterai pas à la modifier si des éléments me permettent de l'améliorer. Je préfère avoir des variations que je corrige au fur et à mesure que de rester avec une convention que je ne trouve plus utile.

Depuis que WinDev permet de sauvegarder le code sous forme de texte, j'envisage de mettre au point un outil qui vous permettra de vérifier que votre convention est bien respectée.