Le travail de Terrence Parr dans son livre Language Implementation Patterns a servi de fondations pour modéliser ce qu’est une API de manière formelle. Il était important que le modèle soit aussi « générique » que possible. Ceci afin qu’il s’adapte à tous les paradigmes de programmation.
Le modèle est constitué d’un ensemble de classes simples :
C’est un type énuméré permettant de représenter de manière générique la portée ou visibilité d’un élément d’API. On retrouvera les différentes valeurs du type énuméré dans la Table 4.
| Valeur | Description |
|---|---|
| PRIVATE | Visibilité restreinte à la classe dans laquelle l’élément est défini (uniquement applicable pour les langages objet). |
| SCOPE | Visibilité restreinte au contexte courant (fichier, classe, module, foncteur, fonction, procédure, etc.). |
| PROTECTED | Visibilité restreinte aux sous-classes de celle où l’élément est défini (uniquement applicable pour les langages objet). |
| PUBLIC | Visibilité maximale. |
La plupart des langages de programmation n’utiliseront pas toutes les valeurs du type énuméré. Par exemple, le langage C n’a que deux niveaux de visibilité possibles : SCOPE pour une variable ou fonction déclarée dans un fichier .c, et PUBLIC pour une variable ou fonction déclarée dans un fichier .h.
La visibilité d’un élément sera cruciale pour déterminer l’importance d’une modification d’API. Plus l’élément affecté par la modification est visible, plus le risque engendré par celle-ci est élevé.
C’est l’élément de base du modèle. C’est une classe abstraite dont toutes les classes du modèle vont hériter. On retrouvera une description de ses attributs et méthodes dans les tables 5 et 6 :
| Attribut | Type | Description |
|---|---|---|
| name | String | Nom de l’élément. |
| language | String | Langage dans lequel l’élément a été défini. Un champ libre est utilisé ici plutôt qu’un type énuméré pour permettre une extensibilité des langages supportés par l’application. |
| sourceFile | String | Chemin du fichier dans lequel l’élément est défini. Il pourra éventuellement contenir une indication de numéro de ligne dans le fichier en respectant la syntaxe : <chemin>:<n° de ligne> |
| visibility | Visibility | Visibilité de l’élément. Voir paragraphe précédent. |
| parent | APIElement | Référence vers l’élément parent de celui-ci. |
| Méthode | Type de retour | Description |
|---|---|---|
| ident() | String | Renvoie un identifiant unique pour cet élément. L’implémentation par défaut renvoie le type effectif et le nom de l’élément : <type>:<nom> |
| path() | String | Renvoie un « chemin » permettant de retrouver l’élément dans la structure d’une API. Cette méthode sera utilisée par l’algorithme de comparaison. Elle utilise l’attribut parent pour calculer le chemin. |
Un symbole est la brique principale d’une API. Cela peut être une variable, une fonction ou même une définition de type (simple ou complexe). La classe Symbol est abstraite et hérite de la classe APIElement. Ses attributs propres sont décrits dans la Table 7.
| Attribut | Type | Description |
|---|---|---|
| modifiers | Set<String> | Ensemble de mots clés qui permettent de qualifier ou modifier le comportement d’un symbole. Ils sont dépendants du langage source. Par exemple en Java, le mot clé static permet de définir une variable ou méthode attachée directement à une classe et non pas aux instances de cette classe. |
Une variable est un espace de stockage pour une valeur auquel est attaché un nom. La plupart du temps cette variable permet de stocker des valeurs d’un certain type. Dans certains langages (comme C) on peut définir des variables avec des contraintes. Par exemple, on peut définir une structure de données avec des champs suffixés par une taille en bits [1] :
typedef struct DISK_REGISTER {
unsigned track :9; /* valeurs (0, 511) */
unsigned sector :5; /* valeurs (0, 31) */
unsigned write_protect :1; /* valeurs (0, 1) */
} disk_register_t;
Ici, les champs sont déclarés comme des entiers non-signés mais leur valeur est limitée à leur taille en bits.
On utilisera des objets de type Variable pour représenter les variables, les attributs de classe et les arguments de fonction. La classe Variable hérite de Symbol. Ses attributs propres sont décrits dans la Table 8.
| Attribut | Type | Description |
|---|---|---|
| type | String | Nom du type de la variable tel qu’il est défini dans le code source. |
| constraints | String | Optionnel. Cet attribut est un champ libre car sa valeur est trop dépendante du langage de programmation et donc trop incertaine. |
On utilisera la classe Function pour représenter à la fois les procédures, les fonctions et les méthodes de classe. Une fonction possède un type de retour, et une liste d’arguments. Function hérite également de Symbol. On retrouvera une description de ses attributs et méthodes dans les tables 9 et 10, respectivement.
| Attribut | Type | Description |
|---|---|---|
| returnType | String | Nom du type de retour de la fonction tel qu’il est défini dans le code source. |
| arguments | List<Variable> | Arguments de la fonction. Peut être vide. |
| hasVarArgs | boolean | Vrai si la fonction accepte un nombre variable d’arguments. Dans la plupart des langages, on symbolise cela par une ellipse « ... » à la fin des arguments. |
| exceptions | Set<String> | Dans certains langages, en plus du type de retour, on peut déclarer des types d’exceptions qui peuvent être émises par une fonction. Celles-ci ne changent pas toujours l’API de manière significative mais peuvent avoir un impact tout de même. |
| Méthode | Type de retour | Description |
|---|---|---|
| signature() | String | Dans certains langages (comme C++ et Java), il peut y avoir plusieurs méthodes d’une classe qui ont le même nom. Ce procédé est appelé « surcharge ». Pour les différencier, on procédera comme la plupart des compilateurs [2] en suffixant les noms de fonction avec les types de leurs arguments. |
| ident() | String | Cette méthode est redéfinie ici en utilisant la valeur de la méthode signature() à la place de l’attribut name. |
Dans la plupart des langages de programmation il est possible d’attribuer arbitrairement un type aux variables pour – entre autres – déterminer la nature des données qu’elle peuvent contenir et la manière dont elle sont enregistrées et traitées par le système. Concrètement le type d’un élément influe sur la taille que le compilateur ou l’interpréteur lui allouera en mémoire.
La classe Type hérite de Symbol et est également abstraite. Elle n’a pas d’attributs ni méthodes propres.
Un type simple sera en règle générale un type scalaire du langage de programmation (booléen, nombre entier, caractère, nombre à virgule flottante, etc.) ou un alias vers un autre type pré-existant [3].
La classe SimpleType hérite de Type et n’en est qu’une version concrète. Elle n’a pas d’attributs ni méthodes propres.
Les types de données complexes sont des types composés de plusieurs types plus élémentaires et qui possèdent une architecture spécifique autorisant des traitements dédiés à leur type.
On utilisera des objets de type ComplexType pour représenter des structures de données ou des classes (pour les langages orientés objet). Un ComplexType peut contenir un ensemble de symboles : des variables, des fonctions ou même d’autres types internes.
ComplexType hérite de la classe Type et possède deux attributs supplémentaires décrits dans la Table 12.
| Attribut | Type | Description |
|---|---|---|
| symbols | Set<Symbol> | Ensemble de symboles définis à l’intérieur du type complexe. |
| superTypes | Set<String> | Ensemble de noms de types dont le type complexe hérite. |
En règle générale, pour faciliter la maintenance et l’utilisation d’un composant, on place les éléments d’API dans des espaces de noms « nommés ». Ceci permet notamment d’éviter les conflits de noms. En Java et Ada, on utilise des packages, en C# et C++ : des namespaces, etc.
La classe APIScope reflète ce concept. Elle hérite directement de APIElement car ce n’est pas un symbole à proprement parler. En revanche, elle contient des symboles et éventuellement un ensemble d’APIScope. Ses attributs et méthodes sont détaillés dans les tables 13 et 14, respectivement.
| Attribut | Type | Description |
|---|---|---|
| dependencies | Set<String> | Ensemble de symboles dont l’APIScope dépend. Cet attribut représente les instructions du type import ou uses dans le code source. |
| symbols | Set<Symbol> | Symboles définis dans cet APIScope. |
| subScopes | Set<APIScope> | APIScopes contenus à l’intérieur de celui-ci. |
| Méthode | Type de retour | Description |
|---|---|---|
| update(APIScope) | - | Met à jour l’APIScope avec le contenu d’un autre passé en argument. |
N’importe quelle API sera toujours représentée par un APIScope « racine » qui n’a pas de nom. Pour les langages qui ne supportent pas les espaces de noms « nommés » (comme C) une API sera modélisée uniquement par cet APIScope « racine ».
L’Illustration 8 montre une vue d’ensemble des modèles précédents organisés les uns par rapport aux autres.
Diagramme de classes, modélisation d’une API
Maintenant qu’une API peut être modélisée, il faut définir comment modéliser des modifications de celle-ci. On partira du principe qu’une modification est détectable entre deux versions (A et B) des informations d’API d’un même composant.
Le type énuméré ChangeType sert à qualifier le type d’une modification d’API afin de pouvoir associer un niveau de risque à celle-ci. Les différentes valeurs du type énuméré sont détaillées dans la Table 15.
| Valeur | Description |
|---|---|
| REMOVED | L’élément a été supprimé de l’API. |
| ADDED | L’élément a été ajouté à l’API. |
| CHANGED | Une des propriétés de l’élément a été modifiée. |
Les modifications seront considérées de manière unitaire. Si plusieurs changements sont détectés sur un même élément d’API, ils donneront lieu à plusieurs « modifications ».
La classe qui représente une modification d’API est APIDifference. Elle est caractérisée par un type de changement (ChangeType), une référence vers le ou les APIElements concernés et dans le cas d’une modification de type CHANGED, l’attribut concerné par cette modification. Pour des raisons pratiques et de performance, les valeurs de l’attribut modifié seront stockées dans les objets APIDifference.
| Attribut | Type | Description |
|---|---|---|
| changeType | ChangeType | Type de modification. Voir paragraphe précédent. |
| elementA | APIElement | Élément de la version A concerné par la modification. Non-défini si il s’agit d’un ajout. |
| elementB | APIElement | Élément de la version B concerné par la modification. Non-défini si il s’agit d’une suppression. |
| attribute | String | Nom de l’attribut concerné par cette modification. Uniquement défini pour les modifications de type CHANGED. |
| valueA | Object | Valeur de l’attribut pour la version A. Uniquement défini pour les modifications de type CHANGED. |
| valueB | Object | Valeur de l’attribut pour la version B. Uniquement défini pour les modifications de type CHANGED. |
Sur l’Illustration 9, on trouvera un diagramme de classes de ChangeType et APIDifference.
Diagramme de classes, modélisation d’une modification d’API
La notion d’APIDifference est trop technique pour pouvoir en tirer des conclusions et évaluer le risque impliqué par celle-ci. Il va falloir trouver une notion dérivée plus exploitable dans la gestion du risque : la « stabilité » d’une API. Le principe de base est que « quand une API ne change pas, le risque est nul ». Le but va ensuite être d’évaluer le niveau de risque attaché à chaque modification détectée.
Le problème sera traité de la même manière que les outils de contrôle de qualité de code source1 tels que lint, CheckStyle, PMD ou Klocwork. Ces outils proposent un mode d’opération assez similaire : ils définissent un ensemble de règles de codage (configurables ou non) qui sont évaluées sur une représentation du code source analysé. Chaque violation de ces règles est associée à un niveau de risque.
Pour clarifier l’intérêt et le fonctionnement de ces outils d’analyse de code, voici un exemple pratique : L’outil CheckStyle possède un ensemble de règles de style de code (configurables pour la plupart). L’une d’entre elles vérifie si le développeur n’utilise pas de nombres magiques. Un nombre magique est une constante numérique qui diminue souvent la lisibilité du code.
Si on considère le code Java suivant :
if (response.getCode() > 400) {
throw new HttpError(response);
}
Ici, le nombre 400 a une signification assez floue. Il serait préférable de le remplacer par une constante nommée. En évaluant ce code, CheckStyle va produire une violation de ce type :
<violation line="87" column="41" severity="error"
message="400 should be defined as a constant."
source="com.checkstyle.checks.coding.MagicNumberCheck" />
Dans APIWatch, les classes mises en œuvre pour la gestion de stabilité d’API sont les suivantes :
Le type énuméré Severity sert à qualifier le niveau de risque engendré par une modification d’API. Les différentes valeurs du type énuméré sont détaillées dans la Table 17.
| Valeur | Description |
|---|---|
| INFO | Niveau de risque nul. La modification n’entraîne aucune rupture de compatibilité. |
| MINOR | Niveau de risque mineur. La modification porte sur des parties réduites et ne risque pas d’entraîner de rupture de compatibilité. Elle mérite néanmoins d’être contrôlée. |
| MAJOR | Niveau de risque majeur. La modification porte sur des parties sensibles et a des chances non négligeables d’entraîner une rupture de compatibilité. Elle doit impérativement être contrôlée. |
| CRITICAL | Niveau de risque critique. La modification a de fortes chances d’entraîner une rupture de compatibilité ascendante. |
| BLOCKER | Niveau de risque bloquant. La modification entraîne une rupture complète de compatibilité ascendante. |
Quand une modification est évaluée par une règle de stabilité d’API (voir plus loin), une violation peut être émise. Les attributs de la classe APIStabilityViolation sont détaillés dans la Table 18.
| Attribut | Type | Description |
|---|---|---|
| difference | APIDifference | Modification d’API qui a donné lieu à cette violation. Voir 3.2.2. |
| rule | APIStabilityRule | Règle qui a donné lieu à cette violation. Voir paragraphe suivant : APIStabilityRule. |
| severity | Severity | Niveau de risque de cette violation. |
| message | String | Message portant plus de détails sur la violation et son contexte. |
Sur l’Illustration 10, on trouvera un diagramme de classes de Severity et APIStabilityViolation.
Diagramme de classes, violation de règle de stabilité d’API
Cette classe abstraite (ou interface) sera utilisée pour représenter la notion de règle de stabilité d’API. Les attributs de la classe APIStabilityRule sont détaillés dans la Table 19.
| Méthode | Type de retour | Description |
|---|---|---|
| id() | String | Identifiant unique de la règle. |
| name() | String | Nom de la règle |
| description() | String | Description de la règle |
| configure(Map<String, String>) | – | Configuration du comportement de la règle. Par exemple, quel est le niveau de risque des violations émises dans certains cas. Les détails sont laissés à chaque implémentation. |
| isApplicable(APIDifference) | boolean | Renvoie Vrai si la règle est applicable à une modification d’API donnée. Cette méthode permet de décider si il est nécessaire d’évaluer les violations provoquées par une modification d’API. |
| evaluate(APIDifference) | APIStabilityViolation | Évalue une modification d’API et renvoie (ou non) une violation de stabilité d’API. Le risque associé à cette violation peut être configuré selon les implémentations des règles. |
On retrouvera un diagramme de la classe de l’interface APIStabilityRule sur l’Illustration 11.
Diagramme de classes, règle de stabilité d’API
Plusieurs implémentations par défaut de l’interface APIStabilityRule seront proposées dans APIWatch :
| Classe | Description |
|---|---|
| ElementRemoval | Suppression d’un élément d’API. |
| ElementAddition | Nouvel élément d’API. |
| ReducedVisibility | Réduction du niveau de visibilité d’un élément d’API. |
| DependenciesChange | Modification des dépendances d’un APIScope. Uniquement applicable aux éléments de type APIScope. |
| FunctionTypeChange | Modification du type de retour d’une fonction. Uniquement applicable aux éléments de type Function. |
| ModifiersChange | Changement des « modifers » d’un symbole. Uniquement applicable aux éléments de type Symbol. |
| SuperTypesChange | Changement des « super-classes » d’une classe ou interface. Uniquement applicable aux éléments de type ComplexType. |
| VariableTypeChange | Modification du type d’une variable. Uniquement applicable aux éléments de type Variable. |
Toutes ces règles sont configurables par l’utilisateur. Un détail des propriétés ajustables est donné en annexe A.
Il doit être possible de fournir des implémentations supplémentaires de l’interface APIStabilityRule (voir paragraphe 3.2.4).
Voir Extensions
Le cœur du système APIWatch se base sur l’analyse de code source pour en extraire des données d’API. Les composants de plus haut niveau (internes ou non à APIWatch) feront appel au service Analyser. Ce service contient des références vers toutes les implémentations disponibles du service LanguageAnalyser indexées par leurs extensions de fichier supportées.
Le service Analyser expose une unique fonction analyse() qui prend un ensemble de fichiers en paramètre. Pour chaque fichier, il va rechercher un analyseur supportant son extension et lui déléguer l’analyse de celui-ci. Le résultat (APIScope) de cette analyse va être stocké temporairement. Enfin, tous ces résultats intermédiaires vont être fusionnés en un seul, représentant l’API du code source analysé.
On trouvera sur l’Illustration 12 un diagramme de flux représentant le mécanisme d’analyse utilisé.
Diagramme de flux, analyse de code source
Pour pouvoir détecter des violations aux règles de stabilité d’API, il faut calculer les différences entre deux versions d’un même composant.
Le service DifferenceCalculator proposera une fonction getDiffs() prenant en paramètre deux objets du type APIScope et renvoyant une liste d’objets APIDifference. Les différences seront recherchées récursivement dans l’ensemble des objets contenus par les deux APIScopes (voir Algorithme de comparaison).
Un diagramme de flux décrivant le mécanisme de calcul des différences d’API est donné sur l’Illustration 13,
Diagramme de flux, calcul des différences entre deux versions d’API d’un même composant
Une fois les différences entre deux versions d’une même API calculées, il faut évaluer ces différences à l’aide de règles de stabilité d’API.
Le service ViolationsCalculator doit en premier lieu être configuré avec un ensemble de ces règles. Puis, on peut appeler la méthode getViolations() avec pour paramètre, les différences calculées. Chaque différence va être évaluée par chaque règle qui va émettre ou non une violation de stabilité d’API. Enfin, l’ensemble des violations détectées seront retournées par le service.
L’Illustration 14 montre un diagramme de flux représentant le mécanisme de détection de violations de stabilité d’API.
Diagramme de flux, détection des violations de stabilité d’API
APIWatch se présentera sous la forme d’un outil en ligne de commande et d’une interface web.
L’application APIWatch est destinée en premier lieu à être utilisée dans le cadre d’une gestion de projet logiciel en intégration continue1. Pour rendre APIWatch facilement automatisable, le plus simple est de fournir une interface en ligne de commande.
L’interface en ligne de commande doit permettre les opérations suivantes :
Afin de pouvoir garder une trace des données d’API des différentes versions des composants logiciels, il faut un support de stockage. Le moyen le plus adapté est d’utiliser une base de données relationnelle (un système de fichiers pourrait convenir mais serait plus difficile à mettre en œuvre). Les moyens d’accès à une base de données sont multiples et manquent de standardisation. De plus, pour l’utilisateur final, l’accès direct à une base de données n’est pas convivial. La technologie HTTP a été choisie pour masquer cette complexité et réaliser une interface utilisateur simple et compatible avec tous les navigateurs web.
L’interface web de APIWatch sera donc couplée à une base de données. Elle doit fournir les fonctionnalités suivantes :
Pour des raisons de modularité et pour faciliter la réutilisation, le découpage en composants suivant a été choisi :
Comme son nom l’indique, ce composant est le « cœur » de APIWatch. Il contient tout le modèle de données décrit aux paragraphes 3.2.1, 3.2.2 et 3.2.3. Il expose les points d’extension définis au paragraphe 3.2.4. Enfin, il contient toute la logique nécessaire aux services décrits au paragraphe 3.2.5.
Ce composant est la première brique modulaire de l’application. Il dépend de Core. Son rôle est d’apporter le support d’un nouveau langage de programmation à APIWatch en implémentant le point d’extension LanguageAnalyser. Plusieurs composants de ce « type » (un par langage) pourront donc exister simultanément dans l’application.
Ce composant est lui aussi un module interchangeable. Il dépend de Core. Son rôle est d’apporter le support d’un nouveau format de sérialisation à APIWatch en implémentant les points d’extension APIScopeSerializer et APIStabilityRuleSerializer. Plusieurs composants de ce « type » (un par format de sérialisation) pourront donc exister simultanément dans l’application.
Ce composant est lui aussi un module interchangeable. Il dépend de Core. Son rôle est d’apporter le support de nouvelles règles de stabilité d’API à APIWatch en implémentant le point d’extension APIStabilityRule. Plusieurs composants de ce « type » pourront donc exister simultanément dans l’application. Un composant implémentant les règles de base de stabilité d’API décrites au paragraphe 3.2.3 sera embarqué par défaut dans APIWatch.
Ce composant permet l’invocation des services de APIWatch via une interface en ligne de commande comme décrit en 3.2.6. Il a une dépendance forte vers Core, et un couplage lâche vers les composants XXX-Analyser, XXX-Serializer et API Stability Rules à travers le mécanisme de points d’extension.
Ce composant permet la persistance et l’accès aux données analysées par APIWatch par le biais d’une interface web comme décrit en 3.2.6. Il a une dépendance forte vers Core, et un couplage lâche vers les composants XXX-Serializer à travers le mécanisme de points d’extension.
On trouvera un schéma représentant tous ces composants et leurs interdépendances dans l’Illustration 15.
Structure et dépendances des composants de l’application APIWatch
Footnotes
| [1] | http://publications.gbdirect.co.uk/c_book/chapter6/bitfields.html |
| [2] | http://en.wikipedia.org/wiki/Name_mangling |
| [3] | Dans certains langages on peut définir des alias vers d’autres types pour rendre le programme plus lisible. Par exemple en C, typedef long ADDRESS; permet de déclarer des variables de type ADDRESS qui seront interprétées comme long par le compilateur. |