«
Grenier, boite 9

Grenier, boite 9

Jonathan LAURENT avait fait le blog Ytreza.org pour partager son approche sur l’utilisation de WinDev (mais pas que).  Le site a malheureusement fermé et nous avons reçu l’autorisation pour vous partager quelques-uns des articles ici.

La Programmation Orientée Objet (POO) - partie 1

J'aurais dû coder tout en objet

Introduction

Si vous venez sur Slack, vous pouvez y croiser @Jack. Je discute beaucoup avec @Jack et on s’apprend mutuellement des choses. Mais assez souvent, @Jack me dit :

C’est compliqué de trouver quelque chose qui explique la POO avec WinDev®.

Quelques jours plus tard, un de mes collègues au travail m’a dit :

Je pige que dalle à la POO.

Ni une, ni deux, j’ai pris mon clavier et j’ai décider de sauver le monde d’expliquer la POO. La théorie, mais aussi la pratique avec WinDev.

Je ne vous fais pas de fausse promesse, je risque d’échouer. En effet, la POO, c’est un concept très vaste avec du concret et de l’abstrait. Le concret explique le comment mais pas le pourquoi, tandis que l’abstrait explique le pourquoi mais pas le comment. Il va me falloir vous expliquer les deux notions en même temps.

Je compte sur vous pour me guider dans l’écriture de ces articles (car il va y en avoir plusieurs). Vous devez me poser des questions, me dire si je suis trop vague, trop compliqué, dans l’erreur ou quoique ce soit. Lorsque j’aurai écrit tous les articles, j’en ferai un beau bien condensé que je publierai sur développez.com.

Présentation de la POO

De manière abstraite

Imaginez votre programme comme une grosse boîte. Vous rentrez dedans et vous voyez plein de grosses boîtes. Elles sont toutes noires, vous ne voyez pas ce qu’il y a à l’intérieur. Elles ne sont pas de la même forme, ni de la même taille. Certaines boîtes sont liées à d’autres. Vous rentrez dans une de ces boîtes et vous constatez la même chose, cette boîte contient elle aussi d’autres boîtes noires. Et ainsi de suite. Vous décidez de modifier le comportement de votre programme, alors, vous créez une nouvelle boîte. Et vous la substituez à une autre boîte. Ces boîtes ont la même taille et la même forme, mais si vous comparez leurs contenus, vous voyez qu’ils sont différents. Deux boîtes de même forme, mais avec un comportement différent.

Et de manière concrète

Les boîtes sont des objets. Elles sont noires pour qu’on ne puisse pas voir à l’intérieur. Cela veut dire que le développeur, lorsqu’il manipule l’objet, ne doit pas savoir comment fonctionne l’objet. Ce principe s’appelle l’encapsulation.

Une boîte ayant la même forme qu’une autre peut remplacer cet autre boîte, même si leur contenu (comportements) sont différents. Cela veut dire que le développeur peut interchanger des objets qui se ressemblent (ces objets ont la même interface) mais qui n’ont pas forcément le même comportement. Ce principe s’appelle le polymorphisme.

Ces deux concepts (encapsulation et polymorphisme) font parti des concepts les plus importants de la POO. Ils font parti du pourquoi de la POO.

Et de manière imagée ?

Je suis en train de construire ma voiture. On va simplifier les choses, il y a le châssis et le moteur. J’ai le châssis d’une Clio® et d’une Ferrari®. De même pour le moteur, j’ai celui d’une Clio et d’une Ferrari.

Avec le principe d’encapsulation, je ne sais pas comment fonctionnent les moteurs. En y réfléchissant bien, je n’ai pas un moteur de Clio et un moteur de Ferrari. J’ai deux moteurs, point. Le principal, c’est qu’ils fonctionnent et font ce que j’attends d’eux.

Avec le principe de polymorphisme, je peux utiliser n’importe quel moteur avec n’importe quel châssis. Les moteurs rentrent dans les deux et se connectent parfaitement.

Au final, en faisant joujou, je peux créer :

  • une Ferrari ;
  • une Clio ;
  • une voiture avec un châssis de Clio et un moteur de Ferrari ;
  • une voiture avec un châssis de Ferrari et un moteur de Clio.

Conclusion

Et voilà, je vous ai parlé de POO sans mentionner les mots classe, méthode, propriété ni héritage. C’est le comment, et on en parlera dans un prochain article.

La Programmation Orientée Objet (POO) - partie 2

Introduction

Dans cet article, je vais vous présenter un exemple où je compare la POO à la programmation procédurale. Vous pouvez télécharger cet exemple sur le dépôt PCSoft, mais s’il vous plaît, pour ceux qui ne connaissent pas la POO, attendez que l’article vous invite à examiner le code pour ne pas aller trop vite.

En effet, je veux continuer à vous apprendre le pourquoi (l’encapsulation et le polymorphisme) et j’ai peur que vous ne vous arrêtiez qu’au comment (création de classe, méthode, propriété, etc…). C’est une erreur que j’ai faîte quand j’ai commencé à apprendre et je me suis mis à vouloir mettre de l’objet partout, sans réfléchir. Pire, j’ai entraîné mes collègues dedans et le code résultant (toujours en activité) me déçoit énormément aujourd’hui.

Prenez le temps…

Télécharger l’exemple

La POO par l’exemple

Avant toute chose, il faut savoir que tout ce que l’on peut faire en procédural, on peut le faire en POO. Mais l’inverse est également vrai. La POO n’est pas mieux que le procédural, la POO c’est différent.

Dans ma manière de vous la présenter, je vais faire en sorte de vous faire préférer la POO, puisque je veux que vous compreniez les avantages. Le niveau de qualité du premier exemple (programmation procédural) est inférieur à celui du second (POO). C’est fait exprès.

En procédural

Le premier exemple est écrit en procédural. Il permet de manipuler une table, une liste et une zone répétée. Trois actions sont possibles sur chacun des champs :

  • Ajouter un élément
  • Supprimer l’élément sélectionné
  • Afficher le contenu dans une liste

Je vous laisse prendre connaissance du code de la fenêtre fenTest01 (pour les novices, Ctrl + E puis tapez 01 et faîtes Entrée). Le code qui nous intéresse se trouve dans les 3 boutons Ajouter un élémentSupprimer l’élément sélectionné et Tracer le contenu.

Vous constaterez que pour chaque champ, on utilise les méthodes avec le préfixe qui va : Tablexxx, Listexxx et ZoneRépétéexxx.

J’aurai pu factoriser un peu le code, mais volontairement, je ne l’ai pas fait, parce que tout le monde sait que la POO, c’est mieux ! (j’espère que vous aurez perçu l’ironie).

Enfin, j’attire votre attention sur la présence d’un mot clé particulier, c’est le mot clé SELON. On verra plus tard pourquoi j’insiste dessus , mais il a son importance.

Voilà, vous avez testé la fenêtre et vous avez fait le tour. Vous en maîtrisez le code, c’est parfait.

Avec la POO

Maintenant, je vous laisse prendre connaissance du code de la fenêtre fenTest02 (pour les novices, Ctrl + E, 02, etc…).

C’est la même fenêtre et elle fait la même chose. Testez-là, vous ne verrez pas de différence.

Le code qui nous intéresse se trouve toujours dans les 3 boutons. Voici le code du bouton Ajouter un élément :

nouveau_libellé est une chaîne = DonneGUID(guidFormaté)

champ est un cChampAvecElements dynamique = recupérer_champ()
champ.ajouter(nouveau_libellé)

Et comparons le avec le code précédent :

nouveau_libellé est une chaîne = DonneGUID(guidFormaté)

SELON brQuelChamp
    // La table
    CAS 1 : TableAjoute(taElements, nouveau_libellé)

    // La liste
    CAS 2 : ListeAjoute(lsElements, nouveau_libellé)

    // La zone répétée
    CAS 3 : ZoneRépétéeAjoute(zrElements, nouveau_libellé)
FIN

Et là, je veux attirer votre attention sur les deux notions dont je vous parle sans arrêt :

L’encapsulation

La première, c’est l’encapsulation.

Je ne sais pas comment fonctionne la méthode Ajouter car j’ai masqué le détail de l’implémentation dans la classe. J’ai donc défini ce que l’on appelle un niveau d’abstraction.

Je n’ai pas à savoir si je dois utiliser TableAjouteListeAjoute ou ZoneRépétéeAjoute qui sont des fonctions très précises. Dans tous les cas, j’aurai simplement à utiliser la méthode Ajouter qui est beaucoup plus abstraite. La POO permet de simplifier les choses en les rendant plus abstraites.

Le polymorphisme

La seconde, c’est le polymorphisme.

Je travaille avec un objet de type cChampAvecElements. Ouvrez la fonction récupérer_champ (Pour les novices, mettez le curseur dessus puis touche F2). Vous retrouvez le code suivant :

PROCEDURE recupérer_champ()
champ est un cChampAvecElements dynamique
SELON brQuelChamp
    // La table
    CAS 1 : champ = vl.champ_table

    // La liste
    CAS 2 : champ = vl.champ_liste

    // La zone répétée
    CAS 3 : champ = vl.champ_zone_répétée            
FIN
RENVOYER champ

On constate les choses suivantes :

On retrouve le SELON. L’objectif du polymorphisme est de faire disparaître les SELON. Si vous utilisez un SELON, alors il y a de forte chance que vous pouvez utiliser le polymorphisme à la place, sauf si vous êtes dans ce que l’on appelle une fabrique (factory en anglais). Ce qui est un peu le cas dans notre exemple (pas tout à fait car on ne crée pas les objets dans cette fonction mais on va faire comme si…).

On voit que l’on a trois variables locales (vl. est une norme de nommage).

Elles sont définies dans la partie déclaration de la fenêtre :

PROCEDURE MaFenêtre()
stVariableFenêtre est une Structure
    champ_table est un cChampAvecElements dynamique
    champ_liste est un cChampAvecElements dynamique
    champ_zone_répétée est un cChampAvecElements dynamique
FIN

vl est une stVariableFenêtre

LOCAL
    vl.champ_table = allouer un cChampTable(taElements, taElements.Colonne1)    
    vl.champ_liste = allouer un cChampListe(lsElements) 
    vl.champ_zone_répétée = allouer un cChampZoneRépétée(zrElements, zrElements.coLibellé)

Chaque variable a un type différent (cChampTablecChampListe et cChampZoneRépétée) mais on peut les stocker dans une variable de type cChampAvecElements. C’est cela qu’on appelle le polymorphisme. Un objet cChampAvecElements peut avoir plusieurs formes.

Conclusion sur l’exemple

Avec le polymorphisme et l’encapsulation, on essaie au maximum de masquer les détails pour permettre au développeur de comprendre beaucoup plus facilement l’intention réelle du code et de permettre une évolution du code.

En effet, si j’ai besoin d’utiliser un champ de saisie, je n’ai qu’à créer une classe cChampSaisie et qui fonctionnera de la même manière que les autres. Je n’aurai qu’à modifier la fonction récupérer_champ pour que mon code continue à fonctionner.

Enfin, je vous invite à lire les codes des deux autres boutons. Le bouton Tracer le contenu est celui que je trouve le plus intéressant.

Conclusion

Dans cet article, je suis encore resté sur le pourquoi. J’espère que cet exemple vous a permis de mieux comprendre les deux notions que sont l’encapsulation et le polymorphisme.

Si vous le souhaitez, vous pouvez commencer à explorer le comment en étudiant les classes cChampAvecElementscChampTablecChampListe et cChampZoneRépétée. Je les expliquerai en détail dans un prochain article.

La Programmation Orientée Objet (POO) - partie 3

Introduction

Dans les articles précédents, je vous expliquais pourquoi on utilisait la POO. Aujourd’hui, je vais vous expliquer le comment. Nous allons voir comment créer une classe, et surtout, comment elle est structurée. J’insiste encore une fois, la POO est intéressante si vous maîtrisez le pourquoi et le comment. Si vous ne comprenez qu’une des deux notions, vous allez semer le chaos dans vos projets. C’est du vécu…

Enfin, la présentation que je vais vous faire va être très générale. Je ne vais pas rentrer tout de suite dans les détails. Donc, pour les connaisseurs, sachez que je ne vais pas parler des cas particuliers aujourd’hui. Ce sera pour une prochaine fois.

Les classes

Lorsqu’on utilise la POO, on crée et on manipule des objets. La création se fait au moyen d’un outil que l’on appelle classe. Il faut considérer une classe comme un moule que l’on peut utiliser à volonté pour créer autant d’objets que l’on souhaite. Si l’on souhaite un autre genre d’objet, on utilise un autre moule. Je vais maintenant vous décrire la composition d’une classe.

Parlez-vous « objet » ?

Avant d’aller plus loin, il est bon d’intégrer quelques notions de vocabulaire.

Les objets (ou instances de classe)

Un objet (que l’on appelle aussi instance de classe) est une entité placée en mémoire qui a été créée à partir du moule qu’est une classe. Chaque objet a une identité propre et plusieurs objets issus de la même classe peuvent co-exister en même temps.

Les membres (ou attributs) d’une classe

De manière générale, lorsqu’on vous parle d’un membre d’une classe, on vous parle d’une variable. Cette variable est particulière car elle est liée à l’objet. Cela veut dire que deux objets différents ont chacun une variable différente. Pour vous donner un exemple, imaginez que vous avez deux fenêtres WinDev. Chacune a une propriété ..Hauteur différente. ..Hauteur est donc l’équivalent d’un membre.

