Bonjour à tous. Merci d'avoir regardé cette présentation. Je vais vous parler de certaines classes JMP et de la façon dont vous pouvez les utiliser en JSL pour implémenter des comportements d'interface dynamiques et extensibles.
Je m'appelle Ben Barrett et je travaille chez IDEXX Laboratories. J'y suis ingénieur depuis une dizaine d'années. J'ai travaillé avec diverses applications, notamment LabVIEW et MATLAB, et j'ai également exploré le C#. De manière générale, quel que soit le langage utilisé, j'ai toujours privilégié une approche de programmation orientée objet.
Pour une raison qui m'échappe, cela me semble intuitif. Cependant, lorsque j'ai commencé à travailler avec des objets en JSL, je me suis rendu compte que cette intuition ne me servait pas à grand-chose. J'ai dû surmonter quelques difficultés, mais une fois que j'ai compris comment les utiliser, je les ai trouvées très utiles.
Je vais commencer par un bref aperçu. Je ne vais pas faire un cours approfondi sur les classes JMP ici. Je souhaite simplement partager mon parcours de découverte lorsque j'ai commencé à les utiliser. J'aborderai les difficultés rencontrées, les problèmes que j'ai surmontés et comment je les ai résolus. Enfin, je présenterai quelques exemples intéressants d'utilisation à la fin.
Voici un programme provisoire ; on verra si je le respecte. Je voudrais aborder brièvement la question de la portée des variables, car j'ai constaté que c'était un problème récurrent dans l'implémentation de certaines classes. Cela me permettra de partager quelques bonnes pratiques. J'aimerais également expliquer en quoi les classes en JSL diffèrent légèrement de celles que l'on trouve dans d'autres langages comme C#. Enfin, je présenterai un exemple de code fonctionnel.
Ne vous attendez pas à un cours approfondi sur les classes en JSL, ni même sur les principes de conception orientée objet en général. J'espère que cela vous évitera quelques maux de tête si vous décidez d'explorer les classes par vous-même.
Voici l'ordre du jour, tout a été passé en revue. Voici un aperçu rapide des différentes portées disponibles en JSL. Je ne prétends pas comprendre parfaitement comment elles interagissent et comment les utiliser. J'en ai quelques notions de base.
Il existe quatre portées générales : la portée locale, la portée globale et vos espaces de noms personnalisés. Ce document explique comment interagir avec chacune de ces portées. Je vous recommande vivement de consulter la documentation en ligne pour plus d'informations. En règle générale, j'ai constaté que, pour travailler avec des classes, la meilleure approche consiste à utiliser les espaces de noms.
Créez vos propres espaces de noms personnalisés si vous devez y faire référence dans vos classes ; cela m'a permis d'éviter de nombreux problèmes de portée et de conflits. J'ai également constaté que l'espace de noms Here n'est pas très fiable pour les classes ; j'évite donc d'utiliser des noms par défaut Here et de le définir sur 1. De plus, l'utilisation de variables locales peut s'avérer utile, à condition d'éviter la commande `local Here`. Cela implique toutefois une maintenance légèrement plus importante, car il faut définir explicitement les variables locales. Ces points seront plus clairs lors des démonstrations.
Je viens de passer en revue ces bonnes pratiques. Évitez les noms par défaut, utilisez des espaces de noms personnalisés, et notamment des classes. On peut considérer les classes comme des espaces de noms copiables, et il est possible d'en avoir plusieurs copies en mémoire simultanément. En quoi les classes JSL diffèrent-elles des classes que vous connaissez peut-être ? Il existe certainement d'autres fonctionnalités que certains utilisateurs attendent des classes, mais ces cinq-là représentent, à mon avis, une description assez standard de ce que l'on attend d'un objet.
Encapsulation, héritage, polymorphisme, réplication et abstraction. L'héritage, le polymorphisme et l'abstraction sont étroitement liés, et les classes JSL offrent une prise en charge assez complète de l'encapsulation, de l'héritage et de la réplication. Toutefois, la mise en œuvre du polymorphisme et de l'abstraction présente certaines subtilités. Je vais aborder brièvement ces cinq points.
L'encapsulation est un concept bien établi. Lorsqu'on définit une classe, on définit des propriétés et des méthodes qui lui sont associées. La seule nuance est qu'aucune de ces affirmations n'est véritablement privée. On ne peut pas définir de membre véritablement privé pour une classe. Tout est public. C'est comparable à un espace de noms : les membres et les propriétés d'une classe sont accessibles globalement.
L'héritage est également pris en charge. Si vous créez une classe dérivée d'une autre, cette classe enfant héritera des membres de la classe parente. Le polymorphisme présente certaines subtilités que nous aborderons plus en détail ultérieurement. Pour implémenter pleinement le polymorphisme, une solution spécifique est nécessaire afin de permettre à une classe parente d'appeler correctement les méthodes de ses enfants depuis ses propres méthodes.
La réplication, c'est précisément l'intérêt d'utiliser des classes. On peut instancier plusieurs copies d'un objet, chacune avec son propre ensemble encapsulé de paramètres et de méthodes, et je n'ai rencontré aucun problème avec cela.
En ce qui concerne l'abstraction, il n'existe pas de notion explicite d'abstraction dans les classes JSL. Cependant, vous pouvez définir une classe parente avec des méthodes par défaut qui peuvent renvoyer une erreur ou simplement effectuer un comportement basique. Si vous êtes familier avec C# ou d'autres langages, cela se rapprocherait davantage d'une méthode virtuelle que d'une méthode abstraite. Voilà un aperçu général des différences entre les classes JSL et celles que vous connaissez peut-être si vous venez d'autres langages. Après cette brève présentation des classes JSL, je vais passer à la suite.
Notre instance JMP est ouverte. J'ai préparé quelques exemples assez simples. Je vais commencer par un exemple simple, puis un autre un peu plus complexe, et enfin un dernier beaucoup plus complexe. Le premier exemple que je vais aborder est une barre de progression très basique.
Ces exemples visent tous à rendre le développement d'interfaces graphiques plus extensible et modulaire, vous permettant ainsi de tirer pleinement parti des capacités des objets. Dans cette barre de progression, je vais simplement l'exécuter rapidement pour que vous puissiez voir son fonctionnement. J'ai trouvé l'implémentation de base d'une barre de progression en ligne, dans une discussion où quelqu'un posait une question à ce sujet. Je vous montrerai les détails de sa création, mais ils ne sont pas essentiels ici.
Ce code configure essentiellement quelques éléments pour définir l'apparence de la barre de progression. Il crée une fenêtre. Il crée… Vous pouvez voir que nous créons ici les objets, et nous examinerons le code de ces objets dans un instant. Ensuite, il organise les éléments dans une fenêtre, en créant des éléments aléatoires… pour représenter la tâche en cours d'exécution. Puis, il itère comme s'il effectuait un traitement et met à jour les barres de progression à l'aide de ces méthodes, qui font partie de la définition de la classe.
Il n'y a pas de changement de direction ici, mais cet objet encapsule tout le comportement qui crée cette visualisation. Je vais ouvrir cette fenêtre. Nous sommes maintenant dans le code. Je vais accéder à la définition de la classe de la barre de progression et réduisons rapidement ces fenêtres. Vous pouvez voir que vous définissez une classe en utilisant l'appel `define class`, en lui donnant un nom, et c'est ainsi que vous allez l'instancier. Ensuite, définir des variables dans une classe est aussi simple que… Vous les définissez de la même manière que n'importe quelle autre variable.
Cette valeur est maintenant assignée à l'instance de la classe. Vous y accédez donc en faisant référence à la classe, puis en utilisant son nom. Les méthodes des classes sont définies de la même manière que les fonctions : ce sont essentiellement des fonctions, mais avec un nom différent, et elles sont assignées à l'objet de la classe. Comme je l'ai mentionné précédemment, les classes sont comparables à un espace de noms ; vous pouvez donc les manipuler. Une fois une instance de classe créée, vous pouvez manipuler tous ces éléments. Vous pouvez ajouter des propriétés qui seront assignées à l'objet créé. Les classes JMP offrent de nombreuses possibilités de manipulation dynamique.
Je ne vais pas entrer dans les détails. Je veux simplement me concentrer sur la création de cette barre de progression. La fonction d'initialisation sera exécutée. Je vais l'afficher dans une vue latérale. Dans cet exemple, cette fonction d'initialisation sera exécutée lors de la création de l'objet. L'appel au nouvel objet exécutera automatiquement la fonction `_init`.
Lors de l'initialisation, une configuration par défaut est définie, mais vous pouvez également la personnaliser. Cette configuration initialise les éléments de base, puis appelle la commande de compilation. La méthode de compilation est définie ci-dessous. C'est dans cette méthode que nous créons tous les objets qui permettent à la barre de progression de prendre son apparence actuelle. Nous avons notre conteneur, ainsi que des espaces, que vous agrandissez et réduisez pour créer l'effet de progression. Cette fonctionnalité provient d'une publication de la communauté JMP dont je n'ai pas le code source sous la main.
Ceci définit précisément l'apparence de la barre de progression. Vous pouvez ensuite voir les méthodes auxiliaires qui permettent de modifier sa hauteur ou sa largeur. Il est possible de les appeler. Enfin, comment interagir avec elle et modifier la progression ? Il s'agit simplement de méthodes aux fonctionnalités différentes : `set` permet de définir explicitement le pourcentage, `step` l'incrémente d'une certaine valeur, et vous pouvez définir d'autres paramètres selon vos besoins.
Vous pouvez spécifier précisément le nombre d'étapes et les incrémenter à chaque itération. Vous pouvez adapter les méthodes en fonction des besoins de la tâche. Reprenons l'exemple, maintenant que le contexte est posé : c'est assez simple. On crée l'objet, puis… Lors de la création de la nouvelle fenêtre, pour exploiter l'objet, il faut pouvoir extraire l'élément d'affichage.
Dans la barre de progression, le conteneur principal est cette liste déroulante H. Mais si nous récupérons l'objet, nous ne faisons que renvoyer sa valeur ou le conteneur lui-même. Dans la définition de la nouvelle fenêtre, si nous voulons insérer la barre de progression à un emplacement précis, nous pouvons simplement récupérer l'objet ou l'objet que nous avons créé et… On pourrait reformuler cela en disant « obtenir le panneau ». Ce serait plus clair, mais voilà.
Bref, voilà comment l'insérer. Ensuite, nous faisons toujours référence à l'objet et nous parcourons les données de la barre principale, puis nous attribuons une valeur explicite à la barre secondaire. Nous mettons à jour le texte. Un dernier rappel pour observer le résultat. Encore une fois, nous appliquons des valeurs aléatoires, nous mettons à jour le code, et vous pouvez facilement imaginer comment l'intégrer à une autre tâche.
Un autre point utile que je souhaite souligner : vous pouvez fermer la barre de progression. Dans ce cas, la prochaine tentative d'itération échouera car le programme ne disposera plus de la référence à la fenêtre. La barre étant fermée, la mise à jour de la fenêtre sera impossible et une erreur sera renvoyée.
Comme tout ceci est inclus dans un bloc try, en cas d'échec, le traitement sera interrompu. C'est donc une méthode rapide et simple pour interrompre le traitement si nécessaire, ce qui m'est arrivé à plusieurs reprises. Voilà la barre de progression. Une implémentation assez basique. On peut la personnaliser de nombreuses façons : afficher des informations, faire ce qu'on veut.
Le point suivant que je souhaite aborder est un peu plus complexe et nécessite probablement une petite préparation. Je vais afficher ces tables. J'ai utilisé la grande table d'exemple et j'y ai inséré des données aléatoires afin de démontrer la fonctionnalité de jointure virtuelle. Pour ceux qui ne connaissent pas les jointures virtuelles, il existe une jointure standard où l'on prend… Nous avons cette classe de scores élevés. Elle contient plusieurs entrées pour chaque étudiant. Ils ont tenté 10 fois de réaliser une tâche et ont obtenu un score. La table ne contient que leur nom, le nombre de tentatives et le score, mais nous avons toutes les autres informations ici. Nous voulons les intégrer ici. Nous effectuons une jointure. Je suis sûr que tous ceux qui utilisent JMP l'ont déjà fait.
Cela permettra d'intégrer ces valeurs dans ce tableau, mais il existe une autre méthode. Tous ces noms sont uniques. Nous pouvons les définir comme identifiants de lien, puis, dans ce tableau, nous pouvons les utiliser pour faire référence à ce tableau. Nous avons ainsi une jointure virtuelle et nous pouvons visualiser toutes ces informations. Si nous revenons ici, je peux masquer ou afficher les données, et nous avons maintenant accès aux informations de cette colonne de ce tableau dans ce tableau sans avoir à effectuer de jointure. Nous pourrions faire la même chose avec ce tableau.
Cela présente des avantages non négligeables. Dans les informations de la colonne et la propriété de référence du lien, vous pouvez configurer le système pour qu'il accepte l'état sélectionné ou exclu. Si j'applique cette configuration et que je reviens ici, lorsque je sélectionne cette ligne, toutes les lignes associées seront également sélectionnées. Le même principe s'applique aux synthèses liées à la table ou à la vue de données.
C'est une fonctionnalité plutôt sympa. J'ai commencé à l'utiliser l'année dernière et j'ai rapidement eu besoin d'automatiser le processus en JSL. J'ai donc fini par créer une sorte de gestionnaire de liens. Je vais effacer ces liens. On va supprimer l'identifiant du lien. On va supprimer la référence du lien. Ce sont maintenant de simples tables non liées.
Je vais ouvrir le gestionnaire de liens d'exemple. Je vais exécuter quelques opérations. Nous allons configurer toutes les tables et initialiser le gestionnaire de liens. Je vous présenterai le code sous-jacent dans un instant.
Voilà. Je ne sais pas quelle était l'erreur, mais voici la fenêtre principale du gestionnaire de liens. L'interface graphique est affichée, mais aucun lien n'est encore créé. L'API nous permet de définir une source de lien, ce qui revient à définir l'identifiant du lien précédemment. Si j'exécute la commande, le gestionnaire de liens sera mis à jour.
Nous obtenons ici une nouvelle entrée d'interface graphique. Nous pouvons maintenant lier l'une des tables. Une nouvelle entrée apparaît. Nous pouvons lier la deuxième table. Une deuxième entrée apparaît, et nous avons maintenant la possibilité d'accéder à ces configurations de liens et de les modifier.
Si je veux que cette requête soit appliquée à la deuxième table, je peux la configurer pour que les critères de sélection et d'exclusion soient tous… En gros, si je sélectionne quelque chose ici, cela se répercutera là. Nous avons sélectionné les éléments. Maintenant, tous ces éléments devraient être sélectionnés, et de plus, dans la requête d'origine, la table source devrait également être sélectionnée.
Normalement, pour mettre à jour ces éléments, il faudrait faire un clic droit, sélectionner « Informations sur la colonne », puis « Référence du lien », « Envoyer » ou « Accepter », modifier les paramètres souhaités, puis appliquer les modifications. Ensuite, il faudrait passer à l'autre élément et répéter l'opération. Je souhaitais une solution plus dynamique, permettant de visualiser l'activité en temps réel. On peut ainsi utiliser une autre table, configurer plusieurs sources, et accéder à tous les paramètres ici. Il est donc possible de désactiver des options, de supprimer un lien, etc.
J'ai trouvé cet outil particulièrement utile pour traiter de grands ensembles de données répartis dans de nombreuses tables. Par exemple, on peut avoir une grande table récapitulative, avoir effectué un traitement sur des données continues et vouloir relier ces données sans créer une table gigantesque, car cela deviendrait vite difficile à gérer. C'est de là qu'est né cet outil.
Maintenant, si l'on s'intéresse au code, cela va être un peu plus complexe, et je tiens à préciser d'emblée que rien n'est optimisé. Il s'agit en grande partie d'une première ébauche. J'ai réussi à le faire fonctionner, mais je n'ai pas encore pris le temps de le peaufiner. Gardez cela à l'esprit lorsque je vais examiner certains points.
Vous pouvez voir ici la fonction d'initialisation que j'ai appelée ; elle vérifie simplement si un objet existe déjà et, le cas échéant, en crée un. Une des améliorations possibles concerne la gestion des liens : j'avais initialement envisagé de pouvoir exécuter plusieurs gestionnaires de liens simultanément si j'avais plusieurs projets ouverts. C'est peut-être utile, peut-être pas, mais pour l'instant, cette classe est implémentée comme un singleton.
Vous pourriez en instancier davantage si vous le souhaitiez, mais cette fonctionnalité serait un peu hasardeuse pour certains cas précis que j'aborderai dans un instant. En résumé, le gestionnaire de liens se contente de gérer des tableaux qui définissent comment les tables sont liées entre elles, quelles tables sont liées à quelles autres et quel est l'état actuel des liens. Il possède des méthodes permettant de modifier les liens et d'effectuer toutes sortes d'opérations.
Pour l'interface graphique, nous avons implémenté deux classes. Il y a la configuration des liens. Si je peux ouvrir… Laissez-moi ouvrir la fenêtre du gestionnaire de liens. Où est-elle ? Juste ici.
Dans la fenêtre du gestionnaire de liens, la configuration des liens correspond à cette zone qui permet de modifier les critères d'acceptation et d'exclusion. L'onglet correspondant à la source du lien est l'un de ceux-ci. Une source possède son propre onglet car des règles définissent les colonnes pouvant être liées et les éléments qui peuvent y être liés, entre autres. Lorsque vous créez une source de lien, le gestionnaire de liens crée un objet de ce type.
Tout un tas d'autres opérations sont effectuées pour vérifier des éléments et configurer des événements afin de détecter la fermeture des tables, etc. Vous pouvez ignorer tout cela. L'essentiel se trouve ici : la création d'un nouvel objet d'onglet source de lien et la fourniture de la table.
Voilà, c'est à peu près tout. L'élément est ajouté à l'interface graphique, je crois que c'est quelque part par ici. La liste des sources est là, et cet élément y est ajouté. Il s'agit d'un simple conteneur. Vous pouvez voir où se trouve la fonction qui récupère l'onglet ; c'est l'objet d'affichage. Ce système gère la réplication. À chaque nouvelle source, un nouvel élément est créé et toutes les fonctionnalités sont automatiquement mises à jour.
Si on accède à l'onglet Source, on y trouve également une initialisation. Il contient son propre objet d'affichage : une boîte de page. Des opérations d'alignement sont en cours, et on y trouve aussi un objet Liste. Ce dernier regroupe toutes les configurations de liens possibles. Encore une fois, n'oubliez pas que ce code n'est probablement pas optimisé, mais vous commencez à comprendre en quoi il est potentiellement plus simple de développer un comportement dynamique pour une interface dans JMP. Je l'ai utilisé à plusieurs reprises.
Certains d'entre vous auront peut-être remarqué la bibliothèque de rappel que j'ai mentionnée ici ; c'est celle dont je parlais il y a quelque temps, lorsque j'ai évoqué une implémentation bien plus complexe. Je n'ai malheureusement pas le temps de m'y attarder. J'aurais aimé, car il s'agit du tout premier projet que j'ai entrepris avec les classes JMP. Malheureusement, il est encore à l'état d'ébauche. De nombreux éléments pourraient être facilement simplifiés.
J'espère surtout vous avoir fait réfléchir à la manière dont les classes JMP peuvent être utilisées pour un comportement plus dynamique et un code plus propre. Car je sais que lorsque j'essayais de… j'ai vu du code d'interface utilisateur complexe et dynamique où tout était intégré directement dans la fonction d'ouverture de la nouvelle fenêtre.
Dès la nouvelle fenêtre, on se retrouve avec une vingtaine d'onglets remplis de fonctions et de boutons, chacun faisant ceci ou cela, alors qu'il existe des méthodes bien plus claires et orientées objet pour obtenir le même résultat. C'est plus facile à maintenir, plus évolutif et plus lisible. Je pense que travailler avec des classes présente de nombreux avantages pour la création d'interfaces utilisateur.
Je ne vais pas aborder la question du rappel. Je tiens simplement à préciser que je vais m'atteler à la tâche de nettoyer et de mettre à disposition tous ces exemples de code ainsi que la bibliothèque de rappel dans un dépôt Git public. Ainsi, toute personne intéressée pourra y jeter un œil, cloner le dépôt, le dupliquer, l'utiliser à sa guise et tenter de comprendre comment tirer parti des classes JMP dans son propre code afin d'en améliorer l'efficacité.
Il ne nous reste plus qu'à rappeler rapidement les points abordés concernant la définition du périmètre. Nous avons vu des exemples, vous avez compris comment structurer ces interfaces graphiques dynamiques, mais il s'agit maintenant de comprendre les difficultés et comment les surmonter. Un bon exemple serait… Voyons si nous pouvons le faire ici.
Si je choisis l'une de ces options, le problème de portée dont j'ai parlé précédemment concerne la manière dont vous appelez les méthodes et la portée dont elles héritent. J'ai constaté plusieurs problèmes lors de l'utilisation de l'espace de noms `Here`. J'ai observé des conflits entre l'espace de noms global et les méthodes. Pour simplifier au maximum, la solution infaillible que j'ai trouvée pour contourner tout problème de portée que vous pourriez rencontrer avec une classe (et je ne la démontre pas ici, mais c'est un bon exemple pour illustrer comment procéder) consiste simplement à utiliser un modificateur `local`. Je préfère le formater ainsi.
La seule variable ici qui nécessite une définition explicite est celle-ci. Il s'agit simplement d'une variable que nous initialisons pour analyser et créer la propriété qui générera la référence. Le reste du code est sans importance. Cette variable pourrait masquer une variable dans une autre portée, simplement parce qu'elle n'est pas définie comme locale, et selon l'endroit où cette méthode est appelée dans votre code, cette variable pourrait engendrer un conflit.
La méthode la plus propre que j'aie trouvée pour éviter les conflits de portée avec les classes consiste à déclarer explicitement les variables locales. Je suppose que je dois appliquer cette méthode à la quasi-totalité de ces méthodes, car la majeure partie du code a été écrite avant que je ne comprenne comment contourner ces problèmes. L'autre solution consiste à utiliser une identification explicite des espaces de noms.
Chaque fois que j'écris une bibliothèque, je l'écris essentiellement dans un nouvel espace de noms personnalisé, et j'essaie de définir toutes mes variables et méthodes, ou mes variables et fonctions, dans cet espace de noms également, ce qui m'a aidé à éviter les conflits.
Ensuite, il faudrait aussi aborder, même si cet exemple n'est pas idéal, certains problèmes liés au polymorphisme et à l'héritage. Voyons voir. La bibliothèque Recall serait un bon endroit pour en parler.
Espérons que ce ne soit pas trop compliqué… Nous pouvons éviter certains détails d'implémentation et nous concentrer uniquement sur les problèmes rencontrés. D'après ce que nous avons vu jusqu'à présent, ce panneau de rappel est essentiellement un conteneur permettant de rappeler automatiquement les valeurs des champs. Il est conçu pour être hérité, et donc pour avoir des classes enfants qui doivent appeler certains comportements. Plus précisément, il s'agit de cette méthode. La classe enfant devra implémenter sa propre version de la méthode `build`.
C'est là que la subtilité du polymorphisme entre en jeu. Si vous tentez d'appeler cette méthode depuis une autre méthode (par exemple, dans la classe parente, si vous souhaitez appeler `build` et qu'une classe enfant redéfinit cette méthode), `build` ne sera pas correctement délégué à la méthode enfant. Je n'ai pas d'explication quant à la raison de cette subtilité liée à l'implémentation des classes en interne. Cependant, il existe une solution de contournement. Je pense qu'une autre bibliothèque serait plus appropriée pour l'illustrer, mais je ne suis malheureusement pas en mesure de l'aborder ici.
Je pense que je vais éviter d'entrer dans les détails et me contenter de dire que le polymorphisme, ou le fait que l'on puisse implémenter une classe enfant qui redéfinit le comportement, peut ne pas fonctionner comme prévu, ce qui est un peu regrettable car je pense que l'héritage est l'un des aspects les plus utiles des classes.
Bien qu'il soit possible d'hériter de comportements, il est un peu plus complexe d'appeler ces comportements hérités dans la classe parente. Je pense que c'est ainsi que je le formulerai. Lorsque je publierai ces ressources en ligne, j'essaierai d'inclure des exemples plus précis expliquant pourquoi cela ne fonctionne pas. Encore une fois, mon objectif est simplement de donner un aperçu de la manière d'intégrer des classes à vos interfaces utilisateur en Junk JSL.
Ceci étant dit, je vais m'arrêter là pour ne pas trop m'éterniser. Je m'appelle Ben Barrett, et je vous invite à me contacter si vous avez des questions ou souhaitez voir d'autres exemples d'utilisation. Merci beaucoup.