Etudiant
Sébastien Schoepfer
Professeur
MM. Didier Mettler et François Birling
Mandants
MM. Christopher Bouzas et Antoine Jeanrichard - Objectis SA
Travail de Bachelor
Préambule
Ce travail de Bachelor est réalisé en vue de l'obtention du titre de Bachelor of
Sciences en Ingénierie.
Doyenne du
Centre Formation de Base
L. Larghi
_______________________________________________________________________________
HEIG-VD, av. Sport 20, 1401 Yverdon-les-Bains
Tél. 024/557 76 09 / 07 Fax : 024/557 76 01
Table des matières
1 Société mandante 6
2 Enoncé 7
2.1 Demande initiale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2 Cahier des charges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3 Démarche 8
3.1 Pré-étude . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.2 Réalisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4 Concept.Web 10
4.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
4.2 Navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
4.3 Page d’accueil . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
4.4 Visualiseur de synoptique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
4.5 Editeur de synoptique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5 Architecture globale 17
5.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
5.2 Organisation du projet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
6 Architecture du client 19
6.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
6.2 Fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
6.3 Application principale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
6.4 Page d’accueil . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
6.5 Objets métiers - Synoptique et composant . . . . . . . . . . . . . . . . . . . . . . . . 22
6.6 Objets métiers - Propriétés, variables et transformations . . . . . . . . . . . . . . . . . 31
6.7 Visualiseur de synoptique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6.8 Concepteur de synoptique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7 Architecture du serveur 50
7.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
7.2 Schéma et base de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
7.3 Services web REST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
9 Tests unitaires 59
9.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
9.2 Test du client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
9.3 Test du serveur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
11 Dossier de gestion 62
11.1 Planning des réunions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
11.2 Planning des tâches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
12 Conclusion 65
12.1 Concept . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
12.2 Choix technologiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
12.3 Expérience personnelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
13 Remerciements 67
Bibliographie 71
Authentification 72
Objectis SA est une société essentiellement active dans le milieu de l’industrie. Basée à Y-
parc (avenue des Découvertes 18, 1400 Yverdon-les-Bains), elle a été fondée par d’anciens
étudiants de la HEIG-VD. Actuellement composée d’une vingtaine d’ingénieurs, elle est admi-
nistrée par MM. François Birling et Christopher Bouzas.
Comme décrit sur le site web de la société[15]. Elle a orienté ses compétences dans les trois
domaines suivants :
Machines et Automatisation
Création de logiciels et frameworks performants et modulaires pour l’automatisation, le
contrôle de machine et la supervision de production.
Business Applications
Développement d’applications métiers avec interfaces modernes et ergonomiques pour
des applications professionnelles sur PC, web, smartphones et tablettes.
Instruments et Embarqué
Applications et frameworks flexibles et performants pour instruments de mesure et systèmes
embarqués temps-réel.
Ce sujet a été traité en détail lors de la pré-étude, il s’agit ici de se remémorer, ce qui a été
défini lors de cette première étape.
L’énoncé, tel qu’il a été proposé initialement par la société Objectis, est le suivant :
“ Les logiciels d’interface homme machine de supervision utilisés dans les systèmes automa-
tisés, appelés SCADA, pourraient tirer un très grand bénéfice des nouvelles technologies HTML5
pour offrir un affichage dynamique avec un visuel attractif et une architecture Web aisément
exploitable depuis tout type de périphérique, y compris tablettes mobiles.
Le but de ce projet est de développer une base de logiciel SCADA en HTML 5, permettant
de superviser une application tournant sur un PC connectés aux systèmes de production (PLC,
CNC). La liaison avec le processus physique sera entre autres réalisée par le protocole Woopsa.
Au stade actuel, un concepteur visuel est déjà disponible, il s’agira donc de développer un
module Runtime remplissant la fonction de serveur Web.
Le sujet étant très vaste, une des principaux éléments de la pré-étude a été de déterminer
l’étendue du projet et d’établir un cahier des charges précis. Résumons ici les axes principaux
déterminé lors de la pré-étude :
D’autres éléments ont également été évoqués lors de la pré-étude. Cette dernière avait permis
de les écarter, car ils n’étaient pas prioritaires et n’entraient pas dans le planning (ex : édition
de composant, édition de page et gestion des utilisateurs).
Une liste des exigences a été rédigée lors de la définition du cahier des charges. Nous revien-
drons sur celle-ci à la fin du document afin de s’assurer que nous les avons bien toutes respec-
tées.
3.1 Pré-étude
La pré-étude a permis d’établir un cahier des charges détaillés. En partant de cette base,
trois étapes ont pu être réalisées :
a. Choix technologiques
Lors de ce processus, un panel de technologies a été défini et évalué en fonction de
l’objectif à atteindre. Le choix final s’est rapidement imposé de lui-même. Il a ensuite fallu
s’assurer que toutes les fonctionnalités pouvaient être réalisé avec les technologies rete-
nues.
Divers prototypes de parties-clés de l’application ont été créés afin de s’assurer de la fai-
sabilité du produit.
Cette étape importante dans la réalisation d’un projet informatique avait permis de définir
le choix technologique suivant :
Il convient de rappeler que le langage Typescript a été utilisé pour créer l’ensemble du
produit.
b. Architecture
Lors de cette étape, un diagramme de classe a été défini. Ce diagramme a été utilisé tout
au long de la réalisation du projet. Il a beaucoup évolué au fur et à mesure du dévelop-
pement. Cependant, les grandes lignes spécifiées dans la pré-étude ont été respectées.
c. Planification
Sur la base du diagramme de classe, le projet a été divisé en tâches qui ont été évaluées
individuellement. Ainsi, les parties du produit pouvant être réalisées ont pu être détermi-
nées.
3.2.1 Semaine 1 à 8
Aucune phase d’analyse n’a été nécessaire au début du projet. étant donné que cette
partie avait été effectuée lors de la pré-étude. Le projet a pu commencer directement par le
développement de l’application à proprement parler.
Globalement, les tâches ont été effectuées en suivant l’ordre défini dans le planning initial.
Dans certain cas, il a tout de même été nécessaire d’en modifier la chronologie pour une plus
grande efficacité.
Certains éléments-clés ont également nécessité de développer des tests unitaires. Ceux-ci ont
été réalisés directement dans le cadre de la tâche.
3.2.2 Semaine 9 à 12
A partir du 3ème tiers, la réalisation de ce dossier a débuté, tout en finalisant la fin du déve-
loppement. Il était important d’atteindre un certain niveau de maturité avant de commencer
à rédiger le mémoire. De cette manière, il était plus évident d’avoir une vue d’ensemble du
projet et ainsi sélectionner le contenu plus efficacement.
4.1 Généralités
Dans ce chapitre, nous allons présenter le produit tel que livré avec ce dossier. Nous allons
passer en revue les fonctionnalités.
A la demande de la société mandante, toute l’application a été développée en anglais. De
ce fait, toutes les captures d’écran et images sont réalisées dans cette langue.
La procédure complète pour installer et démarrer l’application se trouve dans le fichier README.md
situé à la racine du projet. Ce dernier étant livré sur le CD-ROM annexé à ce document.
4.2 Navigation
La barre supérieure est disponible sur toutes les pages. Elle permet de naviguer à travers
l’application, ceci grâce aux trois boutons situés sur la droite :
— Welcome
— Viewer
— Designer
Elle contient une liste des synoptiques existantes. Les icônes sur la droite de la liste per-
mettent de :
— Visualiser un synoptique (cf 4.4)
— Editer un synoptique (cf 4.5)
— Supprimer un synoptique
Cette partie permet d’afficher un synoptique. La liste sur la gauche permet de naviguer
d’une vue à l’autre. Le synoptique s’affichent sur la droite (comme vous pouvez le constater
sur l’image 4.3)
Cette page permet à l’utilisateur de créer de nouvelles vues synoptiques mais également
de modifier les existantes. Elle est certainement la partie la plus intéressante de l’application.
Nous détaillerons dans une prochaine section chaque élément de cette page. Voici un aperçu
global :
La partie centrale contient le synoptique en cours d’édition. Chaque composant peut être
sélectionné et déplacé au moyen de la souris dans le synoptique. Il est également possible
de supprimer les composants en pressant sur la touche Effacer (Delete) ou Retour en arrière
(Backspace) du clavier.
Lorsqu’un composant est sélectionné, la liste des propriétés disponibles pour ce composant
s’affiche dans l’éditeur de propriétés sur la droite de l’écran.
Figure 4.9 – Différence entre propriétés en lecture (à gauche) et propriétés en écriture (à droite)
Chacun des boutons ci-dessus permet d’accéder aux éditeurs que nous décrirons dans les
sections suivantes.
Les propriétés en lecture peuvent être de l’un des types suivants :
— Texte
— Couleur
— Numérique
— Vrai/Faux
Ces propriétés interagissent directement avec des éléments de composant. Par exemple, don-
ner une valeur à une propriété de type couleur peux changer le fond d’un composant. Une
propriété de type vrai/faux peut donner la possibilité à l’utilisateur de choisir d’afficher ou non
un élément de composant. L’effet du type de propriété dépend entièrement de chaque com-
posant.
Editeur de constante
Pour accéder à l’éditeur de lien vers une variable Woopsa, il faut cliquer sur la petite prise
située à côté d’une propriété. Comme son nom l’indique, cet outil permettra de lier une va-
riable à un composant. Il sera par exemple possible d’afficher la valeur d’un thermomètre en
temps réel, mais également de modifier sa couleur en fonction de cette valeur.
Il s’agit donc de faire correspondre une valeur provenant d’un serveur Woopsa avec une pro-
priété comme celle décrite précédemment. Dans certains cas, il est possible de reprendre la
valeur telle quelle et de l’afficher dans le composant (affichage de la température). Dans
d’autres, il faut appliquer une transformation à la valeur retournée par Woopsa pour l’utiliser
dans le composant (changement de la couleur en fonction de la température).
Regardons l’éditeur de lien sur la figure 4.14. A gauche la liste des variables disponibles sur le
serveur Woopsa. Lorsque l’on sélectionne une variable, la liste des transformations disponibles
s’affiche sur la droite :
Cette liste se construit automatiquement en fonction de la variable que l’on veut observer et
de la propriété de destination (celle en cours d’édition). Voici un tableau qui récapitule la liste
des transformations disponibles :
Dans certains cas, il est possible de définir plusieurs transformations pour la même propriété. Il
s’agit de pouvoir changer la valeur de la propriété en fonction de la valeur de la variable. Par
Cet éditeur est disponible grâce à l’icône représentant une flèche pointant vers la droite
depuis la boîte à outil des propriétés. Il permet de sélectionner une variable Woopsa ainsi
qu’une valeur. Depuis le visualisateur de synoptique, lorsque l’utilisateur clique sur une certaine
partie du composant, ce dernier modifie la variable sélectionnée avec la valeur définie. Cela
permet de modifier l’état du serveur.
5.1 Généralités
Dans ce chapitre et les trois suivants, nous allons présenter l’implémentation. L’accent sera
mis sur les éléments-clés du système. Les schémas de classe que l’on trouvera dans ce chapitre
correspondent à l’implémentation du code. Ils sont donc légèrement différents de ceux définis
lors de la pré-étude. Il est important de préciser que les schémas ont volontairement été épurés
afin de mieux cibler la problématique traitée. Il sera ainsi possible qu’une classe apparaisse sans
lien dans une section et que dans la suivante, elle se trouve munie d’un nouvel attribut qui la
lie à une autre entité.
Rappelons également que le code couleur utilisé pour les classes reste identique à celui de la
pré-étude. A noter qu’une couleur plus claire a été utilisée pour symboliser les classes abstraites
ou les interfaces.
Nous ne rentrerons pas dans le détail du fonctionnement du framework Angular. Le web re-
gorge d’articles de qualité à ce sujet. Cependant, quelques éléments seront tout de même
expliqués afin que le lecteur comprenne les raisons de certaines décisions prises.
L’architecture de l’application a été définie lors de la pré-étude. Le schéma suivant rappelle
ce qui a été déterminé.
Nous allons dans cette section expliquer l’organisation au sein du projet. La figure suivante
présente le contenu de son dossier racine.
2. Une API compatible REST, ou « RESTful », est une interface de programmation d’application qui fait appel à
des requêtes HTTP pour obtenir (GET), placer (PUT), publier (POST) et supprimer (DELETE) des données. - http ://www.
lemagit.fr/definition/API-RESTful.
3. Outil d’automatisation de tâches, https://gulpjs.com/
6.1 Généralités
Dans ce chapitre, nous allons traiter la partie client de l’application. Nous présenterons la
partie serveur dans le chapitre suivant. Il convient de rappeler que ces deux éléments commu-
niquent grâce au protocole HTTP et à l’API REST. Les données transitent au format JSON.
Nous allons régulièrement parler de composant. Il faut faire la distinction entre les composants
de l’application qui appartiennent à notre modèle objet et qui composent les synoptiques,
et les composants Angular qui sont omniprésents dans tout le frameworks. Ces derniers per-
mettent d’afficher des informations. D’ailleurs les composants de l’application sont eux-mêmes
des composants Angular.
Chaque composant Angular est formé de trois éléments : une classe Typescript, un tem-
plate HTML et des styles CSS. Il est important de noter qu’un composant peut-être com-
posé de plusieurs autres composants. Ce lien de composition n’est pas forcément visible
dans le code Typescript. Il peut-être matérialise par l’usage de la balise qui représente
le composant en question. Dans les schémas de classes aucune distinction ne sera faite
entre ces deux types de lien, car ils ont exactement la même signification.
Convention de codage Les conventions de codage d’Angular ont été suivies 1 . Cela signifie
que les noms de fichier s’écrivent en minuscule. Chaque mot est séparé par un tiret (”-”), le nom
est terminé par point (”.”) et suivi par l’extension du fichier. Si le fichier a une fonction spécifique
reconnue par Angular (composant, service, module, ...), le nom de la fonction précédé par un
point (”.”) est placé juste avant l’extension.
Pour nommer les classes, la convention fixée par l’équipe Angular est d’utiliser la notation Camel
Case, à savoir tous les mots de la classe sont joints et commencent par une majuscule. Pour
ce qui concerne les noms de méthodes et de propriétés, nous appliquons la même notation
excepté la première lettre qui commencera par une minuscule.
Diagramme de classes Tous les diagrammes de classes ont été conçus pour être les plus com-
préhensibles possible. De ce fait, certains éléments propres au framework Angular qui n’était
pas absolument nécessaires à la bonne compréhension du système ont volontairement été
exclus du schéma de classes.
1. https://angular.io/guide/styleguide#general-naming-guidelines
Nous traiterons ici de l’application principale ainsi que de son point d’entrée. Angular im-
pose une structure de base. La classe AppModule est considérée comme le conteneur de toute
l’application. Elle en connait toutes les classes. L’usage imposé du décorateur NgModule per-
met d’indiquer au framework quelles classes sont des services (Singleton Angular) et lesquelles
des composants.
App Component est le point d’entrée graphique de l’application. C’est d’une certaine ma-
nière notre ”Index.html”.
Comme constaté sur la figure 6.1, la classe TopMenuComponent est liée à AppComponent par
une relation de composition. Tandis que les trois autres pages le sont par une simple relation.
Voyons le code HTML d’AppComponent :
<ofact-top-menu></ofact-top-menu>
<router-outlet>
Listing 6.1 – HTML de AppComponent
@NgModule({
// ....
imports: [
RouterModule.forRoot(routes),
]
// ....
})
export class AppModule {}
Listing 6.2 – Déclaration des routes dans AppModule
Cette page est simplement composée de la liste des synoptiques existants. Pour afficher cette
liste, nous avons créé un composant SynopticListComponent. Ce dernier est utilisé à plusieurs
endroits et permet d’effectuer diverses opérations. En fonction du listType qu’on lui attribue, il
affiche à côté de chaque synoptique différents boutons permettant des opérations comme :
accéder à un synoptique en mode lecture (visualisateur de synoptique), accéder à un synop-
tique en mode écriture (créateur de synoptique), ou plus simplement supprimer un synoptique.
Il aurait été plus judicieux de donner à l’attribut listType un type énuméré. Cependant, il n’est
pas possible d’utiliser ce genre d’artefact dans les templates HTML. Pour cette raison, le type
string a été utilisé. Voilà un exemple d’appel à ce composant dans un template :
<ofact-synoptic-list [listType]="'all'"></ofact-synoptic-list>
Listing 6.4 – Appel de SynopticListComponent
Remarque : Toutes les balises de composant propre à l’application sont préfixées avec le terme
ofact. Ce terme provient du nom initial du produit qui était oFactory.
Il est également possible d’écouter cette liste afin d’être notifié lorsque l’utilisateur sélectionne
un élément de la liste. L’attribut onSelectItemEvent a été créé à cet effet.
La classe SynopticDataService de la figure 6.2 est le service d’accès aux données des synop-
tiques. Les services d’accès aux données seront détaillés dans une prochaine section.
Avant d’aller plus loin dans le détail des pages de l’application et dans les éléments qui les
composent, il est nécessaire d’aborder le cœur de l’application. Il est important de comprendre
le processus de création d’un synoptique.
Le serveur est composé de deux collections : une collection de synoptiques et une de définition
de composants. Cette dernière regroupe toutes les définitions de composants utilisateurs. Lors
de la pré-étude, nous avons mis en évidence deux types de composants. Les composants par
défaut qui sont compilés avec l’application, et les composants utilisateurs qui proviennent de
la base de données et qui pourraient être édités à l’aide d’un outil prévu à cet effet.
La collection de synoptiques, contient toutes les informations des synoptiques. Chaque synop-
tique étant lui-même constitué de composants qui se trouvent être des instances de compo-
sants utilisateurs ou de composants par défaut.
Dans la figure 6.4, le terme Factory désigne plusieurs classes qui permettent d’instancier un
synoptique provenant du serveur. Le principe est simple : en fonction du type de chaque com-
posant, nous définissons s’il faut aller chercher dans les définitions de composants utilisateurs ou
dans la liste des classes de composants par défaut. Ensuite nous pouvons créer des instances
des composants et leur attribué les données qui figurent dans le synoptique. Nous reviendrons
sur ce processus plus tard dans le chapitre.
Regardons de plus près l’implémentation d’un synoptique et d’un composant :
La classe SynopticComponent est suffixée par le terme Component, car il s’agit d’un compo-
sant Angular. Elle est donc directement affichée dans le navigateur. Les diverses propriétés des
classes permettent de dimensionner les éléments et de les placer à l’écran.
Reprenons la figure 6.5 et expliquons la raison d’être des interfaces ISynoptic et ICom-
ponent. Nous avons choisi d’extraire tout ce qui doit persister en base de données dans des
interfaces. Celles-ci sont sauvegardées dans le dossier Common(voir figure 5.3) qui se situe à la
racine du projet. De cette manière, le client et le serveur utilise le même code. Cela présente
les avantages suivants :
— Réutilisation du code,
— Lorsque l’interface change, le client et le serveur seront obligés d’être corrigés pour l’im-
plémenter correctement (sinon cela entrainera erreur de compilation).
— Il est très facile de désérializer du JSON dans une interface.
Les interfaces sont des outils puissants fournis par Typescript. Cependant, JavaScript ne les sup-
portent pas nativement. De ce fait, leur usage est utilisé uniquement pour la phase de déve-
loppement (par les outils de développement pour fournir une complétion de code 2 ) et pour la
phase de transpilation (transformation du code Typescript en JavaScript).
Ce manque de support natif implique qu’il n’est pas réellement possible de transtyper une
classe implémentant une interface dans le type de l’interface. Si nous effectuons cette opéra-
tion, la compilation ne retournera pas d’erreur.
Cependant, à l’exécution, l’objet transtypé contiendra tout ce qu’il contenait à sa création et
pas seulement ce qui est décrit par l’interface. Cela posera problème lorsque nous voudrons
extraire le JSON correspondant à l’interface pour l’envoyer au serveur à des fins de persistance.
Tous les attributs n’appartenant pas à l’interface seront également sérialisés. Voici un exemple
illustrant ce point :
interface Ia{
valueFromA : string;
}
2. Capacité d’un outil à proposer des identifiants en inspectant le code source existant.
this.components.forEach((comp) => {
synoptic.components.push(comp.toJson());
});
return synoptic;
}
Listing 6.6 – Sérialisation d’un synoptique
L’API REST fournie par le serveur permet d’obtenir une représentation JSON de synoptique.
Il s’agit d’un objet JSON qui correspond à l’interface ISynoptic. Voici un exemple de cette re-
présentation :
"_id" : ObjectId("59bd0b3af9dbef1af10ff24b"),
"name" : "Synoptique de démonstration",
"width" : 1000,
"height" : 900,
"components" : [{}]
Listing 6.7 – JSON d’un synoptique vide
Il suffit de transtyper l’objet JSON avec l’interface ISnoptic pour manipuler directement les pro-
priétés du synoptique. Il est clair que l’on ne peut pas créer une instance de la classe Synoptic-
Component de cette manière. Cependant, en faisant ainsi nous profiterons d’un typage fort
sur les variables de types ISynoptic. Cela évitera de nombreuse erreur qui se verront lors de la
transpilation. De plus, nous tirerons parti de toute la puissance des compléteurs de code offerts
par les environnements de développement.
La propriété ”_id” du listing précédent correspond à l’identifiant unique du synoptique. Cette
information est gérée par la base de données.
Si vous n’êtes pas familier avec le framework Angular, vous vous demandez sûrement
pour quelles raisons toutes les classes du diagramme précédent sont suffixées par le terme
”Service”. Il s’agit d’une convention de codage du framework. Angular utilise l’injection
de dépendance pour fonctionner. En décorant une classe avec l’attribut @Injectable()
et en la déclarant au bon endroit dans AppModule, il est possible d’injecter une classe
dans d’autres classes de l’application. Le framework ne crée qu’une seule instance de
ces classes. Toutes celles qui se terminent par ”Service” dans le schéma précédent ont
été déclarées de cette manière. Nous délèguons ainsi la gestion de l’instance unique de
la classe au framework.
Le diagramme de séquences suivant représente les interactions entre les éléments de la figure
6.6. Le processus a été réparti entre plusieurs classes pour être le plus modulaire possible. Ceci
a également pour avantage de rendre l’application plus aisée à tester. Il est également plus
simple de la maintenir car chaque classe a une mission bien précise.
Nous pouvons voir ici les différentes propriétés que nous retrouverons dans l’interface ICom-
ponent. La propriété type est un type énuméré (ComponentType) qui peut prendre une des
deux valeurs suivantes : 0 = Composant utilisateur, 1 = Composant par défaut. Elle sera utilisée
par la classe ComponentFactoryService pour savoir comment construire le composant.
Lors de l’instanciation d’un synoptique, SynopticFactoryService ajoutera tous les composants
au synoptique en appelant la méthode AddComponent de SynopticComponent avec en pa-
ramètre l’instance de l’interface IComponent correspondant au composant à ajouter.
Lorsqu’il faut instancier un composant utilisateur, il est nécessaire d’aller chercher la définition
de ce composant dans la base de données. Cette opération est réalisée grâce à la classe
UserComponentDataService. Cette classe est chargée d’interroger l’API REST pour obtenir le
JSON représentant la définition d’un composant utilisateur. Comme pour le synoptique, une
interface partagée entre le client et le serveur a été créé pour représenter cet élément. Cette
interface n’est implémentée par aucune classe. Sa seule utilité est de fixer un contrat entre le
JSON attendu par le client et celui fourni par le serveur.
L’identifiant ne figure pas dans l’interface car il n’est pas utilisé. Nous utilisons la propriété name
pour identifier une définition de composant. C’est d’ailleurs cette valeur qui fait le lien entre
Abordons la phase de compilation de composant utilisateur. Il s’agit d’une étape clé dans
cette application. C’est grâce à ce mécanisme qu’il est possible de créer des composants
dynamiquement à partir du contenu de la base de données.
Angular est nativement écrit en Typescript. Cependant ce langage n’est pas supporté
par les navigateurs. Pour cette raison, il est nécessaire de transpiler le code Typescript
vers du code JavaScript compatible. L’outil Angular-cli fourni par Google intègre cette
étape de transpilation dans le processus de construction de l’application.
Lors de l’exécution d’une application Angular, chaque composant doit être compilé.
Attention, on ne parle pas ici de la transpilation abordée précédemment. Il s’agit là d’une
interprétation du template HTML lié à chaque composant. La documentation Angular
utilise le terme de compilation pour parler de ce processus.
Impact sur ce projet Nous compilons du template HTML à la volée, il est donc absolu-
ment nécessaire d’embarquer le compilateur de composants au sein de l’application.
Cependant, il serait intéressant de pouvoir utiliser la compilation AOT pour le projet et
d’embarquer le compilateur JIT et ne l’utiliser que pour les composants utilisateurs. Ceci
n’a pas pu être réalisé, car l’outil Angular-cli ne permet pas de le faire nativement. Au
vu des divers recherches effectuées sur le net, il semble possible d’utiliser ce mélange de
compilations. Cependant les exemples trouvés sont très compliqués. Il est fort probable
que les futurs versions d’Angular-cli permettront ceci.
Le listing qui suit permet de créer la classe représentant le composant utilisateur. Nous déclarons
la classe au sein d’une méthode, il faut également la décorer comme nous le faisons pour tout
autre composant Angular. Le type est ensuite retourné.
private createNewComponent(css: string[], tmpl: string): Type<UserComponent> {
@Component({
selector: 'ofact-user-component',
styles: css,
template: tmpl,
})
class CustomDynamicComponent extends UserComponent {
constructor(eRef: ElementRef) {
super(eRef);
}
}
return CustomDynamicComponent;
}
Listing 6.10 – Création du type du composant utilisateur
Dans la portion de code ci-dessous nous créons un module comme on l’a fait précédemment
pour le composant. Ce module ne contient qu’un seul composant, à savoir le composant utili-
sateur que nous désirons compiler et qui a été créé dans le listing 6.10.
private createComponentModule(componentType: Type<UserComponent>): any {
@NgModule({
imports: [],
declarations: [componentType]
})
class RuntimeComponentModule {}
return RuntimeComponentModule;
}
Listing 6.11 – Création d’un module
}).catch(error => {
console.log(error);
});
});
Listing 6.12 – Compilation d’un composant utilisateur
Bien que relativement compliqué, ce procédé a un avantage non négligeable : il permet d’uti-
liser toute la puissance du système de modèle (templating) propre à Angular. Il est ainsi possible
de faire appel à des méthodes Typescript directement depuis le code HTML, mais également
d’effectuer des calculs ou utiliser le data-binding 4 . Chaque composant compilé hérite de la
classe UserComponent nous pouvons donc utiliser toutes les méthodes implémentées par cette
classe directement dans le template.
Vous l’aurez remarqué, Angular utilise beaucoup le terme Factory dans ses identifiants
lorsqu’il s’agit de création dynamique de composant. Dès que nous voulons dynami-
quement ajouter un composant à un autre, il faut obtenir la factory de ce composant
pour l’instancier de manière dynamique. Ceci est rendu possible grâce au service Com-
ponentFactoryResolver du framework. Ce dernier permet d’obtenir la factory d’un com-
posant en fonction d’un type (pour autant qu’il n’ait pas été compilé dynamiquement
comme nous l’avons précédemment fait). Il ne faut pas le confondre avec les différentes
classes factory qui ont été implémentées par nos soins. Elles ont des missions similaires mais
ne font pas partie d’Angular.
La notion de ”propriété” est un concept clé de ce système. Grâce aux propriétés, il est
possible d’interagir sur certains attributs du composant. Nous pouvons par exemple définir une
chaine de caractères, ou la couleur d’un trait ou choisir d’afficher ou non un composant.
Chaque composant implémente ses propres propriétés, il n’y a pas de propriété que l’on re-
trouve sur tous les composants. Il revient au créateur du composant de choisir ce qu’il souhaite
afficher ou non. Les attributs de width, height, left et top de l’interface IComponent (implé-
mentée par la classe BaseComponent) ne sont pas des propriétés comme décrites dans ce
chapitre.
3. Tout ce processus a été repris de l’article suivant https://stackoverflow.com/questions/38888008/how-
can-i-use-create-dynamic-template-to-compile-dynamic-component-with-angular
4. Technique permettant d’afficher le contenu de variables à l’utilisateur et de les modifier
Il convient de préciser que tout comme pour les synoptiques et les composants, l’usage des
interfaces à des fins de persistance a également été appliqué. Voici un exemple de JSON
représentant une définition de propriété au sein d’une définition de composant :
// ... Attributs de ComponentDefinition ...
"properties" : [
{
"type" : 0,
"name" : "Text",
"defaultValue" : "Saisissez du texte ici ..."
},
// ...
]
Listing 6.13 – JSON d’une définition de propriété
L’instance de la propriété permet de lui donner la valeur qui lui est attribuée au sein du synop-
tique. Dans l’interface IProperty l’attribut definition permet de faire le lien avec une définition de
propriété du composant. Il est ainsi possible de déterminer quelle instance de propriété rempli
quelle définition.
Le listing suivant est un exemple du JSON d’une propriété au sein d’un composant (lui-même
situé dans un synoptique).
// ... Attributs du Synoptique ...
"components" : [
{
// ... Attributs du Composant ...
"properties" : [
{
"variable" : null,
"value" : "J'ai saisi un texte !",
"definition" : "Text",
},
]
}
Listing 6.14 – JSON d’une propriété
En comparant les listing 6.13 et 6.14, nous constantons que l’attribut Name de la définition
de propriété correspond à l’attribut Definition de la propriété. Ces deux éléments permettent
d’établir le lien entre une propriété et sa définition.
Les propriétés peuvent être divisées en deux groupes : les propriétés en lecture et les propriétés
en écriture.
Propriété en lecture
Toutes ces propriétés héritent de BaseReadProperty. Leur but est de définir une valeur au sein
d’un composant. Elles peuvent contenir une valeur constante ou un lien sur une variable Woopsa.
Elles sont nommées propriétés en lecture car elle permettre d’utiliser (en lecture) la valeur d’une
variable Woopsa. Nous avons actuellement implémenté quatre types de propriétés :
Une propriété en lecture peut avoir soit une valeur constante soit être liée à une variable. Il n’est
pas possible qu’elle dispose des deux éléments simultanément.
La valeur de la propriété est implémentée par la classe PropertyBase. Comme défini dans l’in-
terface IProperty, le champ value est de type any. Cela signifie que nous pouvons mettre n’im-
porte quel type de valeur dans cet attribut. Les quatre classes concrètes décrites juste au-dessus
imposent l’usage d’un constructeur obligeant l’utilisateur à passer une valeur du type requis par
la propriété. Le type any facilite l’usage de la valeur de la propriété dans le template HTML des
composants. Nous verrons un peu plus loin comment la valeur d’une propriété est remontée
dans le template du composant.
Propriété en écriture
Il existe qu’une seule propriété en écriture. Il s’agit de la classe WriteProperty. Elle a pour objectif
de modifier la valeur d’une variable Woopsa. Contrairement aux propriétés en lecture, elle est
obligée de posséder une valeur constante et d’être liée à une variable. La première étant la
valeur à donner à la seconde lorsqu’une action est effectuée.
Ce type de propriété a été conçu pour répondre à une action de l’utilisateur. Elle fonctionne
avec l’action onClick. Il est possible de l’associer au ”clique” sur un élément de composant (par
exemple un bouton). Ainsi lorsque l’action se produira, la propriété ira modifier la valeur sur le
serveur Woopsa.
Cette propriété n’était initialement pas prévue dans le planning. Aucune exigence fixée lors
de la pré-étude ne demandait ce développement. Cependant, sur l’impulsion des conseillers
et avec le consentement du mandant ce développement a pu été intégré au projet.
Nous allons traiter des variables et de leurs liens avec les propriétés. Au sein de ce projet, les
variables désignent des valeurs provenant du protocole Woopsa. Comme vous l’aurez compris,
il est possible de s’abonner à une variable pour être notifié de ses changements de valeur.
if (this.variable != null) {
// Getting the value of the variable
this.woopsaClient.read(this.variable.propertyPath).then((x) => {this.
variableValueChanged(x); });
// To be notify of changes
this.woopsaClient.onChange(this.variable.propertyPath, this.variableValueChanged,
this.variableValueChangedRegistered);
}
}
Listing 6.15 – BaseReadProperty - afterVariableSet
Dans un premier temps, nous allons lire le contenu de la variable pour pouvoir donner une
valeur à la propriété. Ensuite nous nous abonnons à la variable. A chaque fois que le serveur
notifiera le client Woopsa d’un changement, la méthode variableValueChanged est exécutée.
Elle s’occupe de mettre à jour la propriété avec la nouvelle valeur de la variable.
Dans certain cas, une valeur récupérée depuis une variable nécessite d’être transformée
avant d’être utilisée au sein d’un composant. Il est par exemple intéressant de faire varier une
couleur en fonction d’une variable numérique. Pour cette raison, un système de transformation
a été mis en place, selon le schéma suivant :
if (booleanValue) {
return this.trueColor;
} else {
return this.falseColor;
}
}
Listing 6.16 – TransformationBooleanToColor - transform
Les transformations qui permettent de passer d’une valeur numérique vers d’autres valeurs
(couleur, booléen ou texte) dérivent du type BaseTransformationNumber. Dans ce cas, il est
possible, voir nécessaire, d’ajouter plusieurs transformations du même type. De cette manière,
nous pouvons couvrir une plage illimitée de valeurs numériques. Prenons un exemple : Ima-
ginons une variable qui représente la température d’un élément. On souhaite changer une
couleur en fonction de cette température. Admettons que nous souhaitons afficher du bleu
si nous sommes en dessous de 0, du vert entre 0 et 20 et du rouge au-delà. Voilà ce à quoi
ressemblerait la liste des transformations :
Pour effectuer la transformation, la valeur de la variable se place à gauche du symbole (<, <=,
>, >=, =).
L’ordre dans lequel apparait ces transformations est très important. L’ordre d’affichage permet
de définir la valeur de l’attribut order des transformations. La liste des propriétés est toujours triée
de manière croissante selon cet attribut. Voyons de plus près ce qu’il se passe dans la méthode
variableValueChanged de la classe BaseReadProperty.
Nous comprenons pourquoi l’ordre d’application des transformations a une grande impor-
tance. La première transformation qui retourne un résultat est appliquée, les autres sont igno-
rées.
switch (propDef.type) {
case PropertyType.TextProperty:
return new TextProperty(propDef.name, propDef.defaultValue, this.woopsaClient)
;
case PropertyType.BooleanProperty:
return new BooleanProperty(propDef.name, propDef.defaultValue, this.
woopsaClient);
case PropertyType.ColorProperty :
return new ColorProperty(propDef.name, propDef.defaultValue, this.woopsaClient
);
case PropertyType.NumberProperty :
return new NumberProperty(propDef.name, propDef.defaultValue, this.
woopsaClient);
case PropertyType.WriteProperty :
return new WriteProperty(propDef.name, this.woopsaClient);
default:
throw new TypeError('Unknown property type exception !');
}
}
Listing 6.17 – PropertyFactoryService - createProperty
Nous avons expliqué comment les propriétés sont instanciées, nous allons détailler comment
leurs sont attribués des valeurs, des variables et des transformations.
Ce diagramme de séquence correspond à la suite du diagramme 6.19. Une fois que le com-
posant est créé avec ses propriétés, il est nécessaire d’affecter à chacune d’elles la valeur, la
variable et les transformations qui lui est attribuée au sein du synoptique.
Etant donné que le cœur de l’application a été détaillé, nous allons aborder le visualisateur
de synoptique.
Cette page est composée de deux éléments principaux : La liste de synoptique et le synoptique
affiché.
Le synoptique à afficher est passé en paramètre dans l’URL de l’application. La dernière partie
de cette adresse représente le synoptique à afficher.
La méthode ngOnInit est fournie par le framework et plus particulièrement par l’interface
OnInit. Il suffit à un composant d’implémenter cette interface. Ainsi lorsqu’un composant
est initialisé (construit et affiché), ladite méthode est exécutée.
Cette classe fait le lien entre les trois éléments cités précédemment. Elle permet notamment
de créer, d’ouvrir et de sauvegarder un synoptique.
Le chargement d’un synoptique utilise le même principe que le visualiseur. L’identifiant de la vue
à charger est passé en paramètre de l’url. La création d’un nouveau synoptique, fonctionne
de la manière suivante :
if (result != null && result != '') {
const newSynoptic: ISynoptic = {
name: (result as string),
width: 1000,
height: 900,
components: null
};
this.synopticPlaceHolder.clear();
this.currentSynoptic = <SynopticDesignedComponent>this.synopticFactory.
loadSynopticFromData(
this.synopticPlaceHolder, newSynoptic,true).instance;
}
Listing 6.20 – Création d’un nouveau synoptique
Une instance d’un synoptique vide est créée. Elle est ensuite passée à la méthode loadSynop-
ticFromData de la classe SynopticFactoryService. Comme nous l’avons vu dans les chapitres
précédents, cette classe va se charger de créer l’instance du synoptique. Attention, dans ce
contexte, nous utilisons le paramètre designedMode pour demander à la factory de créer
une instance SynopticDesignedComponent plutôt qu’une instance de SynopticComponent.
La classe SynopticDesignedComponent dérive de SynopticComponent, elle ajoute simplement
tout une série de fonction permettant l’édition du synoptique.
Pour la sauvegarde, nous faisons appel à la méthode saveSynoptic de la classe SynopticData-
Service. Nous lui passons en paramètre la représentation JSON du synoptique en cours d’édition
obtenu grâce à la méthode toJson de la SynopticComponent.
Cet outil est piloté par la classe SynopticDesignedComponent. Lorsque l’utilisateur clique sur le
synoptique (événement onMouseDown), il va détecter si cette opération est effectuée sur un
composant. Si c’est le cas, nous conservons la référence du composant dans l’attribut current-
SelectedComponent de la classe SynopticDesignedComponent. Voici comment cette opéra-
tion est effectuée :
for (const comp of this.components) {
comp.isSelected = comp.eRef.nativeElement.contains(event.target);
if (comp.isSelected) {
this.currentSelectedComponent = comp;
}
}
Listing 6.21 – Sélection d’un composant
La liste de composants est parcourue et ainsi, le composant visé par l’événement onMouse-
Down sera sélectionné, grâce à l’instruction {comp.eRef.nativeElement.contains(event.target);}.
Cette instruction détecte si l’événement a eu lieu au sein du DOM 6 lié au composant.
Cette étape va également entrainer le déclenchement de l’événement componentSelect.
Cela permettra à la page SynopticDesignedComponent de savoir quel composant est sélec-
tionné.
L’utilisateur peut déplacer un composant grâce à une opération de cliquer-déplacer. Les évé-
nements onMouseDown et onMouseUp de la classe SynopticDesignerPageComponent vont
modifier l’état de sa propriété isDragging. Le premier la mettra à vrai alors que le second à
faux. Lorsqu’elle est vraie, l’événement onMouseMove va automatiquement modifier la posi-
tion du composant sélectionné en fonction du mouvement de la souris. Lors du relâchement
du clique (onMouseUp), le composant n’est plus déplacé par le mouvement de la souris.
Sélecteur de composant
6. DOM, signifie Document Object Model, structure de données arborescente représentant les éléments d’une
page web.
Grâce à cette méthode, chaque fois que l’utilisateur effectue un clique dans le navigateur
nous pouvons savoir s’il a été effectué dans le synoptique ou pas. Nous considèrons que le
synoptique n’est plus actif si un clique a été effectué en dehors de celui-ci. Dans ce cas, aucun
composant ne pourra être supprimé.
Cette boîte à outil permet d’éditer les propriétés. Lorsque l’utilisateur va sélectionner un
élément dans le synoptique, la boîte à outils est informée et elle affiche les propriétés du com-
posant sélectionné. Il est possible d’éditer deux types de propriétés : les propriétés en lecture
(à gauche sur l’image) et les propriétés en écriture (à droite sur l’image).
Les propriétés en lecture peuvent être remplies par des constantes ou des liens sur des variables,
ceci implique deux types d’éditeur différents. Les propriétés en écriture sont éditées par un seul
type d’éditeur.
A noter, nous ne reviendrons pas sur l’aspect visuel des éditeurs. Il a déjà été abordé pré-
cédemment.
Création de l’éditeur
Pour générer les éditeurs de constante, nous utilisons une factory, à savoir la classe ConstantPro-
pertyEditorFactoryService. Celle-ci va créer le bon éditeur en fonction du type de la propriété
qui lui est passé. Elle contient un dictionnaire de correspondance entre le type de propriété et
la classe de l’éditeur lui correspondant.
Chaque éditeur de constante implémente la méthode getValue() qui est appelée pour définir
la nouvelle valeur de la propriété.
Avant d’aborder les deux types d’éditeur suivant, il est nécessaire d’aborder l’explorateur
de variables Woopsa. Il s’agit d’un composant créé pour parcourir un serveur Woopsa et sé-
lectionner une variable.
Ce composant se nomme WoopsaVariableViewerComponent. Voici un rappel de son aspect
visuel :
Ce composant est basé sur le composant Tree de la librairie open source PrimeNG. Il
permet d’afficher une arborescence de donnée. Pour plus d’informations sur cet élément
https://www.primefaces.org/primeng/#/tree.
Nous allons commencer par expliquer comment la liste des transformations est affichée. Sur la
figure 6.34 nous pouvons voir qu’il est possible de sélectionner deux types de transformation. La
première ”No transformation” signifie que la valeur de la variable devra être reprise telle quelle.
La seconde ”Numerical value to text” permet d’afficher un texte en fonction d’un chiffre.
<span *ngIf="isTransformationNumberToColor()">
<p-colorPicker [(ngModel)]="getTransformationNumberToColor().color"></p-colorPicker>
</span>
<span *ngIf="isTransformationNumberToBoolean()">
<md-slide-toggle [color]="primary"
[checked]="getTransformationNumberToBoolean().value"
(change)="onSliderChange($event)">
</md-slide-toggle>
</span>
Listing 6.25 – Template HTML des transformations numériques
Comme vous le constatez, nous utilisons la fonction ngif qui permet d’afficher des éléments
HTML seulement si la condition entre guillemets est vraie.
Chaque instance NumberTransformationElementComponent contient un pointeur sur la trans-
formation qu’elle représente. Il est ainsi possible d’effectuer un lien entre le template du com-
posant et la transformation.
La liste de la figure 6.35 est triée selon le champ order de la classe BaseTransformation. Chaque
élément de cette liste peut être déplacé grâce à la souris, ceci affecte la valeur de l’attribut
order .
Cette liste provient également de la librairie PrimeNG. Pour plus d’informations sur ce
composant : https://www.primefaces.org/primeng/#/orderlist
Le but de ce type d’éditeur est de sélectionner une variable et une valeur à lui attribuer
lorsque l’utilisateur effectue une opération.
Lorsque la variable est sélectionnée, un simple champ de saisie est affiché. La variable sélec-
tionnée et le texte saisi permettent de remplir les champs variable et value de la propriété.
Les propriétés en écriture n’étaient pas prévues dans le cahier des charges. Elles ont été ajou-
tées afin de montrer l’étendue des possibilités. L’éditeur que nous venons de présenter n’est
pas sécurisé. Il faudrait limiter la saisie aux valeurs compatibles avec la variable sélectionnée.
7.1 Généralités
Dans ce chapitre, nous allons traiter du serveur. Ses deux missions sont de servir l’application
et de fournir l’API REST qui permet d’interroger la base de données. Il est composé de deux
éléments centraux. Une application Node.js et une base MongoDB. L’application Node.js est
développée à l’aide du framework Express.js et du framework Mongoose.
Rappelons que tout le serveur est développé en Typescript puis transpilé en JavaScript.
Express.js Ce système offre les fonctionnalités de base pour créer une API REST. Il simplifie la
création du serveur web ainsi que la gestion des routes.
Mongoose Cette librairie fournit les fonctionnalités pour manipuler la base MongoDB. Elle per-
met de créer les schémas et de les interroger pour récupérer les données. Rappelons que Mon-
goDB est une base de données dites NoSQL (”Not only structured query langage”). Dans ce
système de gestion de base de données, les données sont stockées sous la forme de docu-
ment JSON.
Toute l’initialisation de la base de données se fait depuis le code du server. Pour qu’il soit
fonctionnel, il suffit d’avoir installé et lancé MongoDB. L’adresse du serveur est configurée dans
des fichiers JSON situés dans le dossier config. Il existe trois configurations : default, dev et test. En
fonction de l’environnement défini pour lancer le serveur Node.js, l’une ou l’autre configuration
sera chargée. Ainsi, il est possible d’avoir une base de données pour les tests et une pour le
développement.
L’instance de la base MongoDB est créée à la volée lors du premier accès.
Le schéma suivant présente la structure de l’application qui permet de gérer la base de don-
nées
Nous présentons ci-dessous comment sont créés nos schémas. Chacun d’eux sera utilisé
pour créer une collection dans la base MongoDB.
export let ComponentDefinitionSchema: Schema = new Schema({
name: String,
resizable: Boolean,
template: String,
properties: [{
type: {type: Number, enum: Tools.fromEnumToNumberArray(PropertyType)},
name: String,
defaultValue: {}
}]
});
Listing 7.1 – Schéma du modèle ComponentDefinition
Les schémas ci-dessus sont des structures JSON représentant les données que les collections
devront stocker. Chaque champ correspond à un attribut des interfaces IComponentDefinition
et ISynoptic. Rappelons que ces interfaces sont utilisées sur le client également.
Le type : {} permet de stocker n’importe quel type d’informations. Cela permet une grande sou-
plesse, mais comporte certains risques. Nous pourrions nous retrouver dans une situation dans
laquelle les données sont si-différentes qu’il devient trop compliqué de les gérer. Il est donc
nécessaire d’être très rigoureux et d’éviter au maximum le type : {}. Il faut aussi prévoir un mé-
canisme pour être capable de retrouver le type des données stockées lors de leur extraction.
Pour le cas des transformations, l’interface ITransformations contient une valeur énumérée qui
définit le type de la transformation.
Les deux portions de code suivantes définissent le modèles objet appliqué à notre schéma.
Lorsque nous effectuerons des opérations sur nos schémas, ce modèle sera utilisé par Mongoose
pour structurer les données.
Dans le cas du synoptique nous avons utilisé une union de types. Cet artifice fourni par Typescript
permet de fusionner deux types en un seul. Il a été nécessaire de procéder ainsi car ISynoptic
possède le même attribut _id que Document. Il n’était donc pas possible de créer une nouvelle
interface implémentant ISynoptic et Document comme nous l’avons fait pour IComponentDe-
finitionModel.
Il est encore nécessaire d’instancier la collection ou de s’y connecter. Pour se faire, nous pro-
cédons ainsi :
this.synoptic = this.connection.model<ISynopticModel>('Synoptic', SynopticSchema);
this.componentDefinition = this.connection.model<IComponentDefinitionModel>('
ComponentDefinition', ComponentDefinitionSchema);
Listing 7.5 – Création des modèles
Ces deux lignes instancient les objets synoptic et componentDefinition de la classe DB. C’est à
travers ces objets que nous effectuerons toutes les opérations sur la base de données.
Pour commencer rappelons la définition d’un service web REST (ou API REST) tel que pré-
sentée lors de la pré-étude : ”Une API compatible REST, ou « RESTful », est une interface de pro-
grammation d’application qui fait appel à des requêtes HTTP pour obtenir (GET), placer (PUT),
publier (POST) et supprimer (DELETE) des données.” 1 .
Voyons comment se structure l’application.
Il est important de préciser que ce schéma représente une vue fonctionnelle des classes. L’im-
plémentation réelle des méthodes au sein des classes ComponentDefinitionRoute et Synop-
ticRoute diffère légèrement. Ces méthodes étant utilisées lors d’appel HTTP par le framework
Express.js, elles ont des paramètres nécessaires à son bon fonctionnement. Nous retrouvons ce-
pendant au sein de ceux-ci les paramètres que l’on peut voir dans le diagramme 7.2. Ils sont
simplement encapsulés dans des objets représentant des requêtes et réponses HTTP.
1. source : http://www.lemagit.fr/definition/API-RESTful
}
Listing 7.6 – Méthode getComponentDefinition
Grâce au modèle que nous avons vu dans la section précédente, il est très aisé de récupérer
un composant en fonction de son nom. La méthode findOne fourni par Mongoose prend en
charge ce travail.
Pour ces méthodes, nous avons utilisé la notation fléchée fournie par Typescript. Elle a l’avan-
tage de préserver le pointeur this lorsque la méthode est utilisée dans un callback (méthode
passé en paramètre d’une fonction). Dans ce contexte, le pointeur this n’est plus l’instance de
classe dont la méthode est membre, il est nécessaire d’utiliser cette notation pour éviter tout
problème.
8.1 Généralités
Ce client a été développé en suivant les spécifications du protocole que l’on peut trouver
sur www.woopsa.com. Il est également inspiré du client JavaScript fournit sur ce site. La société
mandante a également fourni un exemple de client Woopsa développé en Typescript. Cepen-
dant cet exemple n’était pas complet. La gestion du publier/s’abonner n’étant pas implémen-
tée, décision a été prise de le développer depuis la base. Cette opération a permis de bien
comprendre le fonctionnement du protocole.
Le client Woopsa a été développé dans son propre module. L’idée derrière cette séparation est
de limiter au maximum le lien avec l’application. D’ailleurs, seule quelques classes sont exposée
et utilisable par l’application. Il sera plus simple d’extraire le client pour en faire une librairie.
Ce client ne peut se connecter qu’à un seul serveur. Lors de l’analyse (durant la pré-étude),
le mandant avait émis l’idée de développé un serveur intermédiaire qui ferait le tampon entre
différents serveurs Woopsa et le client. Ceci permettrait entre autres de suivre et d’enregistrer
l’évolution de certaine variable. La communication entre ce serveur et le client serait effectuée
avec le protocole Woopsa tel que développée dans ce projet. Pour cette raison, le client n’a
pas été prévu pour se connecter à plusieurs serveurs simultanément.
8.2 Configuration
Pour utiliser ce client, il faut importer le module WoopsaModule dans le module de l’ap-
plication. La classe principale, à savoir WoopsaClient est un service (une seule instance dans
toutes l’application). Elle utilise l’injection de dépendance pour connaître la configuration qu’elle
doit utiliser pour se connecter au serveur Woopsa. Ainsi en injectant correctement la configura-
tion dans notre module AppModule, il est possible d’utiliser la classe WoopsaClient dans toutes
l’applications sans se soucier du serveur qui est utilisé.
L’adresse du serveur Woopsa et le port sont stocké dans le fichier de configuration du client
Angular. Il s’agit du même fichier qui contient les informations concernant l’API REST nécessaire
au bon fonctionnement de l’application.
C’est pour les raisons évoquées ci-dessus que les variables (classe Variable) ne conservent que
le chemin vers l’élément Woopsa désiré sans se soucier des informations propres au serveur.
La classe exposée au public est WoopsaClient. C’est depuis cette dernière que les opéra-
tions en lien avec Woopsa sont effectuées.
Les classes SubscriptionChannel et Subscription sont utilisés de manière interne. Elle permette
de gérer le système d’abonnement à une variable, via la méthode onChange.
Pour utiliser le système d’abonnement, le serveur Woopsa doit être muni de l’extension na-
tive SubscriptionService. Le serveur Node.js fournit par www.woopsa.org contient cette dernière.
Pour bien comprendre ce principe, voyons comment le serveur Woopsa fonctionne.
Tout d’abord, il est nécessaire de créer un Subscription Channel en invoquant la méthode Crea-
teSubscriptionChannel sur le serveur. Cette méthode retourne l’id du canal créé. A parti de là,
il est possible de souscrire à une variable avec la méthode RegisterSubscription du serveur. Les
méthode sont invoquée sur le serveur en appelant la fonction invoke de notre classe Woopsa-
Client qui elle-même effectue un appel grâce au client Http d’Angular.
Pour récupérer les notifications, le client utilise la méthode WaitNotification du serveur. Cet ap-
pel est bloquant tant qu’aucune notification n’est à récupérer. Dès que des notifications se
présentent, le serveur les envoie au client. Ce dernier peut ainsi les traiter, et effectuer un nouvel
appel à WaitNotification. Cette méthode retourne des lots de notifications qui doivent ensuite
être attribués aux abonnements. WaitNotification prend en paramètre le SubscriptionChannel
ainsi que l’identifiant de la dernière notification. Il a besoin de cette information pour supprimer
les éléments qui ont déjà été récupérées par le client. Pour plus d’informations, veuillez consulter
la spécification du protocole : http://www.woopsa.org/specifications/.
Pour s’abonner à une variable, il faut utiliser la méthode onChange de la classe Woopsa-
Client. Voici le prototype de cette dernière :
public async onChange(path: string, onChangeCallBack: (value: WoopsaValue) => void,
onRegisterCallBack: (subId: number) => void = null)
Listing 8.1 – Classe WoopsaClient méthode onChange
Le premier paramètre représente le chemin vers la variable à laquelle s’abonner. Le second est
la méthode à appeler lorsque le client est notifié d’un changement de valeur par le serveur.
Le dernier est également une méthode, celle-ci est appelée lorsque la variable a bien été
enregistrée. Ce paramètre est optionnel.
Voici le processus d’abonnement à une variable Woopsa, pour des raisons de compréhension
et de lisibilité seul les étapes principales sont représentées dans ce schéma.
Chaque appel à une méthode register correspond à un appel REST décrit dans la section pré-
cédente. Voyons de plus près le fonctionnement de la méthode waitNotification :
9.1 Généralités
Des tests unitaires ont été écrits pour certains éléments. Des frameworks différents ont été
utilisés pour le client et pour le serveur. Pour le client nous avons utilisés les frameworks Karma[5]
et Jasmine[7]. Pour le serveur Mocha[11] et Chai[1]. Le but initial était d’utiliser la même tech-
nologie de test pour l’ensemble de l’application. Cependant, les différentes recherches effec-
tuées ont poussé à prendre des technologies précitées du fait de leur intégration avec Angular
pour Karma et Jasmine et Node.js pour Mocha et Chai.
Les outils Karma et Mocha sont des frameworks de tests. Ils ont pour mission de créer l’environ-
nement propice aux tests et de les exécuter. Chai et Jasmine sont des librairies d’assertions.
Les tests de l’applications Angular n’ont pas pu être développé de manière aussi complète
que souhaitée au départ. Développer des tests pour la partie graphique d’une application est
relativement compliquée et chronophage. La prise en main du framework demande beau-
coup d’investissement. Des tests ont été développés pour le composant affichant la liste de
synoptique car il est utilisé à plusieurs endroits. C’est le seul élément graphique qui dispose de
tests unitaires. Il serait intéressant de tester d’autre éléments graphiques. Il faudrait cependant
déterminer une politique de test basé sur l’orientation donnée à l’application.
Des tests ont été implémentés pour les services d’accès aux données (SynopticDataService
et UserComponentDataService). Chaque transformation possède également son propre test
unitaire.
Pour développer l’api REST, les tests ont été d’une grande utilité. Grâce à eux, il est possible
de se passer du client pour valider chaque route du serveur.
Le serveur est testé de deux manières différentes. La première consiste à tester les classes du
modèle. La seconde test l’API REST via un client HTTP. De cette manière, on peut tester l’api de
bout en bout.
L’usage de test unitaire pour développer le serveur a permis de gagner beaucoup de temps
de développement.
Nous allons passer en revue les exigences établies lors de la pré-étude. Il est important
de noter que seules les exigences dont le numéro est plus petit que 2000 sont considérées.
Les autres avaient été directement exclues en accord avec toutes les parties prenantes par
manque de temps disponible pour les réaliser.
Nous constatons que toutes les exigences planifiées ont été réalisées. La numéro 0030 est notée
en orange car l’exigence très difficile à tester et valider.
Cette exigence a été ajoutée en cours de développement sur l’impulsion des conseillers et
avec la validation du mandant.
Page 63
Sébastien Schoepfer
Figure 11.2 – Planing des tâches, partie 2
Page 64
Conclusion 12
12.1 Concept
A ce stade, il est difficile d’évaluer la réussite du concept. Il faudra attendre que le système
soit présenté et mis en place pour évaluer le produit dans sa globalité.
Cependant, Objectis souhaitait poser les bases d’une plateforme Web permettant l’édition et
la visualisation de vues synoptiques pour superviser des installations techniques. Cet objectif est
rempli et démontre de la faisabilité du concept.
Le premier choix technologique a été celui du Web. Nous avions la possibilité de dévelop-
per l’éditeur de synoptique dans un client lourd. Cette piste avait rapidement été écartée au
profit des technologies web. Cela permettait de se démarquer des autres produits existants sur
le marché.
Nous sommes heureux de dire que cela reste bien une excellente décision. Premièrement, le
produit est très homogène à l’utilisation. Ce niveau, d’homogénéité aurait été plus difficile à
atteindre en développant un client lourd pour l’édition de synoptique. Deuxièmement, grand
nombre d’éléments développés pour le visualisateur de synoptique ont pu être réutiliser dans
l’outil de conception, nous avons ainsi pu être plus efficaces.
Il est indéniable qu’Angular était un très bon choix pour réaliser le client. La panoplie d’outils
dont ils disposent à presque suffit à tout faire. Seules deux librairies de composants graphiques
ont dû être ajoutées pour développer la totalité du client. Ce côté ultra complet, lui donne une
réputation ”d’usine à gaz” au sein de la communauté. Ceci ne s’est absolument pas fait sentir
lors du développement.
Les équipes derrière cette technologie sont très actives. Diverses versions mineures ont été pu-
bliées durant le développement. C’est très agréable de savoir que l’outil que l’on utilise est
amélioré de jour en jour. Certaines mises à jour ayant un impact sur notre application peuvent
poser problème, mais autant les traiter tout de suite et ainsi rester à niveau. Cela garantit éga-
lement une meilleure sécurité.
Couplé avec Angular, ces technologies forment ce que nous appelons communément la
pile MEAN (MongoDB ,Express, Angular et Node.js). Les choix technologiques côté serveur ont
également été positifs.
L’utilisation de Node.js et Express s’est révélée assez pratique grâce au nombre important d’ar-
ticles et de documents que nous trouvons sur le net à leurs sujets.
12.2.3 Typescript
Après plusieurs années passées comme développeur Delphi, puis .Net sur Winforms le tout
avec en tâche de fond de petits projets Java sur Swing puis JavaFX, il est assez effrayant de se
lancer dans un projet web, qui plus est, entièrement basé sur du JavaScript. L’utilisation du suren-
semble Typescript pour développer l’entier du projet s’est cependant révélée très concluante.
Ce langage allie le meilleur des deux mondes : la rigueur des langages objets tels que C# ou
Java et si nécessaire la souplesse de JavaScript et son typage dynamique.
La syntaxe élégante de Typescript rend son utilisation très agréable. Les fonctionnalités telles
que la déclaration de propriété directement au sein du constructeur allège le code (les para-
mètres du constructeur sont utilisés comme attributs de la classe, pas besoin de faire l’assigna-
tion à la main) .
Certes, il serait idéal que les navigateurs supportent nativement Typescript, mais le choix de
ce langage est une décision plus qu’appréciée et approuvée. Tout comme pour Angular de
nombreuses améliorations sont apportées fréquemment au langage et à son écosystème.
Il est fort probable que d’autres technologies Web auraient aussi pu être utilisées sur ce projet.
Cependant, la mise en œuvre de ces technologies s’est révélée un vrai succès et il a été très
plaisant de travailler avec.
Le soussigné, Sébastien Schoepfer, atteste par la présente avoir réalisé seul ce travail et
n’avoir utilisé aucune autre source que celles expressément mentionnées, si ce n’est les connais-
sances acquises durant ses études et son expérience acquise dans une activité professionnelle.
Epalinges, le 28.09.2017
S. Schoepfer