Les membres ont des caractéristiques que nous verrons une prochaine fois.

Les méthodes

Si les membres sont des variables, alors les méthodes sont des procédures ou des fonctions. Une chose les distingues des procédures / fonctions dont vous avez l’habitude. Elles ont un accès direct aux membres ainsi qu’à l’instance de la classe elle-même.

Les méthodes ont elles aussi des caractéristiques que nous étudierons plus tard.

Dans une classe, on distingue deux méthodes particulières :

Le constructeur

Le constructeur est une méthode particulière qui permet d’initialiser l’objet, donc ses membres. Il accepte des paramètres comme n’importe quelle méthode. Par contre, il ne peut pas retourner de valeur (on considère de manière implicite qu’il renvoie l’objet créé). Il faut noter que le constructeur est forcément appelé à la création d’un objet.

Le destructeur

C’est la seconde méthode particulière que vous croiserez mais que vous n’utiliserez presque jamais. Elle est appelée lorsque l’objet est détruit et permet de libérer les ressources maintenues par l’objet. Pour des raisons techniques que vous verrez certainement dans les commentaires ou dans mon 10eme article sur la POO, on ne sait pas forcément à quel moment le destructeur va être appelé. C’est pourquoi je vous déconseille de l’utiliser sans savoir ce que vous faîtes vraiment.

Les propriétés

Pour les novices, les propriétés sont un concept que l’on ne retrouve pas dans le procédural. Vu de l’extérieur, une propriété ressemble à un membre, mais vu de l’intérieur, elle ressemble à une méthode. C’est une notion très intéressante dans la POO.

Bien sûr, vous allez me dire que vous connaissez déjà les propriétés puisque vous en avez certainement déjà manipulé avec les objets de WinDev (..hauteur dont je parlais plus haut par exemple). Mais dans ce cas, faites-vous réellement la différence entre une propriété et un membre ? Vous verrez la différence un peu plus loin dans l’article et vous comprendrez pourquoi c’est inédit (edit : merci @Jack pour cette remarque).

Créer une classe avec WinDev

Dans WinDev, la création d’une classe est toute simple. C’est la même action que créer une collection de procédures sauf que vous choisissez Classe à la place (pour les novices, dans WinDev 22, vous faîtes Ctrl + N / Code / Classe).

Et voilà, vous vous retrouvez avec une jolie classe fraichement créée et vous voyez 3 blocs de code :

Déclaration

Dans ce bloc, vous déclarez les membres de la classe. Pour faire simple, vous les déclarez comme si vous étiez dans la partie Déclaration globale d’une fenêtre ou d’une procédure. Un membre se déclare de la même manière qu’une variable. On peut indiquer une valeur par défaut pour chaque membre dans la partie déclaration, mais je préfère les initialiser explicitement dans le constructeur.

Par exemple, pour avoir une classe avec un membre ma_valeur :

cMaClasse est une Classe
    ma_valeur est une chaine
FIN

Le constructeur

Comme expliqué précédemment, le constructeur permet d’initialiser les membres.

PROCEDURE Constructeur(valeur_initiale est une chaine)
    ma_valeur = valeur_initiale

Afin d’avoir un code propre et optimisé, je vous conseille de n’y mettre que le code d’initialisation des membres. Et faîtes en sorte que l’optimisation soit la plus rapide possible (évitez d’y lire un fichier ou de faire un accès à la base de données par exemple).

Le destructeur

Laissez-le vide pour commencer.

PROCEDURE Destructeur()

Ajouter une méthode

Une méthode se crée comme dans une collection de procédures (pour les novices : F4, indiquez le nom et laissez tout par défaut). Et voilà, une nouvelle méthode. Codez la comme vous coderiez une procédure / fonction et c’est tout.

La seule particularité, c’est que vous avez un accès direct au membre ainsi qu’à l’instance de la classe (l’objet).

Pour accéder aux membres, vous pouvez mettre directement le nom du membre. En fonction du paramétrage de votre projet, vous aurez peut-être l’obligation de précéder le nom du membre avec le symbole deux-points (:).

PROCEDURE ajouter_suffixe(suffixe est une chaine)
ma_valeur = ma_valeur + suffixe

ou

PROCEDURE ajouter_suffixe(suffixe est une chaine)
:ma_valeur = :ma_valeur + suffixe

Pour accéder à l’objet, vous avez à disposition le mot clé objet (faîtes attention, on l’oublie très facilement ce mot). Considérez que c’est l’équivalent de la variable MaFenêtre dans une fenêtre.

PROCEDURE est_égal(autre_objet est un cMaClasse)
    renvoyer objet.ma_valeur = autre_objet.ma_valeur

Créer une propriété

Pour créer une propriété, il faut passer par le menu contextuel (clic droit sur la classe dans l’explorateur de projet puis nouvelle propriété ou clic droit dans le volet code au niveau de la liste des membres et méthodes de la classe).

On vous demande un nom et si vous souhaitez que la propriété soit accessible en lecture et / ou en écriture. Pour les autres paramètres, laissez-les de côté pour le moment.

Comme je vous l’ai dit, une propriété ressemble à une procédure quand on la regarde de l’intérieur. Effectivement, WinDev crée deux blocs de code :

  • Récupération de la propriété (lecture)
  • Affectation de la propriété (écriture)

La présence de code dans un bloc l’active tandis que l’absence le désactive. Cela vous permet d’utiliser un système inédit pour vous si vous n’avez jamais fait de la POO. La possibilité d’avoir des variables qui sont accessibles soit en lecture, soit en écriture, soit les deux.

Par exemple, je peux créer la propriété taille qui fait la chose suivante :

PROCEDURE taille()
    Renvoyer Taille(:ma_valeur)

Je peux lire la taille de mon objet mais je ne peux pas la modifier.

Manipuler une classe

La manipulation d’une classe ressemble un peu à la manipulation d’une structure, sauf que la structure ne propose pas de méthode (sauf si vous avez lu les premiers articles de mon blog).

Un exemple sera plus parlant qu’un long texte :

mon_objet est une cMaClasse("valeur test")
Trace(mon_objet.ma_valeur)

mon_objet.ajouter_suffixe(" fin")
Trace(mon_objet.ma_valeur)

SI mon_objet.taille >= 15 ALORS
    Trace("L'objet a une taille de 15 ou plus")
SINON
    Trace("L'objet a une taille de moins de 15") 

Petites remarques en vrac

  • Vous pouvez utiliser la syntaxe mon_objet.ma_valeur ou mon_objet:ma_valeur.
  • Je l’ai déjà dit, mais n’oubliez pas le mot clé objet !!!
  • Je vous invite à lire la documentation de WinDev sur les classes.

Conclusion

Et voilà, vous connaissez les bases de la POO avec WinDev. Je vous ai volontairement caché beaucoup de chose pour avoir un article le plus simple possible. Dans le prochain article, j’aborderai la mise en place du polymorphisme, la partie la plus intéressante à mon avis de la POO.

La Programmation Orientée Objet (POO) - partie 4

Aujourd’hui, nous allons parler d’héritage. C’est un des outils offerts par la POO pour mettre en place le polymorphisme.

On hérite du vivant de ses parents

Lorsqu’on parle d’héritage, on pense la plupart du temps à ce moment triste et douloureux où l’on récupère les biens d’un membre proche de sa famille qui vient de décéder. Cet épisode malheureux de la vie n’a aucun rapport avec la POO !

Si je devais faire une analogie, l’héritage serait plutôt lié au principe de la naissance et plus exactement à la transmission de l’héritage génétique d’un parent à son enfant. C’est donc dans la joie et la bonne humeur que nous pouvons continuer à développer !

Le mot clé hérite

Dans WinDev, la mise en place de l’héritage se fait au moyen du mot clé hérite. Une classe enfant hérite de son parent.

Voyons voir l’utiliser l’exemple de deux classes : cParent et cEnfant qui hérite de la première.

Cela donne dans le code de déclaration des deux classes :

Dans cParent :

cParent est une Classe
    ...
FIN

Dans cEnfant :

cEnfant est une Classe
    hérite de cParent
    ...
FIN

Dans notre exemple, on voit que l’enfant n’hérite que d’un seul parent. Avec WinDev, il est tout à fait possible d’hériter de plusieurs parents (ce n’est pas le cas dans tous les langages). Je vous invite toutefois à éviter d’utiliser les héritages multiples, puisque comme nous ne le verrons pas, cela peut poser des ambiguïtés dans le code.

Pas satisfait de l’héritage ? Redéfinissez le !

Revenons sur la notion d’héritage génétique dont je parlais plus tôt. Dans le cas d’un être humain, on récupère une partie du génome paternel et une partie du génome maternel. Cette manière de faire garantit à la population humaine une quasi unicité de chaque individu.

Par contre, dans le cas de la POO, il faudrait considérer qu’il n’y a qu’un seul parent. 100 % du génome du parent est transmis à l’enfant. Et qu’est-ce que ça donne ? Un clone !

Dans l’exemple précédent, la classe cEnfant est un clone de la classe cParent. Les deux classes font exactement la même chose. Ce qui ne sert pas à grand chose en programmation… C’est pourquoi nous allons voir qu’il est possible de modifier certains des éléments que l’enfant hérite et lui ajouter aussi de nouveaux éléments.

Ajouter des méthodes

La manière la plus simple pour enrichir une classe enfant, c’est en lui créant de nouvelles méthodes. Ces méthodes seront disponibles pour l’enfant, mais pas pour le parent.

Modifier des méthodes

On commence à toucher au polymorphisme. Imaginons que la classe cParent ait la méthode suivante :

PROCÉDURE qui_es_tu()
RENVOYER "je suis le parent"

Cela n’a pas de sens si on invoque la méthode sur une instance de la classe cEnfant qui répondra elle aussi : je suis le parent.

Pour corriger cela, on va simplement créer une nouvelle méthode dans la classe cEnfant qui porte la même signature que la méthode parente :

PROCÉDURE qui_es_tu()
RENVOYER "je suis l'enfant"

Tout simplement !

Accéder au méthode du parent

Une classe enfant peut toujours invoquer les méthodes du parents, même si celles-ci ont été redéfinies. Cet exemple sera très parlant :

PROCÉDURE qui_es_ton_parent()
RENVOYER "mon parent aurait dit : " + cParent:qui_es_tu()

Pour appeler le constructeur d’un parent, il faut écrire :

PROCÉDURE Constructeur()
Constructeur cParent()

On peut bien entendu passer les paramètres que l’on souhaite au constructeur de cParent.

Un peu de polymorphisme

Avec tous ces éléments, on peut commencer à mettre en place certains éléments du polymorphisme. Imaginons que l’on définisse la méthode suivante dans la classe cParent :

PROCÉDURE présente_toi()
RENVOYER "Je me présente : " + :qui_es_tu()

Mais nous ne redéfinissons pas cette méthode dans la classe cEnfant. Lorsque le parent l’appelle, on obtient : Je me présente : je suis le parent.

A son tour, l’enfant peut bien entendu utiliser cette méthode. Mais quel résultat allons nous avoir ? En effet, nous allons appeler une méthode qui_es_tu, mais laquelle ? Celle de l’enfant bien sûr. Nous obtenons : Je me présente : je suis l'enfant.

Nous venons de modifier le comportement d’une méthode sans la redéfinir, commencez-vous à comprendre l’intérêt de l’héritage ?

Il faut être dynamique pour profiter de l’héritage de ses enfants !

Nous allons enfin aborder le polymorphisme ! Mettons en place l’exemple suivant :

Tout d’abord, la classe cAnimal :

cAnimal est une Classe

PRIVÉ
    __prénom est chaîne
FIN

PROCÉDURE Constructeur(prénom est chaîne)
:__prénom = prénom

PROCÉDURE fais_ton_action()
RENVOYER "je ne fais rien"

PROCÉDURE fais_ton_bruit()
RENVOYER "..."

PROCÉDURE presente_toi()
RENVOYER _([
    Je m'appelle '%1'.
    Je suis un animal de type '%2'.
    Mon bruit ? '%3'.
    Qu'est-ce que je fais ? %4.
    ], :__prénom, :donne_ton_type(), :fais_ton_bruit(), :fais_ton_action())

PROCÉDURE donne_ton_type()
RENVOYER "inconnu"

On crée ensuite une classe cChien :

cChien est une Classe
hérite de cAnimal
FIN

PROCÉDURE Constructeur(prenom est chaîne)
Constructeur cAnimal(prenom)

PROCÉDURE donne_ton_type()
RENVOYER "chien"

PROCÉDURE fais_ton_action()
RENVOYER "j'aboie et je creuse des trous"

PROCÉDURE fais_ton_bruit()
RENVOYER :aboies()

PROCÉDURE aboies()
RENVOYER "ouaf ouaf"

En enfin la classe cChat :

cChat est une Classe
hérite de cAnimal
FIN

PROCÉDURE Constructeur(prenom est chaîne)
Constructeur cAnimal(prenom)

PROCÉDURE donne_ton_type()
RENVOYER "chat"

PROCÉDURE fais_ton_action()
RENVOYER "je miaule et je chasse les souris"

PROCÉDURE fais_ton_bruit()
RENVOYER :miauler()

PROCÉDURE miauler()
RENVOYER "miaou miaou"

Et un code pour tester tout cela (mettez le dans le bouton d’une fenêtre):

animaux est un tableau de cAnimal dynamique = [allouer un cAnimal("Denver"), allouer un cChien("PIF"), allouer un cChat("HERCULE")]
un_animal est un cAnimal("")

POUR i = 1 _À_ animaux..Occurrence
    un_animal = animaux[i]
    Trace("presente_toi :", un_animal.presente_toi())
    Trace("")
FIN

Pour le dernier code, j’aurai pu faire un POUR TOUT, mais j’ai préféré faire une simple boucle pour mettre en évidence la notion de polymorphisme.

Tout d’abord, testez ce code et voyez ce que ça fait. On obtient la trace suivante :

presente_toi : Je m'appelle 'Denver'.
Je suis un animal de type 'inconnu'.
Mon bruit ? '...'.
Qu'est-ce que je fais ? je ne fais rien.

presente_toi : Je m'appelle 'PIF'.
Je suis un animal de type 'inconnu'.
Mon bruit ? '...'.
Qu'est-ce que je fais ? je ne fais rien.

presente_toi : Je m'appelle 'HERCULE'.
Je suis un animal de type 'inconnu'.
Mon bruit ? '...'.
Qu'est-ce que je fais ? je ne fais rien.

Ensuite, remplacez la ligne un_animal est un cAnimal("") par un_animal est un cAnimal dynamique et exécutez à nouveau le code. On obtient cette trace :

presente_toi : Je m'appelle 'Denver'.
Je suis un animal de type 'inconnu'.
Mon bruit ? '...'.
Qu'est-ce que je fais ? je ne fais rien.

presente_toi : Je m'appelle 'PIF'.
Je suis un animal de type 'chien'.
Mon bruit ? 'ouaf ouaf'.
Qu'est-ce que je fais ? j'aboie et je creuse des trous.

presente_toi : Je m'appelle 'HERCULE'.
Je suis un animal de type 'chat'.
Mon bruit ? 'miaou miaou'.
Qu'est-ce que je fais ? je miaule et je chasse les souris.

Constatez-vous la différence ? Dans les deux exemples, on manipule des instances issues de cAnimalcChien et cChat. Le résultat du premier exemple montre cependant que l’on considère chaque instance manipulée comme si elle était issue de la classe cAnimal. On a un seul comportement.

Le deuxième résultat montre par contre une évolution du comportement et bien qu’on utilise seulement une variable de type cAnimal dynamique pour parcourir le tableau, on obtient le comportent d’un cAnimal, puis d’un cChien et enfin d’un cChatVoilà ce qu’est le polymorphisme !

Pourquoi cette différence ? Parce que l’on a utilisé le mot clé dynamique. Je vais éviter de rentrer dans le détail du pourquoi et du comment. C’est ce mot clé qui permet de mettre en œuvre le polymorphisme dans WinDev (du moins jusqu’à la version 22, car les interfaces apparues en 23 permettent aussi d’utiliser le polymorphisme, mais nous verrons cela une prochaine fois).

Ce mot clé dynamique est accompagné des deux mots clés Allouer et Libérer. Le premier permet de créer une instance de classe (on appelle le constructeur) tandis que le second permet de libérer cette instance (et donc on appelle le destructeur). Je ne libère pas souvent les instances car WinDev s’en occupe lui même (de manière générale) mais j’use et j’abuse des mots clés dynamique et allouer.

Conclusion

Voilà, nous avons vu comment mettre en place l’héritage de classe dans WinDev avec le mot clé hérite. L’objectif de l’héritage est d’enrichir ou modifier le comportement de la classe enfant, pour cela, vous pourrez utiliser la redéfinition des classes. Et enfin, un des éléments clés de la POO, le polymorphisme avec le mot clé dynamique.

La Programmation Orientée Objet (POO) - partie 5

Introduction

Dans cette partie, nous allons étudier les classes abstraites et les interfaces. Ce sont de puissants outils qui permettent de simplifier la vie du développeur. En effet, le compilateur les utilise pour vous prévenir lorsqu’où vous oubliez certaines choses dans votre code.

Les classes abstraites sont apparues dans WinDev® 22 et les interfaces dans la version 23.

Les concepts sont assez proches, mais on ne s’en sert pas de la même manière.

Les classes abstraites

Les classes abstraites utilisent le principe de l’héritage et permettent de spécialiser une classe mère à l’aide de ses enfants. La classe mère décrit les principaux comportements attendus et les classes filles décrivent les spécificités.

Lorsqu’on crée une classe abstraite (la classe mère), on indique à l’aide du mot clé Abstrait les méthodes qui doivent être définies dans les classes filles. Ces méthodes abstraites n’auront aucun code dans la classe mère (seulement une signature) et ce sont les classes filles qui implémenteront le code manquant.

Il n’est pas possible d’instancier une classe abstraite. Il faut impérativement instancier une de ses classes filles qui ont elle-même implémenté toutes les méthodes abstraites. Bien entendu, les propriétés peuvent aussi être abstraite.

A l’utilisation, les classes abstraites s’utilisent comme des classes classiques.

Exemple de classe abstraite

Nous définissons une classe cEtreVivant :

cEtreVivant est une classe

Fin

Procédure respirer() // les mammifères et les poissons respirent
    :inspirer() 
    :expirer()
FIN

// mais ils ne récupèrent pas l'oxygène de la même manière
Procédure abstraite inspirer()

// mais ils n'évacuent pas l'oxygène de la même manière
Procédure abstraite expirer()

Une classe cMammifère :

cMammifère est une classe
    herite de cEtreVivant
FIN

Procédure inspirer()
:poumons.remplir()

Procédure expirer()
:poumons.vider()

Et une classe cPoisson :

cPoisson est une classe
    herite de cEtreVivant
FIN

Procédure inspirer()
:branchies.inspirer()

Procédure expirer()
:branchies.expirer()

Les interfaces

Les interfaces permettent de définir un contrat du style : la classe doit savoir faire ceci ou cela. A l’instar de la classe abstraite, l’interface définit des méthodes et des propriétés abstraites. Mais à l’inverse, elle ne définit aucun comportement initial.

Lorsqu’on utilise les interfaces, on ne parle pas d’héritage. On dit qu’une classe implémente une interface, c’est à dire qu’elle respecte le contrat proposé par celle-ci. Mais cette règle n’empêche pas d’utiliser l’héritage.

En règle générale, l’héritage permet d’avoir des classes qui ont une nature proche (un oiseau est un animal par exemple). Mais quand on parle d’interface, les classes qui en découlent peuvent n’avoir aucun lien (si on parle de l’interface PeutVoler, alors, un oiseau et un avion qui sont fondamentalement différents partagent la même interface).

Utilisation

Les interfaces ne s’utilisent pas de la même manière que les classes et classes abstraites.

Définition

Tout d’abord, pour créer une interface, il faut employer une syntaxe qui est assez proche de la définition d’une structure. Cette syntaxe est décrite dans la page d’aide associée de WinDev.

Exemple
ObjetPouvantVoler est une interface
Procédure décoller()
Procédure voler()
Procédure atterir()
Propriété altiltude() : entier
FIN

Implémentation

Lorsqu’on indique qu’une classe implémente une interface, le compilateur vérifie si les propriétés et les méthodes définies par l’interface existent. Si ce n’est pas le cas, une erreur de compilation apparaît. En faisant un clic droit sur ce message d’erreur, le menu contextuel nous donne la possibilité de créer la ou les méthodes manquantes.

Attention, il arrive que la création automatique ne respecte pas le prototype défini dans l’interface. Si on utilise un type structure comme paramètre, l’outil va mettre le mot clé structure au lieu du nom du type. (Souhaitons que ce problème soit corrigé dans les versions supérieures de WinDev).

Dans tous les cas, la première chose à faire lorsqu’on indique que la classe implémente l’interface, c’est d’aller créer les méthodes qui manquent.

Exemple
cAvion est une classe
    hérite de cVéhicule
    implemente ObjetPouvantVoler
Fin

cOiseau est une classe
    hérite de cAnimal
    implemente ObjetPouvantVoler
Fin

Utilisation

Pour utiliser une interface, il faut créer une variable du type de l’interface. On initialise ensuite cette variable depuis une instance d’une classe. Pour cela, il faut utiliser l’opérateur <- (affectation par référence). Si la classe implémente l’interface ou si elle possède les méthodes et propriété définies par l’interface (duck typing), alors la variable pointe vers l’instance. Par contre, si ce n’est pas le cas, la variable contiendra la valeur nulle (ce qui montre l’incompatibilité).

La variable s’utilise ensuite comme une instance de classe traditionnelle mais seule les méthodes et propriétés définies dans l’interface seront disponibles.

Exemple
objet_volant est un ObjetPouvantVoler <- allouer un cAvion()
objet_volant.décoller()
objet_volant.voler()
trace(objet_volant.altitude)
objet_volant.atterir()

Conclusion

Nous avons vu le principe de fonctionnement des interfaces et des classes abstraites ainsi que les manières de les créer. Ces outils sont très pratiques à condition de savoir les utiliser. En effet, ils sont très liés aux notions d’abstraction, de polymorphisme et d’encapsulation que nous avons étudié dans les précédents articles.

L'art d'utiliser les procédures internes
Les variants et JSON

Dans cet article, je vais vous présenter les variants. Vous connaissez, ces petites variables qui peuvent prendre n’importe quelles valeurs ? Et bien, depuis les versions 19 et 20 de Windev, les variants ont évolué pour devenir très intéressants.

Qu’est-ce qu’un variant ?

Un variant est un type du WLangage. Il vous permet de créer une variable qui pourra prendre n’importe quelle valeur, que ce soit une chaîne de caractère, un numérique, voire un tableau ou une structure.

MaVariable est un variant

On peut l’initialiser avec un numérique.

MaVariable = 5 // Notre variant est un entier

Ou une chaîne de caractère.

MaVariable = "cinq" // Notre variant est une chaîne

Ou un booléen.

MaVariable = Vrai // Notre variant est un booléen

On peut aussi l’utiliser comme un tableau. Dans ce cas, chaque élément du tableau est un variant et peut donc contenir des valeurs de plusieurs types.

MaVariable[1] = "un" // Notre variant est un tableau avec 1 élément
MaVariable[2] = 2 // ou deux éléments, de types différents

Ou comme un tableau associatif. Attention, les clés doivent toutes être du même type. Chaque élément sera un variant.

MaVariable["Un"] = 1
MaVariable["2"] = "deux"

Enfin, on peut l’utiliser comme une structure. Chaque membre sera un variant ou, plus exactement, une variable de type MembreVariant (c’est toujours utile lorsqu’on type les paramètres dans les procédures).

MaVariable.MaSousVariable = "un"
MaVariable.UneAutreSousVariable = 2

Pour ceux qui utilisent un langage de script, l’utilisation des variants vous rappellera des souvenirs.

Par contre, un variant ne peut pas contenir de procédure.

Un variant, c’est null

Un titre complet rien que pour cette fonctionnalité ! Un variant accepte la valeur null.

Lorsqu’on crée une variable de type variant, cette dernière a pour valeur null. On peut tester et afficher cette valeur (dans ce cas, null est affiché avec la valeur 0).

MaVariable est un variant
SI MaVariable = Null ALORS
    Trace("C'est null les variants")
    Trace(MaVariable) // Affiche zéro mais la valeur est bien null
FIN

Le côté pratique des variants

Lazy initialization

Les variants permettent des choses pratiques. Je m’en sers pour les Lazy initialization (initialisation paresseuse).

On déclare une variable globale (ou un membre privé si on est dans une classe).

MaValeurStockée est un variant = null // Le nom de la variable est explicite car elle est utilisée plus loin

Puis on crée une fonction qui permet d’initialiser et de lire la valeur de cette variable

LireMaValeurStockée est une procédure()
SI MaValeurStockée = Null ALORS
    MultiTâche(500) // On simule un calcul très long, au moins cinq secondes
    MaValeurStockée = "un"
FIN

RENVOYER MaValeurStockée 

Et enfin, on peut lire la valeur

Trace(LireMaValeurStockée()) // Cet appel prendra bien cinq secondes à s'exécuter, fichtre !
Trace(LireMaValeurStockée()) // Mais, mais ??? celui-ci est beaucoup plus rapide ! Merci la lazy initialization !

JSON

Et enfin, la fonctionnalité la plus intéressante avec les variants de Windev, c’est qu’ils simplifient l’utilisation de JSON.

Pour ceux qui ne connaissent pas Json, c’est un format de données textuelles au même titre que le format XML, sauf qu’il est beaucoup plus simple et moins verbeux. Il se base sur le principe Clé / Valeur. Il est très utile dans l’échange de données, ainsi que dans les fichiers de configuration.

Et pour l’utiliser, deux fonctions sont nécessaires :

Par exemple

MonVariant est un Variant

MonVariant.UnEntier = 5
MonVariant.UneChaine = "hello world"

MonVariant.UnTableau[1] = "Salut"
MonVariant.UnTableau[2] = 5
MonVariant.UnTableau[3].UnSousMembre = "Un sous membre"

MonVariant.UnTableauAssociatif["un"] = "Bonjour"
MonVariant.UnTableauAssociatif["trois"] = "25"
MonVariant.UnTableauAssociatif["test"].UnSousMembre = "Un deuxième sous membre"
MonVariant.UnTableauAssociatif["test"].UnAutreSousMembre = "Un dernier sous membre"

Trace(VariantVersJSON(MonVariant, psdMiseEnForme))

Donnera comme trace :

{
    "UnEntier":5,
    "UneChaine":"hello world",
    "UnTableau":
    [
        "Salut",
        5,
        {
            "UnSousMembre":"Un sous membre"
        }
    ],
    "UnTableauAssociatif":
    {
        "un":"Bonjour",
        "trois":"25",
        "test":
        {
            "UnSousMembre":"Un deuxi\u00e8me sous membre",
            "UnAutreSousMembre":"Un dernier sous membre"
        }
    }
}

Il y a plusieurs détails à noter dans l’exemple :

  • Les caractères spéciaux sont encodés.
  • Le paramètre psdMiseEnForme permet de rendre le JSON plus lisible pour un humain avec des retours chariots et des indentations.

Et l’exemple inverse :

MonJSON est une chaîne = "{ ""UnMembre"":""cinq"" }"
MonVariant est un Variant = JSONVersVariant(MonJSON)
Trace(MonVariant.UnMembre) // Affichera "cinq"

Voilà, vous en savez maintenant un peu plus sur l’utilisation de Json avec Windev.

Rendre ChaineConstruit plus élégant

Je vous partage aujourd’hui une petite astuce pour rendre votre code un peu plus lisible. A force de me lire, vous finirez par savoir qu’avoir un code clair et lisible est très important pour moi.

Cette idée m’est venue en apprenant à utiliser d’autres langages de programmation (Python en particulier qui est un langage que j’adore).

L’astuce d’aujourd’hui concerne la fonction ChaineConstruit.

ChaineConstruit – Qu’est-ce que c’est ?

La fonction ChaineConstruit vous permet de créer des chaînes de caractères à partir d’un modèle. On pourra ensuite changer certains éléments de ce modèle lors de l’appel de ChaîneConstruit.

Voici un petit exemple :

Trace(ChaineConstruit("Le chien fait %1 mais pas %2", "Ouaf", "Miaou"))
// Affiche "Le chien fait Ouaf mais pas Miaou"

Je vous laisse lire la documentation de WinDev® sur ChaineConstruit pour en savoir plus.

Il existe aussi d’autres fonctions *Construit :

J’aurai pu utiliser dans l’exemple précédent la fonction TraceConstruit, mais pour mettre en évidence ce que je souhaite vous apprendre, nous allons considérer que cette fonction n’existe pas. Si vous suivez mes recommandations, vous ne devriez jamais utiliser les fonctions *Construit. Non pas parce qu’elles sont mauvaises mais parce qu’elles sont incompatibles avec ma méthode. Si vous n’utilisez pas ma méthode, alors, utilisez-les !

ChaineConstruit – la fonction aussi longue que ce qu’elle construit

WinDev est très verbeux. C’est le prix à payer lorsqu’on veut utiliser des termes clairs et francisés. La fonction ChaîneConstruit n’y fait pas exception. Lorsque vous lisez l’exemple précédent, la fonction ChaineConstruit prend environ 21% de la taille totale de la ligne de code. C’est beaucoup.

Cela pose problème car on perd de vue les éléments les plus importants :

  • Le contenu : La chaîne de caractères
  • L’action sur le contenu : La fonction Trace

Au lieu de cela, on se focalise sur ChaineConstruit.

Je vais vous montrer comment la rendre moins verbeuse

Faire disparaître ChaineConstruit mais l’utiliser quand même

Afin de faire disparaître ChaineConstruit, on va utiliser une petite variable dont je vous parle souvent : la variable de type Procédure.

Tout d’abord, nous allons créer une procédure globale qui va encapsuler notre ChaîneConstruit. En effet, on ne peut pas utiliser une variable de type Procédure (à ma connaissance) avec des fonctions du WLangage, hélas.

PROCEDURE ChaîneConstruit(ChaineInitiale <utile>, *) : chaîne
RENVOYER WL.ChaîneConstruit(MesParamètres)

J’utilise ici la surcharge de procédure. Cela signifie que ma fonction porte le même nom qu’une fonction du WLangage. Désormais, si j’appelle cette fonction, ce sera celle que j’ai définie qui sera utilisée. Pour utiliser l’autre fonction, il faut préfixer l’appel avec WL. Vous noterez l’utilisation du paramètres ***** dont vous trouverez la documentation ici :

Avec cela, nous avons tout ce qu’il faut pour utiliser notre variable de type Procédure.

Dans le code du projet, vous allez écrire :

_ est une Procédure = yChaine.ChaîneConstruit

Vous aurez ainsi une procédure nommée _ définie de manière globale (puisque c’est dans le code du projet) qui aura exactement la même syntaxe que ChaîneConstruit.

Veuillez notez que j’ai préfixé l’appel de la fonction par yChaine. qui est le nom de la collection de procédures (utilisez le nom que vous souhaitez). En effet, WinDev refuse d’initialiser la variable si on indique seulement ChaîneConstruit, certainement parce que c’est le nom d’une fonction du WLangage.

Reprenons l’exemple du début avec notre nouvelle procédure :

Trace(_("Le chien fait %1 mais pas %2", "Ouaf", "Miaou"))
// Affiche "Le chien fait Ouaf mais pas Miaou"

Bingo ! C’est beaucoup plus lisible !

Aller plus loin

Les petits malins pourraient me dire :

Pourquoi ne pas avoir appelé la procédure globale directement avec le nom _ au lieu de ConstruitChaîne.

C’est une excellente remarque !

La raison est toute simple. Je veux que le fonctionnement de _ soit propre au projet. En effet, on n’est pas obligé d’utiliser la procédure ConstruitChaine avec. On peut utiliser d’autres procédures. Il faut donc supprimer toutes les dépendances avec une quelconque collection de procédures.

Dans un prochain billet, je vous montrerai la méthode que j’utilise pour traduire mes applications avec la fonction _.

Résumé

Nous avons vu dans ce billet que la fonction ChaineConstruit est particulièrement verbeuse et qu’il est possible de la remplacer par une variable de type procédure nommée _.

On a vu aussi qu’il est possible d’aller plus loin avec cette astuce et qu’elle peut éventuellement être utilisée dans la traduction d’une application.

Améliorer la recherche dans les tables avec les Sur-entêtes de colonne

Pour commencer ce calendrier de l’avent, voici la première astuce. Et ce sera une première dans ce blog, j’ai fait une vidéo de quelques minutes.

Je m’excuse d’avance, je n’ai pas l’habitude de faire des videos (et en plus, je déteste ma voix). Elle sera donc d’une qualité digne d’un débutant.

J’espère que cette astuce vous sera utile.

Ne plus oublier de restaurer la position d'un fichier HFSql

Est-ce qu’il vous arrive de faire des HSauvePosition et des HRetourPosition ? Personnellement, je les utilise de temps en temps. Et ça m’est déjà arrivé d’oublier de faire le HRetourPosition.

J’ai réfléchi à comment résoudre ce problème et j’ai pondu ces deux fonctions :

Une pour ne pas récupérer de résultat :

PROCEDURE ExecuterEtSauvegarderContexteSansRésultat(p est une Procédure, *)
PilePosition est une Pile d'entiers

POUR i = 2 _A_ MesParamètres..Occurrence
    Empile(PilePosition, HSauvePosition(MesParamètres[i]))
FIN

p()

PositionSauvegardée est un entier
TANTQUE Dépile(PilePosition,PositionSauvegardée)
    HRetourPosition(PositionSauvegardée)
FIN

Et une pour récupérer un résultat :

PROCEDURE ExecuterEtSauvegarderContexteAvecRésultat(p est une Procédure, *)
PilePosition est une Pile d'entiers

POUR i = 2 _A_ MesParamètres..Occurrence
    Empile(PilePosition, HSauvePosition(MesParamètres[i]))
FIN

soit Résultat = p()

PositionSauvegardée est un entier
TANTQUE Dépile(PilePosition,PositionSauvegardée)
    HRetourPosition(PositionSauvegardée)
FIN

Renvoyer Résultat

Elle me permet d’exécuter une procédure et de sauvegarder le contexte avant et après la procédure.

Je vous propose l’exemple suivant : on souhaite compter le nombre de lignes d’une commande. Imaginons que l’on travaille avec les deux fichiers Commande et LigneCommande, mais on souhaite pas perturber le parcours :

PROCEDURE CompterLigne(NumCommande)
    PROCEDURE INTERNE iCompterLigne()
        NombreLignes est un entier = 0
        SI HLitRecherchePremier(Commande, Num, NumCommande) ALORS
            HLitRecherchePremier(LigneCommande, Num, NumCommande)
            TantQue HTrouve(LigneCommande)
                NombreLignes ++
                HLitSuivant(LigneCommande)
            FIN
        FIN
    FIN

    Renvoyer ExecuterEtSauvegarderContexteAvecRésultat(iCompterLigne, "Commande", "LigneCommande")   

Et voilà.

J’espère que cette petite astuce vous inspirera pour d’autres codes. La sauvegarde de contexte ne s’applique pas seulement aux fichiers HFSql.

Passer un nombre dynamique de paramètre à une fonction

La question

Aujourd’hui, j’ai vu passer la question suivante :

Comment passer dynamiquement un nombre variable de paramètres à une fonction ?

Si vous ne comprenez pas la question, voici un petit exemple :

Nous avons une fonction afficher_plusieurs_paramètres qui accepte 0, 1 ou plusieurs paramètres

PROCEDURE afficher_plusieurs_paramètres(*)
POUR i = 1 _A_ MesParamètres..occurrence
    Trace(MesParamètres[i])
FIN

Et nous souhaitons l’appeler, mais sans connaître à l’avance le nombre de paramètre. Une idée de code serait la suivante :

nb_paramètres est entier = 3
SELON nb_paramètres
    CAS 1 : afficher_plusieurs_paramètres(paramètre_1)
    CAS 2 : afficher_plusieurs_paramètres(paramètre_1, paramètre_2)
    CAS 3 : afficher_plusieurs_paramètres(paramètre_1, paramètre_2, paramètre_3)
    AUTRE CAS : afficher_plusieurs_paramètres()
FIN

L’idéal serait de pouvoir écrire le cas suivant :

CAS n : afficher_plusieurs_paramètres(paramètre_1, paramètre_2, ..., paramètre_n)

Mais ce n’est pas possible.

Et pourquoi ne pas utiliser un tableau ?

C’est vrai, on peut utiliser un tableau. Mais cela ne répond pas à la problématique. La question, c’est comment passer ces fichus paramètres de manière dynamique. On ne cherche pas à proposer une alternative.

La solution

La réponse est toute simple, il suffit d’utiliser la compilation dynamique, et deux options sont possibles :

La première utilise les fonctions ExecuteCode (on peut aussi utiliser Compile), la seconde, EvalueExpression (on peut aussi utiliser ExecuteCode et Compile mais le résultat sera plus verbeux) .

ExecuteCode, tout est compilé dynamiquement

Cette version est compatible avec la plupart des versions de WinDev (et si ce n’est pas le cas, vous pouvez utiliser la fonction Compile). L’idée est simplement de créer dynamiquement le code que l’on souhaite appeler.

On pourrait donc avoir le code suivant (en supposant que les variables paramètre1 et paramètre2 existent) :

paramètres est une chaine = "paramètre1, paramètre2" 
ExecuteCode(ChaineConstruit("afficher_plusieurs_paramètres(%1)", paramètres))

Et voilà, nous n’avons plus qu’à créer correctement le contenu de paramètres et nous pouvons indiquer les paramètres dynamiquement.

Mais cette solution a un inconvénient, c’est que si on renomme la fonction afficher_plusieurs_paramètres, le compilateur ne nous préviendra pas et nous aurons une erreur de compilation au moment de l’exécution. Heureusement, il existe une solution qui corrige ce problème.

EvalueExpression avec un tupple aka les valeurs de retour multiples

Il existe une solution plus simple et tout aussi efficace, c’est de combiner la fonction EvalueExpression avec un tupple. Dans WinDev, les tupples sont appelés des valeurs de retour multiples (L’inconvénient de ce nom, c’est que ça fait référence aux valeurs renvoyées par une fonction, mais pas aux valeurs reçues par une fonction).

Voici le même exemple que le cas précédent :

paramètres est une chaine = "paramètre1, paramètre2" 
afficher_plusieurs_paramètres(EvalueExpressiont("(" + paramètres + ")"))

Deux avantages :

  • Le code est lisible.
  • La fonction afficher_plusieurs_paramètres est détectée par le compilateur. Si on modifie le nom de cette fonction, WinDev signalera une erreur de compilation.

Pour comprendre cet exemple, il faut savoir que l’on peut passer à une fonction recevant x paramètres le résultat d’une fonction renvoyant x valeurs.

Voici un petit exemple :

afficher_plusieurs_paramètres(lister_paramètres())  // Affiche a + rc + b + rc + c

PROCEDURE lister_paramètres()
RENVOYER ("a", "b", "c")

Le secret, pour renvoyer un tupple, c’est de ne pas oublier les parenthèses sur la ligne du RENVOYER.

Et EvalueExpression, ça fait quoi exactement ?

EvalueExpression est une fonction qui prend une expression compatible WLangage®, elle l’exécute dynamiquement puis renvoie ainsi la nouvelle valeur créée. En ajoutant les parentèses, EvalueExpression crée donc un tupple que la fonction afficher_plusieurs_paramètres peut consommer.

Pour finir

Et voilà, si vous connaissez le nombre et le nom de vos paramètres, vous pouvez désormais créer et appeler des fonctions avec un nombre variable de paramètres.

Testez vos fenêtres WinDev sans vous embêter avec la base de données

Le contexte

Il y a quelques jours, je travaillais pour un de mes clients, et je devais intervenir sur une fenêtre pour laquelle je ne pouvais pas utiliser la base de données (je ne l’ai jamais paramétré chez moi). Je me suis souvenu que chaque fois que les développeurs travaillaient sur ce projet, le temps de lancement du go pouvait prendre plusieurs dizaines de seconde avant d’afficher quoique ce soit. On a même cru que la fenêtre buggait tellement elle mettait de temps à s’afficher.

J’aurais pu paramétrer la base de données comme tout le monde et tomber dans un monde où un simple test met une plombe à se lancer. Mais vous commencez peut-être à me connaître, je suis fainéant et je n’aime pas attendre. J’ai donc réfléchi à un moyen d’avoir une fenêtre qui s’ouvre rapidement sans cette fichue base de données.

J’ai donc appliqué le principe D de SOLID (si vous ne connaissez toujours pas, cliquez ici et lisez le paragramhe DIP) et j’ai réfléchi à comment inverser les dépendances et que ma fenêtre ne dépende plus de cette base.

Avant de rentrer dans le côté technique, il faut savoir que cette méthode me permet aussi de tester plusieurs scénarios sur ma fenêtre. Je peux tester ma fenêtre avec aucun enregistrement dans la base de données, avec 10, 20, 100 et ajouter tous les scénarios qui me viennent à l’esprit. Je peux simuler une erreur de la base de données au moment de l’ajout par exemple.

Par contre, cette technique peut se révéler compliquée à mettre en place si vous travaillez directement avec des tables reliées à un fichier. En effet, il faut enlever les dépendances et ce lien est une dépendance. Il faudra le faire sauter si vous souhaitez appliquer cette technique.

Bien, parlons techniques.

La technique

tl;dr :

L’idée de base est d’utiliser la compilation conditionnelle et le polymorphisme pour séparer le cas réel du cas de test. Et si vous n’avez pas envie de lire, vous pouvez suivre la formation vidéo (plus d’information dans la conclusion).

Vous êtes toujours là ? C’est parti !

1. Extraire toutes les dépendances

On commence par créer une classe. Personnellement, je l’appelle c + le nom de la fenêtre + le préfixe final (c’est une convention temporaire).

Nom de fenêtre : wdClient
Nom de classe : cWdClientFinal

Ensuite, j’instancie cette classe dans les globales de la fenêtre

PROCEDURE MaFenêtre()
comportement is cWdClientFinal dynamic = créerComportement()

ainsi que la procédure créerComportement

PROCEDURE créerComportement()
RENVOYER allouer cWdClientFinal()

Et je parcours toute ma fenêtre pour trouver le code qui dépend d’un élément externe.

Si par exemple, j’ai le code suivant dans le champ d’initialisation de la fenêtre :

Nom = Client.Nom // Nom est un champ de saisie et Client est un fichier HF SQL

Je peux le remplacer par

Nom = comportement.obtenirNom()

et je crée la méthode obtenirNom dans la classe cWdClientFinal:

PROCEDURE obtenirNom()
RENVOYER Client.Nom

Je fais ça pour tous les appels à la base de données, ainsi que toutes les dépendances qui sont complexes à gérer.

Ensuite, vous pouvez tester votre fenêtre, elle devrait fonctionner comme avant.

2. Extraire une interface

Après tout ça, on va faire en sorte de remplacer ce comportement par un autre comportement. Pour cela, on va utiliser le polymorphisme. Ce mot vous fait peut-être peur, mais vous allez voir, la mise en place est toute simple.

On commence par remplacer le type de notre variable comportement (pour rappel, c’était un cWdClientFinal dynamic) par un nouveau type (qui n’existe pas encore) : cWdClientN’oubliez pas de changer l’opérateur d’affectation.

PROCEDURE MaFenêtre()
comportement is cWdClient <- créerComportement() 

Le compilateur vous crie dessus parce que cWdClient n’existe pas. On va donc lui faire plaisir et le créer. Et ce nouveau type, ça va être une interface que vous pouvez mettre dans le code du projet, collection de procédures, etc…

cWdClient est interface
Fin

Après cela, le compilateur va crier quelque chose de différents. Il va vous dire que les méthodes n’existent pas dans le type cWdClient. C’est normal puisqu’elles n’existent que dans la classe cWdClientFinal. On va donc rajouter dans l’interface toutes les méthodes qui manquent. Vous devez indiquer exactement la même syntaxe que celles de la classe cWdClientFinal.

cWdClient est interface
    Procédure obtenirNom()
FIN

Si vous le souhaitez, vous pouvez indiquer que cWdClientFinal implémente cette interface. Mais cette étape est optionnelle (quoique conseillée).

cWdClientFinal est une classe
    implémente cWdClient
FIN

Et voilà, si vous avez fait les choses correctement, votre fenêtre fonctionne toujours de la même manière (beaucoup de travail pour ne rien changer).

3. Implémenter le nouveau comportement

Les choses sérieuses commencent maintenant. Nous allons voir pour implémenter le nouveau comportement à notre fenêtre. Et là encore, ça reste simple.

Nous allons créer une classe cWdClientForTest qui va implémenter l’interface cWdClient.

cWdClientForTest est une classe
    implémente cWdClient
FIN

Le compilateur va nous indiquer qu’il faut créer toutes les procédures dans cette nouvelle classe, ce que nous allons faire.

PROCEDURE obtenirNom()
RENVOYER "Roger"

Et voilà, notre nouveau comportement est prêt à être utilisé

4. Utiliser le nouveau comportement (ou l’ancien)

Maintenant, nous voulons utiliser le nouveau comportement à la place de l’ancien, mais nous souhaitons aussi que l’ancien fonctionne toujours. C’est normal, c’est lui que nous souhaitons livrer au client.

Pour cela, nous allons utiliser les configurations de projet et créer la configuration Test. Nous mettons dans cette nouvelle configuration la fenêtre wdClient, la collection de procédures contenant l’interface cWdClient et la classe cWdClientForTest. Nous pouvons d’ailleurs exclure cette classe de la configuration principale.

Enfin, nous modifions le code de la procedure créerComportement.

PROCEDURE créerComportement()
<COMPILE SI Configuration="Développement">
    RENVOYER allouer cWdClientForTest()
<SINON>
    RENVOYER allouer cWdClientFinal()
<FIN>

Et voilà, pour changer de comportement, il suffit de changer de configuration de projet.

5. Gérer des scénarios différents pour vos tests

Vous souhaitez maintenant gérer des scénarios différents ? Vous pouvez utiliser la ligne de commande et ajouter un nouveau paramètre scénario. En fonction de ce paramètre, votre comportement de test changera.

PROCEDURE obtenirNom()
SELON LigneCommande("Scenario") :
    CAS "Avec Robert"
        Renvoyer Robert
    AUTRE CAS
        RENVOYER "Roger"
FIN

Conclusion

Le test est toujours un art difficile, surtout celui des IHM. Mais avec cette technique, vous pouvez vous simplifier énormément la vie, pour peu que vous preniez le temps de mettre ces tests en place.

Imaginez, vous rencontrez un bug chez un client, il vous suffit de mettre en place le scénario adéquat pour le reproduire et le corriger. Vous pouvez aussi vous servir de cette méthode pour développer votre écran puis implémenter la partie base de données plus tard. La seule contrainte, c’est le temps d’apprentissage.

C’est pourquoi, en plus de cet article, je vous propose une formation vidéo pour maîtriser cette technique encore plus vite. Elle contiendra des manipulations, plus de théorie, plus de pratique ainsi qu’un projet exemple. Si vous êtes intéressés, je vous invite à aller voir la page de la formation : WinDev – Testez vos fenêtres sans vous prendre la tête.

Convention de codage dans mes projets

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 :

  • fenêtres
  • collections de procédures
  • classes
  • procédures publiques des collections de procédures
  • variables globales et constantes (projet, collection de procédures, classes)
  • structures, interfaces, énumérations (projet, collection de procédures, classes)
  • fichiers HFSQL
  • états
  • requêtes
  • les fonctions et procédures du WLangage®
  • les constantes, structures, énumérations du WLangage
  • etc…

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 :

  • en snake_case
  • minuscule, y compris les acronymes
  • sans préfixe
  • avec un verbe d’action à l’infinitif (plutôt qu’à l’impératif car le framework WinDev utilise l’impératif, encore une manière de se distinguer de WinDev)

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 :

  • en snake_case
  • minuscule, y compris les acronymes
  • sans préfixe

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 :

  • path_file_parameter au lieu de pathFileParameter
  • current_date au lieu de currentDate
  • articles au lieu de tArticles

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 :

  • _public_member
  • _protected_member
  • _private_member

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
    _HOURS_IN_DAY_ = 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 :

  • en CamelCase
  • un préfixe qui désigne le type

Les structures, les classes et les interfaces

  • préfixées par la lettre c en minuscule

Exemples :

  • cStructureName
  • cClassName
  • cInterfaceName

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 methodextract 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

  • préfixées par tu pour les tests unitaires, ti pour les tests d’intégration et te pour les tests de bout en bout (test end to end).

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

  • préfixées par en

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

Les combinaisons

  • préfixées par cm

Les valeurs des combinaisons sont écrites comme des constantes.

Les collections de procédures

  • en CamelCase
  • le préfixe m pour module

Exemple :

mMath

Les fenêtres

  • en CamelCase
  • le préfixe wd pour window

Exemples :

  • wdMain
  • wdTest

Les champs

  • en CamelCase
  • un préfixe en fonction du champ

Exemples :

  • btRefresh pour un bouton (button)
  • tbName pour un champ de saisie (text box)
  • taCustomerspour un champ table
  • cbCountry pour un champ combo (combo box)

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.

Règle d'or : Utiliser le GDS (ou un autre gestionnaire de source)

Quand vous codez, je vous conseille d’utiliser un gestionnaire de source. Les avantages que vous pouvez en tirer :

  • le travail en équipe ;
  • le travail à distance ;
  • un historique pour savoir qui a fait quoi et pourquoi ;
  • une sauvegarde de votre projet ;
  • un outil qui permet d’éviter certains codes mort.

Tous ces éléments permettent de diminuer la peur et le stress du développeur.

Un gestionnaire de source, qu’est-ce que c’est ?

Wikipedia le définit ainsi :

Un logiciel de gestion de versions (ou VCS en anglais, pour version control system) est un logiciel qui permet de stocker un ensemble de fichiers en conservant la chronologie de toutes les modifications qui ont été effectuées dessus. Il permet notamment de retrouver les différentes versions d’un lot de fichiers connexes.

Pour résumer, le gestionnaire de source permet de pouvoir revenir à un état précis de votre projet à tout moment.

Il est possible d’utiliser n’importe quel gestionnaire de source pour un développeur WinDev®, mais certaines contraintes s’appliquent ce qui rend la tâche compliquée. La solution la plus simple est d’utiliser l’outil fourni par PCSoft®. Il s’appelle tout simplement GDS (Gestionnaire De Source).

Le GDS, l’outil incontournable du développeur WinDev

Vous présenter le GDS en détail nécessiterait un ou plusieurs articles.

Dans l’immédiat, je vous invite simplement à lire la page dédiée au GDS dans la documentation PCSoft : Le gestionnaire de sources

Il faut tout de même noter que le fonctionnement du GDS de PCSoft est différent des autres gestionnaires de source du marché (git, mercurial, tortoise, svn, etc…)

L’installation d’un GDS n’est pas simple (pas trop compliquée non plus), mais vous pouvez utiliser le GDS Drive proposé par PCSoft qui fonctionne plutôt bien. Pour plus de détail, vous pouvez consulter la page GDS Drive.

Les avantages d’utiliser le GDS (ou un autre gestionnaire de source)

Le travail en équipe

Le travail en équipe est facilité. Sans cet outil, il est très compliqué de travailler à plusieurs sur un même projet. La plus grande contrainte étant que l’on ne peut pas modifier en même temps un même fichier (fenêtre, collection de procédures, classes, etc…).

Le GDS permet de passer outre cette contrainte. Si deux personnes travaillent en même temps sur le même fichier, un outil permettra par la suite de fusionner les deux modifications.

Le travail à distance

Lorsqu’on n’utilise pas le gestionnaire de sources, les fichiers sont présents dans un répertoire. Il devient alors compliqué de travailler sur un autre poste ou dans un autre lieu. Il faut être en permanence connecté au réseau local et utiliser des disques réseaux.

Le GDS permet de s’affranchir de cela et de travailler sur plusieurs postes différents. En effet, les sources sont stockées dans un répertoire local de la machine. Pour les récupérer, on passe simplement par le réseau. Si l’adresse IP et le port du GDS sont accessibles, on peut l’utiliser de n’importe où, y compris par Internet.

Il est aussi possible de travailler en mode nomade depuis un ordinateur portable, de travailler sur son projet sans avoir de connexion au réseau et de tout réintégrer quand on a de nouveau accès au réseau.

Historique du code

Bien utilisé, le GDS permet de savoir qui a fait quoi, quand et pourquoi.

En effet, chaque modification est historisée et cet historique est facilement consultable. On peut aussi comparer le code actuel à un code historisé. On voit ainsi facilement les modifications qui ont été faîtes.

Pour avoir un historique de qualité, il faut essayer de respecter quelques règles :

  • mettre un commentaire explicatif lorsqu’on réintègre son code ;
  • extraire et réintégrer au bon moment. Il faut essayer de réintégrer les fichiers chaque fois que l’on a fini le travail en cours (ajout d’une fonctionnalité, correction d’un bug, etc…) ;
  • ne faire qu’une seule chose à la fois. Cette règle vient en complément de la précédente. Essayez de n’avoir qu’une seule tâche à la fois. Soit vous ajoutez une (et une seule) fonctionnalité, soit vous corrigez un (et un seul) bug.

Sauvegarde de projet

Tous les projets qui sont stockés sur le GDS sont sauvegardés à plusieurs endroits :

  • sur le serveur du GDS ;
  • sur chaque poste qui travaille sur le projet.

Bien entendu, l’historique des sources est stocké sur le serveur. Perdre le serveur reviendra à perdre tout l’historique. De même, le projet sur les postes peuvent ne pas être à jour. Ne vous laissez pas surprendre !

Suppression du code mort

Le GDS permet de supprimer toutes sortes de code mort.

J’ai vu beaucoup de projet où les développeurs laissaient du code en commentaire, au cas où… En regardant la date de ces commentaires, on se rend compte qu’ils ont plusieurs années. Ils sont laissés à l’abandon et commencent à « moisir ». Aucun développeur n’ose les enlever, au cas où…

Avec le GDS, vous n’avez plus cette peur. Vous pouvez supprimer ces commentaires. Au cas où vous en auriez besoin (et soyons honnête, c’est jamais), le GDS se fera un plaisir de vous restituer le code en quelques clics.

Le GDS vous permet de modifier votre code et de revenir à l’état précédent si vous constatez un bug.

Conclusion

Le GDS (ou tout autre gestionnaire de source) est vraiment indispensable pour un développeur. Il enlève beaucoup de « peurs ».

En l’utilisant correctement, vous n’aurez plus peur :

  • pour vos sauvegardes ;
  • de travailler à plusieurs sur un même projet ;
  • de supprimer et d’améliorer votre code.

Car le GDS est le gardien de la mémoire de votre projet.

N’hésitez plus, lorsque vous démarrez un nouveau projet, vous le mettez dans le GDS.

Mettre en place un événement sur un groupe de champs

Introduction

En parcourant les forums, je suis tombé sur cette question : comment mettre en place un événement sur un groupe de champs ?

La solution initiale est d’utiliser la fonction Evenement et elle est très pertinente.

Je vous propose ici une autre solution qui est d’utiliser la propriété ..Traitement des champs. Elle permet d’ajouter un comportement à un champ lorsqu’un événement se produit.

Nous allons utiliser ce système pour ajouter un nouvel événement à tous les champs du groupe.

Les étapes

Etape 1 – Lister les champs du groupe

Dans un premier temps, je souhaite obtenir la liste des champs qui sont associé au groupe. Pour cela, nous allons utiliser la méthode EnumèreChamp. Cette méthode permet de lister les champs d’un groupe (mais aussi d’une fenêtre et d’autres éléments).

Vous pouvez définir une procédure locale à la fenêtre, mais je vous encourage à créer une procédure globale, vous pourrez ainsi l’utiliser dans toutes vos fenêtres.

PROCEDURE lister_champs_du_groupe(groupe est un Champ) : tableau de Champs
champs est un tableau de Champs

indice_champ est entier = 1

nom_champ_courant est une chaîne = EnumèreChamp(groupe, indice_champ)
TANTQUE ChampExiste(nom_champ_courant)
    champ est un Champ <- {nom_champ_courant, indChamp}
    Ajoute(champs, champ)

    indice_champ ++
    nom_champ_courant = EnumèreChamp(groupe, indice_champ)
FIN

RENVOYER champs

Etape 2 – Associer le nouveau traitement au champ

Une fois que j’ai obtenu mes champs, je souhaite associer le traitement à l’ensemble du groupe. J’ai séparé le travail en deux méthodes.

La première permet d’associer le traitement au groupe :

PROCEDURE associer_traitement_au_groupe(identifiant_traitement est entier, 
procédure_traitement est une Procédure, groupe est un Champ)

POUR TOUT champ DE lister_champs_du_groupe(groupe)
    associer_traitement_au_champ(identifiant_traitement, procédure_traitement, champ)
FIN

La seconde permet d’associer le traitement au champ, c’est dans ce code que vous verrez la syntaxe intéressante. La propriété ..Traitement permet d’associer un nouveau traitement au champ sous la forme d’une procédure (les variables de type procédure sont acceptées).

Cette propriété possède elle même deux propriétés ..Avant et ..Après qui permettent de définir si on ajoute la nouvelle procédure avant ou après la procédure déjà définie.

Enfin, il faut faire très attention à l’opérateur d’ajout. Dans l’exemple, j’utilise += pour ne pas écraser d’éventuelles traitement déjà défini sur le champ.

PROCEDURE associer_traitement_au_champ(identifiant_traitement est entier, procédure_traitement est une Procédure, champ est Champ)

champ..Traitement[identifiant_traitement]..Après += procédure_traitement

Etape 3 – Mettre en place le nouveau comportement

Je suis fin prêt à mettre en place le nouveau traitement dans ma fenêtre. Je place ce code dans le traitemet Fin d’initialisation de ma fenêtre.

Pour l’exemple, je crée les éléments suivants dans ma fenêtre :

  • un groupe de champ nommé gcSaisie ;
  • quatre champs de saisie associés au groupe gcSaisie ;
  • un libellé nommé lbChampEtHeureModification.

Je vais aussi utiliser une procédure interne nommé enregistrer_modification qui contiendra le nouveau comportement. Il faut noter que pour connaître le champ courant dans la procédure interne, on peut utiliser le mot clé MoiMême.

La constante trtModification correspond au traitement à chaque modification de lorsque vous allez dans le code du champ. Mais d’autres constantes sont disponibles sur la page associée.

associer_traitement_au_groupe(trtModification, enregistrer_modification, gcSaisie)

    PROCEDURE INTERNE enregistrer_modification()
        lbChampEtHeureModification = MoiMême..Nom + " : " + HeureVersChaîne(HeureSys(), "hh:mm:ss")
    FIN

Le libellé se met à jour chaque fois que l’un des champs est modifié en indiquant le nom du champ à l’origine de la modification et l’heure de modification.

Conclusion

Je vous ai montré comment mettre en place un traitement spécifique sur plusieurs champs sans devoir coder l’ensemble de tous les champs. Vous pouvez vous servir de cette fonctionnalité pour détecter si une fiche a été modifiée et activer / désactiver le bouton de Validation.

Vous pouvez aussi vous en servir pour mettre en place un undo / redo sur l’ensemble des champs de votre fiche.

Convertir un enregistrement en variant et donc en json

Si vous avez lu mon article sur les variants (http://blog.ytreza.org/windev-les-variants-et-json/), vous saurez que j’aime les utiliser. Il y a deux raisons à cela, ils sont vraiment pratiques et ils permettent de faire du json.

En ce moment, je travaille sur un projet où on doit faire de l’échange de données. Mes données sont dans une base HFSql et je les transmets en Json. Il me fallait donc une méthode pour mettre en place ce transfert facilement.

Je vous ai donc concocté une petite procédure qui fait ça tout simplement. La voici :

PROCEDURE HVersVariant(Enreg est un Enregistrement)
UnVariant est un Variant

POUR TOUTE CHAÎNE NomRubrique DE HListeRubrique(Enreg) SEPAREE PAR RC
    NouveauMembre est un MembreVariant
    NouveauMembre..Nom = NomRubrique
    NouveauMembre..Valeur = {"Enreg." + NomRubrique}
    Ajoute(UnVariant..Membre, NouveauMembre)
FIN

RENVOYER UnVariant

Et l’utilisation est toute simple. J’ai par exemple un fichier CLIENT. Je souhaite convertir un enregistrement en variant :

HLitPremier(CLIENT)
Enreg est un Enregistrement de CLIENT = CLIENT
UnVariant est un MembreVariant = HVersVariant(Enreg)

Et si je veux convertir mon variant en json :

Trace(VariantVersJSON(UnVariant, psdMiseEnForme))

Et voilà ! Une procédure toute simple pour faire du Json à partir de vos enregistrements.

Débugger les boucles comme un pro

Corriger les bugs dans les programmes, c’est tout un sport, surtout quand le bug est perdu au milieu d’une boucle qui est très longue à s’exécuter.

A force d’expérience, chacun met au point ses méthodes. Je vous livre aujourd’hui une des astuces que j’utilise assez souvent pour corriger mon code.

Et pour faire moderne, j’utilise de nouveau le format vidéo. Bon visionnage !

Filtrer et refiltrer une table basée sur une variable

Vous avez dû vous en rendre compte, j’aime beaucoup utiliser les tables basées sur une variable. Je trouve que cela permet de séparer la base de données de l’IHM.

En règle générale, pour appliquer des filtres, on passe par les loupes prévus dans les entêtes de colonne. Elles sont assez pratiques, mais elles ont quand même des limites. C’est pourquoi il m’arrive de rajouter une zone de saisie me permettant de filtrer. Je remplis le tableau avec les éléments voulus, un petit coup de TableAffiche(MaTable, taInit) et le tour est joué, ma table est à jour.

Mais comment faire lorsque le remplissage de ce tableau coûte cher. Par exemple, on récupère des données sur Internet et on souhaite les afficher. A chaque fois qu’on change le filtre, on effectue de nouveau la requête sur Internet ?

Voici ma solution : il suffit tout simplement d’avoir deux tableaux, un qui contient les données et l’autre qui contient les données filtrées. A chaque fois que l’on modifie le filtre, il n’y a qu’à remplir le tableau filtré à partir des données du tableau des données. Et appliquer un filtre sur des données déjà en mémoire, c’est beaucoup, beaucoup plus rapide.

Par exemple, dans la déclaration des variables globales de la fenêtre :

tDonnées est un tableau de Personne = RécupérerDonnées()
tDonnéesFiltrées est un tableau de Personne = FiltrerDonnées(tDonnées)

Et à chaque modification du filtre :

tDonnéesFiltrées = FiltrerDonnées(tDonnées)
TableAffiche(MaTable, taInit)

Tout simplement.

Indirection sur les variants

J’avais lu une news comme quoi un correctif permettait de faire des indirections sur les variants en WinDev® Mobile mais je ne m’étais pas posé plus de question. Et aujourd’hui, une collègue bloque sur la création de membre pour un variant de manière dynamique et cette news me revient en tête.

Ça fonctionne et c’est très pratique, vous pouvez faire des indirections sur les variants. Inutile de vous embêter avec des variables de type membrevariant, voyez plutôt :

MonVariant est un variant
{"MonVariant.test"} = "coucou"
Trace(VariantVersJSon(MonVariant))

Bingo, le membre test a bien été créé.

Je ne le conseille pas, mais on peut aussi ajouter des espaces dans le nom du membre.

MonVariant est un variant
{"MonVariant.te st"} = "coucou"
Trace(MonVariant.'te st')
Trace(VariantVersJSon(MonVariant))

Dans ce cas, si vous voulez appeler la valeur du membre, vous devez l’encadrer par des quotes simples (‘).

J’avoue que je suis assez fan.

Partager des événements entre applications

Suite à une question sur le forum, voici une méthode vous permettant de partager des événements entre deux ou plusieurs applications WinDev®.

Explication

Le principe est d’utiliser la méthode PostMessage (ou SendMessage). Ces procédures permettent d’envoyer un message windows.

On réceptionne ensuite ces messages via la procédure Evénement.

Avant, on était obligé de définir des n° de messages (toujours expliqué dans l’aide), mais il est possible de simplifier le processus en utilisant des chaînes de caractères personnalisées pour chaque message que l’on souhaite créer. C’est ensuite WinDev qui se charge d’associer cette chaîne de caractère à un n° de message Windows.

Enfin, pour envoyer le message à toutes les applications, il suffit d‘indiquer -1 dans le hwnd de la fonction PostMessage. Sinon, il faut aller rechercher le handle de la fenêtre en question, et c’est un peu de travail en plus, alors que nous, nous voulons des choses simples.

Un exemple valant plus que 10000 mots, je vous propose de tester cette méthode ensemble :

Vous devez créer un ou plusieurs projets.

Pour chaque projet qui doit réceptionner le message ou les messages, vous devez créer une fenêtre. Dans le code d’initialisation de la fenêtre, vous indiquez :

CONSTANTE
    _MESSAGE_ = "MESSAGE_TEST"
    _MESSAGE_2_ = "MESSAGE_TEST 2"
FIN

Evénement(_tester_01, "*.*", _MESSAGE_)
PROCEDURE INTERNE _tester_01(_message)
    Trace(ChaîneConstruit("Evenement '%1' reçu", _message))
FIN

Evénement(_tester_02, "*.*", _MESSAGE_2_)
PROCEDURE INTERNE _tester_02(_message)
    Trace(ChaîneConstruit("Evenement '%1' reçu", _message))
FIN

Et voilà, cette fenêtre affichera une trace au moment de la réception du message.

Pour envoyer le message « MESSAGE_TEST »

PostMessage(-1, _MESSAGE_, 0, 0) // Non bloquant
// ou SendMessage(-1, _MESSAGE_, 0, 0) // Bloquant

Pour envoyer le message « MESSAGE_TEST 2 »

PostMessage(-1, _MESSAGE_2_, 0, 0) // Non bloquant
// ou SendMessage(-1, _MESSAGE_2_, 0, 0) // Bloquant

Conclusion

Voilà, avec cette méthode vous pouvez déclencher des événements dans une application à la demande d’une autre application. Bien entendu, cette méthode est à utiliser avec précaution car elle n’est pas facile à debugger et peut vous apporter plus de problèmes que de solutions.

Pour bien intégrer cet exemple, prenez 10 minutes et testez le code que je vous ai donné.

Un Compile qui compile

J’utilise la fonction Compile tous les jours. En effet, je me suis créé un petit exécutable qui me permet de saisir du code WLangage et je l’exécute en appuyant sur F5. Une trace m’affiche le résultat et les erreurs. C’est très pratique pour tester une fonction ou un code en quelques secondes.

Mais bien que je m’en serve souvent, je trouve la fonction Compile assez limitée. Une des particularités est qu’on ne peut définir qu’une seule fonction dedans et les procédures internes sont interdites.

Malgré ce que je dis, il est quand même possible de définir d’autres procédures, tout simplement en ré-utilisant la fonction compile.

Voici un exemple de code que vous devez compiler avec Compile.

PROCEDURE MaProcédure()
AfficherLeCarré est une procédure = Compile([
    PROCEDURE AfficherLeCarré(Valeur est un entier)
        Trace(Valeur * Valeur)
])
POUR i = 1 A 10
    AfficherLeCarré(i)
FIN

Si vous souhaitez réutiliser la fonction dans les autres procédures compilées dynamiquement, vous devez donner un nom à votre procédure. Mais attention, dans ce cas, votre fonction est déclarée de manière globale au niveau du projet. Faîtes attention à ne pas utiliser un nom de fonction déjà utilisé.

PROCEDURE MaProcédure()
AfficherLeCarré est une procédure = Compile("AfficherLeCarré", [
    PROCEDURE AfficherLeCarré(Valeur est un entier)
        Trace(Valeur * Valeur)
])

AfficherCarréDansInterval est une procédure = Compile("AfficherCarréDansInterval", [
    PROCEDURE AfficherCarréDansInterval(ValDébut est entier, ValFin est entier)

    POUR i = ValDébut A ValFin
        AfficherLeCarré(i)
    FIN
])
AfficherCarréDansInterval(5, 10)
AfficherCarréDansInterval(25, 30)

Collez ce code dans un fichier texte, et utilisez la fonction Compile dessus (avec un fChargeTexte) et exécutez la fonction ainsi créée.

Et voilà, avec cette astuce, vous pouvez utiliser des procédures dans vos procédures compilées dynamiquement.

Une fenêtre est un champ avec des traitements

Je voulais voir si on pouvait coder directement les traitements d’une fenêtre. La réponse est oui !

Le test pour voir ce que ça donne :

PROCÉDURE MaFenêtre()
champ_fenetre est un Champ <- MaFenêtre
champ_fenetre..Traitement[trtMiseAJourIHM] = test

    PROCÉDURE INTERNE test()
        Trace("coucou")
    FIN

A quoi cela va me servir ? Tout simplement à avoir un objet qui va contenir toute la logiqe de ma fenêtre et avec l’attribut d’extension présentation je vais pouvoir gérer l’événement Demande de mise à jour de l’affichage de la fenêtre dans une classe et appeler directement une méthode de ma classe.

Utiliser les composants pour faire le ménage dans les classes

En développant yLogger, je suis passé par plusieurs versions du programme afin d’éviter quelque chose qui me gène au plus haut point. Je parle de la multiplication des classes.

En effet, WinDev® n’offre pas de notion de package et lorsqu’on déclare une classe, elle est globale à tout le projet. Cela peut devenir rapidement lourd lorsqu’on travaille exclusivement avec la POO et que l’on utilise l’héritage des classes. On peut se retrouver avec un projet ayant énormément de classes qui ne sont utilisées qu’à très peu d’endroit.

Cela fait donc plusieurs années que je cherche des méthodes pour essayer de mettre en place une notion de package pour organiser correctement mes projets. Mais jusqu’à présent, je n’ai rien trouvé qui me convienne…

Mais j’ai quand même découverts des méthodes intéressantes !

Les pseudo-classes

C’est un terme que j’ai inventé dans le cadre de WinDev. J’ai utilisé des structures avec des procédures internes (voir mon article sur les procédures internes.

Cette méthode m’a permis de regrouper au sein d’un même fichier de collection de procédures globales tous les objets dont j’avais besoin. La collection devient ainsi un espace de nom et m’a permis de simuler la notion de package.

De plus, il me suffisait de partager via le gds un seul fichier wdg pour avoir mes pseudos-classes dans n’importe quel projet.

Je pensais avoir trouvé LA solution. Seulement, bien qu’on puisse la simuler un peu, la dérivation des classes n’est hélas pas possible avec cette méthode.

Pire, si j’encapsulais cette collection dans un composant et que je l’utilisais dans un composant, il ne m’était plus possible d’utiliser les procédures internes dans un composant (j’y reviendrai certainement dans un autre article).

Cette méthode est donc très intéressante, mais elle a son lot de défaut. Je l’utilise donc toujours, mais avec parcimonie…

Le composant

En travaillant avec la méthode précédente sur les composants, j’ai vu une fonctionnalité très intéressante qui pouvait peut-être m’aider. J’ai donc ré-écris yLogger en utilisant des classes en lieu et place de mes pseudos-classes.

Je me suis donc retrouvé avec plusieurs classes que je ne souhaitais pas afficher dans mes projets. J’ai donc utilisé cette fonctionnalité qui est d’indiquer les éléments accessibles et j’ai décoché tous ceux que je ne voulais pas voir apparaître à l’exception des classes de base qui me permettent de manipuler les autres.

Ce qui donne ça : Éléments accessibles

Mais cette technique ne permet pas d’instancier mes classes dérivées. Pour contourner cela, j’ai tout simplement fait ce que je faisais pour les pseudos-classes. J’ai défini dans ma collection de procédure initiale une fonction qui appelle le constructeur de la classe dérivée.

PROCEDURE créer_formateur_json()
formateur est un cFormateur dynamique = allouer un cFormateurJson()
Renvoyer formateur

Et voilà, avec cette méthode, j’ai pu utiliser et instancier une classe sans qu’elle soit accessible (et visible) dans le projet qui utilise les composants. Et tout cela en mettant accessibles 4 fichiers au lieu de 9 dans mon exemple).

Cette méthode n’est pas donc pas parfaite mais si vous travaillez avec des composants et la POO, vous pouvez l’utiliser pour faire un peu de ménage dans votre projet principal (celui qui intègre les composants).

Résumé

Nous avons vu dans cet article deux méthodes pour diminuer le nombre de classes dans un projet. L’utilisation de mon concept de pseudo-classe et l’utilisation d’un composant avec une collection de procédures pour masquer les classes non nécessaires.

Ces méthodes ne sont pas parfaites, mais elles permettent de mieux organiser le code.

Valeurs de retour multiples

Je crée dans WinDev® des procédures depuis des années. Avec le temps, on peut dire que je suis devenu un pro de la procédure. Vous avez peut-être lu mes articles, je jongle avec les procédures globales, les procédures locales, les procédures internes et les procédures compilées dynamique.

Mais il y a quelque chose sur lequel je n’ai jamais trouvé de solution qui me conviennent, c’est lorsque ma procédure doit renvoyer plusieurs valeurs. Voyons les différentes méthodes et pourquoi je ne ne suis pas satisfait. Et peut-être vous trouvez vous dans le même cas ?

Pourquoi renvoyer plusieurs valeurs ?

C’est une question fondamentale. Dans l’idéal, vos fonctions ne devraient renvoyer qu’une seule valeur. Une addition renvoie une valeur, une multiplication aussi.

Bien que chaque cas soit particulier, la plupart du temps, c’est pour gérer les erreurs. Le plus souvent, on définit une valeur qui symbolise l’erreur. Par exemple, lorsqu’on ouvre un fichier, on attend un identifiant numérique, on renvoie -1 lorsqu’on ne peut pas l’ouvrir. Cette méthodologie fonctionne dans certains cas, mais si j’ai une fonction diviser, cette méthode ne permet pas d’exprimer l’erreur provoquée lors d’une division par zéro.

On peut bien entendu gérer les erreurs par un traitement dédié, mais il est souvent lourd à mettre en place, et on préfère trouver un contournement avec une valeur. Dans un prochain article, j’aborderai la gestion des erreurs, mais pour le moment, on passe par des valeurs de retours multiples.

Il y a d’autres cas où l’on doit renvoyer plusieurs valeurs de retour. Je n’ai pas d’exemple précis, je vous laisse donc les trouver.

Comment renvoyer plusieurs valeurs ?

Il y a plusieurs méthodes. Je vais les énumérer de la plus sale à la plus propre, avec leur avantages et inconvénients.

La variable globale

On définit une ou plusieurs variables globales et on les affecte lorsqu’on utilise la fonction. Pour moi, c’est une méthode interdite pour plusieurs raisons :

  • je ne vois pas la déclaration de ces variables ;
  • je ne sais pas comment sont utilisées ces variables alors qu’elles sont dans le code de ma fonction ;
  • elles sont affectées par chaque appel de la fonction, et s’il y a plusieurs appels imbriqués, on court à la catastrophe.

On oublie !

Le paramètre passé par référence

J’utilise les paramètres passés par référence dans certains cas, je ne serai donc pas violent. Mais dans la mesure du possible, j’évite de passer de tels paramètres.

Les raisons sont les suivantes :

  • l’utilisation n’est pas intuitive ;
  • le fait que ce soit une variable de retour n’est pas très explicite et oblige à mettre un commentaire ;
  • quelqu’un qui n’a pas lu le commentaire ne comprendra pas pourquoi une des variables qu’il utilise change de valeur.

La structure

C’est une méthode beaucoup plus lisible que les précédentes, mais elle présente quand même des petits défauts :

  • c’est lourd à mettre en place, il faut définir la fonction et la structure qui va avec ;
  • ça augmente les dépendances car la structure doit être déclarée quelque part et ce quelque part doit être accessible partout où l’on utilise la fonction ;
  • dans certains cas, les différentes valeurs n’ont rien à voir l’une et l’autre, et donc, leur présence dans la même structure n’est pas intuitive.

Le variant

Suite à une remarque d’Alexis dans les commentaires, on peut aussi utiliser les variants comme valeur de retour. Le fonctionnement sera très proche des structures, mais contrairement à celles-ci, il n’y a pas de définition à mettre en place et il n’y a aucune dépendance. L’utilisation est donc plus facile.

Je vous le déconseille toutefois pour les raisons suivantes :

  • Comme le dit Alexis, c’est opaque donc illisible. L’auto-complétion est impossible sur un variant. Il faut donc aller lire la documentation pour connaître sa structure.
  • Vous êtes limités dans le type des données transmises. En effet, vous ne pouvez pas renvoyer d’objets, ni de procédures, ni de tableau, ni n’importe quel élément qui subit une conversion lorsqu’on l’affecte à un variant.

La solution ?

Depuis WinDev 19, on peut utiliser la syntaxe permettant de renvoyer plusieurs valeurs de retours. La documentation de Renvoyer explique tout cela en détail.

Voici un petit exemple qui vous permettra de tester cette superbe fonctionnalité :

PROCEDURE Test(pNombre est entier)
RENVOYER (pNombre, NombreEnLettres(pNombre))

Et le test qui permet d’utiliser la fonction :

var1 est un entier
var_un est une chaîne

(var1, var_un) = Test(1)

soit (var2, var_deux) = Test(2)

Trace(var1, var_un, var2, var_deux)

Trace(Test(3))
Trace(Test(4), Test(5))

stStructure est une Structure
    UnEntier est un entier
    UneChaine est une chaîne
FIN

var6_six est une stStructure

AVEC var6_six
    (.UnEntier, .UneCHaine) = Test(6)
FIN

Trace(var6_six.UnEntier, var6_six.UneChaine)

Var7 est un entier
Var7 = Test(7)
Trace(Var7)

Var_huit est une chaîne
(,Var_huit) = Test(8)
Trace(Var_huit)

soit (, var_neuf) = Test(9)
Trace(var_neuf)
soit (var10) = Test(10)
Trace(var10)

Voici quelques explications pour chaque valeur :

  1. on initialise deux variable ;
  2. on peut utiliser le mot clé Soit pour définir et initialiser les deux variables ;
  3. on peut directement utiliser le résultat de la fonction dans une autre procédure, dans le cas présent, les 2 premiers paramètres de la procédure Trace sont initialisés ;
  4. idem que pour le 3, mais on peut utiliser plusieurs fonctions à retours multiples.
  5. idem que 4 ;
  6. on ne peut pas initialiser une structure, mais on peut contourner la question en passant les membres de la structure ;
  7. on peut récupérer la première valeur de retour ;
  8. on peut récupérer la deuxième valeur de retour ;
  9. avec le mot clé Soit, on peut définir et initialiser une variable avec la première valeur de retour ;
  10. idem que 9 mais pour la seconde valeur.

Et voilà, vous savez beaucoup de choses à propos de cette fonctionnalité très puissante mais très méconnue de WinDev. Il ne vous reste plus qu’à imaginer dans quel cas vous pouvez l’utiliser.

Résumé du billet

Nous avons vu dans ce billet les différentes méthodes pour renvoyer plusieurs valeurs de retour dans une fonction. Il est possible de le faire avec des variables globales, des paramètres passés par référence et des structures.

Nous avons aussi vu qu’il était possible d’utiliser la fonctionnalité des valeurs de retour multiples présent depuis la version 19 de WinDev.

Utiliser un tableau d'éléments comme contenu pour un sélecteur

J’aime énormément travailler avec des tables ou des listes alimentés par des variables. Mais cela ne fonctionne pas pour les sélecteurs. Mais j’ai développé une petite fonction qui permet de simuler le comportement avec une fonction SélecteurAffiche.

Tout d’abord, il faut paramétrer le sélecteur pour qu’il retourne la valeur spécifiée par la Valeur Renvoyée de Option dans l’onglet Détail.

Et ensuite vous pouvez utiliser le code suivant :

PROCEDURE SélecteurAffiche(Sélecteur est un Champ, tDonnées est un tableau de stElement)

TANTQUE SélecteurOccurrence(Sélecteur) > 1
    SélecteurSupprime(Sélecteur, 1)
FIN

POUR TOUT UnElement DE tDonnées
    soit Indice = SélecteurAjoute(Sélecteur, UnElement:Libellé)
    Sélecteur[Indice]..ValeurRenvoyée = UnElement:Code
FIN

SélecteurSupprime(Sélecteur, 1)

Et un code exemple :

stElement est une Structure
    Code est une chaîne
    Libellé est une chaîne
FIN

tElements est un tableau de stElement = [["01", "Libellé 01"], ["02", "Libellé 02"], ["03", "Libellé 03"]]

SélecteurAffiche(Sélecteur1, tElements)

Et voilà, un sélecteur qui affiche le contenu d’un tableau. Libre à vous d’adapter le code pour aller encore plus loin.

Éditer un champ de saisie dans n'importe quel éditeur texte

je vous propose une petite astuce pour rendre une zone de saisie éditable dans n’importe quel éditeur de texte y compris ceux qui ne permettent pas d’ouvrir en mode bloquant (style Notepad ++ ou Sublime Text).

L’astuce consiste à ouvrir le texte dans un fichier temporaire et à surveiller les modifications qui surviennent sur ce fichier.

Voici les étapes qui permettent d’arriver à ce résultat :

Vous devez d’abord créer une variable qui contiendra le chemin du fichier d’édition : FichierEdition.

Ensuite, vous devez créer les fonctions suivantes :

Celle qui permet d’arrêter la surveillance d’un fichier. Elle est utilisée dans le code suivant, mais elle doit être aussi utilisée lorsqu’on arrête de surveiller le fichier, par exemple, dans le cas de la fermeture de la fenêtre :

SI PAS FichierEdition ~= "" ALORS
    fSurveilleStop(FichierEdition)
    SI fFichierExiste(FichierEdition) ALORS fSupprime(FichierEdition)
    FichierEdition = ""
FIN

Et celle qui contient le reste :

PROCEDURE EditeurTexteOuvrir(LOCAL pChamp est un Champ)
Texte est une chaîne = pChamp

FichierTemporaire est une chaîne = fFichierTemp("EDT") + ".txt"
fSauveTexte(FichierTemporaire, Texte)

HashFichierTemporaire est une chaîne = HashFichier(HA_HMAC_MD5_128, FichierTemporaire)
iEditeurTexteDémarrerSurveillance(FichierTemporaire, iSurModificationFichier)

LanceAppliAssociée(FichierTemporaire)

    PROCEDURE INTERNE iSurModificationFichier()
        Contenu est une chaîne = fChargeTexte(FichierTemporaire, foAnsi)
        NouveauHash est une chaîne = HashFichier(HA_HMAC_MD5_128, FichierTemporaire)

        // Notepad ++ et Sublime Text semblent sauvegarder le fichier 2 fois. La première fois à vide et la seconde fois avec les données
        // On ne considère pas comme une modification le fait que le fichier soit vide
        FichierModifié est un booléen = PAS Contenu ~= "" _ET_ NouveauHash <> HashFichiertemporaire

        SI FichierModifié ALORS
            HashFichiertemporaire = NouveauHash
            ExécuteThreadPrincipal(iPrévenirUtilisateur)
        FIN

        PROCEDURE INTERNE iPrévenirUtilisateur()
            TexteModifié est une chaîne = fChargeTexte(FichierTemporaire)

            ModificationDétectée est un booléen = (PAS TexteModifié ~= pchamp)

            SI ModificationDétectée ALORS
                SELON Dialogue("Le texte a été modifié dans l'éditeur de texte. Souhaitez-vous récupérer les modifications ?", ["Récupérer le texte", "Annuler"],1, 2)
                    CAS 1 :
                        pChamp = TexteModifié
                        ExécuteTraitement(pChamp, trtModification)

                    CAS 2 : // On ne fait rien
                FIN
            FIN
        FIN
    FIN

    PROCEDURE INTERNE iEditeurTexteDémarrerSurveillance(pFichierSurveillé est une chaîne, pProcédureSurModification est Procédure)
        EditeurTexteStopperSurveillance()
        FichierEdition = pFichierSurveillé

        fSurveilleFichier(pFichierSurveillé, pProcédureSurModification, fsTout)
    FIN

Vous n’avez plus qu’à appeler la fonction EditeurTexteOuvrir et à lui passer le champ de saisie que vous souhaitez éditer.

Les expressions régulières, le casse-tête des développeurs (partie 1)

Les expressions régulières, kesako ?

^[a-zA-Z-_]+@[a-zA-Z-_]+\.[a-zA-Z_-]{2,6}$
^[0-9]{2}[/-.][0-9]{2}[/-.][0-9]{4}$

Si c’est la première fois que vous voyez un code de ce genre, ne paniquez pas. Cela peut paraître très compliqué, mais ça ne l’est pas tant que ça (un peu quand même). Je vous présente deux expressions régulières. La première permet de vérifier qu’une adresse mail est valide, la seconde permet de contrôler une date.

C’est un outil très puissant qui permet d’analyser et d’extraire des informations depuis une chaîne de caractère. Si la fonction Position de WinDev est un vélo, les expressions régulières sont un TGV.

On peut s’en servir pour contrôler la saisie sur des zones où la structure de la chaîne est importante, on peut aussi les utiliser pour parser un texte. Bref, c’est un outil très pratique, mais il paraît tellement compliqué que certains développeurs préfèrent vivre sans (c’est possible, mais c’est dommage).

Comment WinDev gère les expressions régulières

WinDev® propose une et une seule fonction pour traiter les expressions régulières : VérifieExpressionRégulière (Je vous laisse lire la documentation et revenir ici, on a encore du travail).

Mais il y a un problème avec cette fonction. C’est que je n’ai pas réussi à déterminer quelle norme elle utilise pour gérer les expressions régulières. Il faut savoir qu’il existe plusieurs normes et chacune a ses spécificités. Pour ma part, je suis habitué à la norme qui vient de Perl. Vous trouverez un petit listing sur Wikipedia.

J’avais donc écrit il y a quelques années une fonction qui me permettait de me rapprocher de la norme définie par Perl. Mais pour vous, Je l’ai remise au goût du jour et la voici, flambant neuve.

Voici la procédure RegExp qui a la même syntaxe que VérifieExpressionRégulière :

PROCEDURE RegExp(Expression est une chaîne, LOCAL Format est une chaîne, *)
    Format = TraduireRegExpPerlVersRegExpWd(Format)
    QUAND EXCEPTION DANS
        RENVOYER VérifieExpressionRégulière(Expression, Format, MesParamètres[3 A ])
    FAIRE
        RENVOYER Faux
    FIN

Et la procédure TraduireRegExpPerlVersRegExpWd.

PROCEDURE TraduireRegExpPerlVersRegExpWd(LOCAL Format est une chaîne)

Format = (Gauche(Format, 1) = "^") ? Format[[2 A ]] SINON ".*" + Format
Format = (Droite(Format, 1) = "$") ? Format[[1 A Taille(Format) - 1]] SINON Format + ".*"

tElements est un tableau de chaîne = ["", "", Format]
Format = ""
TANTQUE VérifieExpressionRégulière(tElements[3],"([^\]*)(\\.)(.*)", tElements)
    Format += tElements[1] + iTraduire(tElements[2])
FIN

Format += tElements[3]
RENVOYER Format

    PROCEDURE INTERNE iTraduire(LOCAL ElementATraduire est chaîne)
        SELON ElementATraduire
            CAS "\d" : RENVOYER "[0-9]"
            CAS "\D" : RENVOYER "[^0-9]"
            CAS "\t" : RENVOYER TAB
            CAS "\n" : RENVOYER RC
            CAS "\s" : RENVOYER "[" + RC + TAB + " ]"
            CAS "\S" : RENVOYER "[^" + RC + TAB + " ]"

            AUTRE CAS : RENVOYER ElementATraduire
        FIN
    FIN

Enfin, sachez qu’il y a d’autres méthodes pour gérer les expressions régulières dans WinDev. Cependant, cela implique d’avoir des dépendances avec d’autres langages, et à force de me lire, vous devez le savoir, je n’aime pas les dépendances. Mais cela n’engage que moi, je vous mets donc un lien vers le forum de PCSoft® : Expressions régulières, regex, cherche et remplace (Merci aux contributeurs de ce post). Je l’ai testé il y a des années, mais l’outil du vbscript marchait bien à l’époque.

Je vous apprendrai dans un prochain billet à utiliser les expressions régulières. Il nous reste à apprendre les bases des expressions régulières ainsi que les options de capture. Mais il y a tant à dire que je pourrais y consacrer tous les articles de ce blog. En attendant, n’hésitez pas à poser vos questions par commentaire, ça me permettra de savoir ce que devra contenir le prochain billet.

Code source et exemple complet de ce billet

Comme d’habitude, je met à disposition de mes abonnés le projet complet qui m’a permis de mettre en place l’exemple.

Ce projet contient :

  • la collection de procédures yChaine qui contient les procédures RegExp et TraduireRegExpPerlVersRegExpWd ;
  • les tests unitaires qui m’ont permis de développer les deux procédures et qui donnent quelques exemples d’expressions régulières.

Le projet est stocké dans le dossier :

  • 2017-11-19 Expression reguliere.

Résumé du billet

Nous avons vu dans ce billet une courte présentation des expressions régulières. Il existe plusieurs normes et WinDev semble avoir sa propre norme. Afin d’avoir une version plus standard, j’en encapsulé l’utilisation de VérifieExpressionRégulière dans une fonction regexp qui se rapproche de la norme Perl. Enfin, je propose aux abonnés un projet contenant le code source de l’article.

Savoir si une date est dans une période sans tenir compte de l'année

Hier, je faisais mon petit tour habituel sur les forums de PCSoft® et je suis tombé sur une question intéressante. La personne voulait savoir si une date était dans la période de Noël mais sans tenir compte de l’année.

Vous pouvez lire le thread en question ici.

La réponse la plus courante a été de de recalculer à chaque fois les bornes de début et de fin en fonction de la date passée en paramètre.

Mais si on souhaite ignorer l’année, pourquoi s’embêter avec ?

Voici une petite fonction qui permet de résoudre ce problème :

PROCEDURE EstDansPériodeSansAnnée(Date est une Date, BorneDébut est une chaîne, BorneFin est une chaîne)

PériodeSurDeuxAnnées est un booléen = BorneDébut > BorneFin 

SI PériodeSurDeuxAnnées ALORS
    RENVOYER PAS (BorneFin < DateVersChaîne(Date, "MMJJ") < BorneDébut)
SINON
    RENVOYER BorneDébut <= DateVersChaîne(Date, "MMJJ") <= BorneFin
FIN   

Les dates au format AAAAMMJJ sont intéressantes parce qu’on peut savoir si une date est supérieure à une autre juste en comparant les chaines à ce format. Ce qui induit que l’on peut aussi comparer la partie MMJJ pour savoir si une date est dans une période ou pas.

Mais dans la question sur le forum, il y a un piège, c’est que la période est à cheval sur deux années. Et ça complique un peu le code. Si la date n’est pas à cheval sur une année, on peut directement comparer la partie MMJJ de la date avec les bornes début et fin.

Mais si elle est à cheval sur les deux années, il faut vérifier si la date est dans la période non-voulue (qui elle est forcément sur la même année). Si elle est dans la période non-voulue, alors elle n’est pas dans la période demandée.

Et en bonus, une petite closure :

PROCEDURE ClosureEstDansPériodeSansAnnée(LOCAL BorneDébut est une chaîne, LOCAL BorneFin est une chaîne)
p est une Procédure = iEstDansPériodeSansAnnée

    PROCEDURE INTERNE iEstDansPériodeSansAnnée(Date est une Date)
        RENVOYER EstDansPériodeSansAnnée(Date, BorneDébut, BorneFin)
    FIN

RENVOYER p

Et pour l’utiliser :

EstDansPériodeNoel est une Procédure = yDate.ClosureEstDansPériodeSansAnnée("1201", "0115")
SI EstDansPériodeNoel(MaDate) ALORS
    ....
FIN