Modularité - Développez réutilisable

19/09/2018

Dans l'optique de réduire nos coups de production et de maintenance, il est intéressant de s'appuyer sur la notion de module, de package de fonctionnalités suffisamment abstraites pour être réutilisées dans différents projets. Selon vos frameworks et vos outils vous avez probablement déjà eu le besoin de piocher un module proposé à la communauté de développeurs dans telle ou telle bibliothèque de modules/packages. Mais qu'en est-il quant à la création de vos propres modules? En effet, la communauté est probablement très prolifique mais vous avez sans doute des fonctionnalités habituelles qui ne sont pas forcément disponibles et dans ce cas, il va falloir créer votre propre module.

 

Qu'est ce qui fait un bon module ?

On a tous utilisé des modules proposés par la communauté et on a tous fait l'expérience de modules de plus ou moins bonne qualitéet parfois, il arrive qu'on n'utilise pas les modules dans les cas pour lesquels ils ont été créés. En effet, c'est un peu comme un jeu pour enfants où on doit faire passer une forme ronde dans un trou carré... En terme plus technique, on doit faire passer notre cas d'entrée (notre besoin métier) dans le filtre fonctionnel du module. Parfois ça passe sans problème, parfois il faut forcer, et parfois ça ne peut pas passer du tout. Un bon module doit être en mesure de gérer un maximum de cas où ça passe, et pour cela, il doit aussi permettre à l'utilisateur de modifier/altérer le filtre pour lui permettre d'y passer son cas d'entrée, sans dénaturer la fonctionnalité finale, le point de sortie.

De là, on peut se demander ce qui fait un bon module. Quel est le cahier des charges que vous devez remplir lorsque vous créez un module ? Pour moi il existe trois points fondamentaux :

1. Il fonctionne... C'est à dire que la fonctionnalité principale fonctionne dans les cas prévus. C'est la priorité.

2. Il prévoit un large panel de cas d'entrée. C'est la notion d'abstraction.

3. Il prévoit l'altération fonctionnelle en limitant les risques de dysfonctionnement que cela peut engendrer.

 

Qu'est ce que la modularité ?

Lorsque vous développez une fonctionnalité pour des usages abstraits, une fois que vous avez pris en compte les notions de maintenabilité (je vous renvoie vers l'article sur l'abstraction), il est nécessaire de prévoir la manière dont votre fonctionnalité va être utilisée. Les différents développeurs vont devoir s'approprier et peut-être même déformer votre fonctionnalité pour l'adapter à leurs besoins. C'est là que va intervenir la notion de modularité et la nécessité pour vous de permettre la réutilisation de votre fonctionnalité dans des contextes particuliers mais abstraits. 

De la même manière que pour l'article sur la maintenabilité, je vais concrétiser ici certaines notions en php et en Drupal 8 mais ces notions ne dépendent pas d'un langage ou d'un framework, elles sont applicables à tout framework permettant l'intégration de modules / packages.

La signature

L'une des premières notions à prendre en compte lors du développement d'un module est la question de l'accès aux fonctionnalités. Aussi la question de la signature des méthodes est primordiale.

Il existe un principe qui veut que la signature de méthode par défaut soit privée. Ce principe veut que l'accès public à une méthode soit réfléchi et donc que sans réflexion (par défaut) on privilégie la signature privée. Je pense que c'est un bon principe mais pas dans le cas de modules réutilisables. Une méthode privée est inaccessible, point. Or, autant l'accès ne doit pas être systématiquement public, évidement, autant il ne doit surtout pas être privé. Ça empêche toute évolution, accès, branchement, altération d'une fonctionnalité qui  proviendrait de l'extérieur du module. heureusement, il existe une troisième voie, qui permet d'allier la sécurité et l'évolution avec la signature protégée. Et donc, dans notre cas, les méthodes protégées sont le meilleur compromis entre protection et ouverture. 

Les interactions entre modules

Lors du développement d'un module, on doit être en mesure de se demander quel est le panel de cas d'entrée que son module doit couvrir. Étant donné que le but n'est pas de définir l'ensemble des cas d'usage (au contraire, le but est d'en définir le moins possible), il faut s'assurer de mettre à disposition des points d'entrée accessibles aux développeurs tiers afin qu'ils puissent ajuster la fonctionnalité à leurs besoins, leurs cas d'entrée. Pour cela, il existe plusieurs méthodes qui vont être différentes selon les frameworks utilisés. Dans Drupal 8, on va pouvoir utiliser les hooks, les events ainsi que le système de plugin.

Les hooks

Les hooks permettent de proposer des points d'entrée uniques aux développeurs tiers de manière synchrone. Comme son nom l'indique il permet d'ajouter des actions spécifiques à des endroits précis d'un processus fonctionnel. Ils ne permettent pas de sortir de ce processus. Il s'agit de méthodes ayant un nom (ou un pattern de nom) précis, avec des paramètres clairement définis (potentiellement altérables), qui attendent (ou non) une réponse qui sera ensuite traitée par le processus de base. Ils permettent ainsi d'insérer des actions particulières à des développeurs tiers. 

Chaque module tiers peut donc venir se brancher à ces hooks et y insérer des actions particulières. Il faut bien prendre en compte que les hooks sont appelés successivement et que l'ordre peut avoir un impact important sur leur implémentation.

J'en profite pour ajouter une petite remarque aux utilisateurs de hooks :
Ces méthodes sont des points d'entrées et non des espaces de développement. Il n'est pas normal de trouver des hooks comprenant une centaine de lignes d'exécution, il est nécessaire pour la lecture du code d'atomiser vos processus et de déporter les parties fonctionnelles en dehors des hooks.

 

Voici comment déclarer un hook dans Drupal 8 : 

// Déclaration du hook : 
$nomDuHook = 'mon_hook';
\Drupal::moduleHandler()->alter($nomDuHook, $entity, $arg2, $arg3);




/*...*/

// Accrochage du hook en déclarant la fonction de hook : 
function mon_module_mon_hook($entity, $arg2, $arg3){
  $entity->set('title','Mon title');
}

Les events

De la même manière que les hooks, les events permettent de déclencher des actions tierces à des moments clés d'un processus. La différence principale est la méthode d'accroche, on passe ici par la gestion de base d'événements avec un dispatcher et un listener. 

Dans la logique, les events n'ont pas cette notion de synchronisme. C'est à dire qu'en théorie les méthodes accrochées par les événements n'attendent pas de valeurs de retour parce qu'ils ne sont pas sensés être récupérés par le processus de base. Les events sont déclenchés à des points précis et permettent de lancer des actions à des moments particuliers mais, dans la logique, il ne sont pas sensés altérer le processus de base. Ainsi l'ordre d'appel des différentes méthodes accrochées n'est pas sensé avoir d'impact sur celles-ci. Dans la pratique, ce n'est pas forcément le cas et les events peuvent être déclaré dans le même but qu'un hook en prenant en compte l'altération de paramètre ou une valeur de sortie. L'intérêt dans ces cas est probablement de rentre prioritaire l'utilisation de structure objet, ce que les hooks ne permettent pas instinctivement.

Voici comment générer un event dans Drupal 8 : 

// Génération de l'event : 
/** @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher $dispatcher */
$dispatcher = \Drupal::service('event_dispatcher');

$event = new Event();
$event->set('entity', $entity); // admettons qu'on veuille passer une entité dans l'événement.
$dispatcher->dispatch('custom_event', $event);


// Accrochage de l'événement, pour appeler la méthode 'onCustomEvent'
$dispatcher->addListener('custom_event', [$this, 'onCustomEvent']);

Les plugins

Sous Drupal 8, il existe un système de plugin qui s'appuie sur les annotations proposées par Symfony. Le principe est simple et souple : n'importe quel module peut déclarer une classe à un endroit précis avec une annotation particulière et cette classe sera récupérée et utilisée par un manager de plugin d'un autre module qui aura prévu l'utilisation de ce genre de classe annotée. Ici on est dans un modèle plus complet qu'une simple porte d'entrée puisque, grâce à ce système, on peut redéfinir plusieurs parties d'un processus, par exemple la lecture d'un flux pour une migration, la création d'un type de champs etc... Dans ces deux cas d'exemples, il existe un module dont la phase d'abstraction a suffisamment été poussée pour permettre aux développeurs tiers de créer un point d'entrée selon leur besoin sans dénaturer la fonctionnalité de base :  la migration ou la proposition de champ. 

Les plugins sont plus difficiles à mettre en oeuvre puisqu'ils nécessitent la création d'annotation et de manager de plugin mais c'est probablement la fonctionnalité qui permet la plus grande souplesse d'évolution et d'altération.

 

Les classes abstraites et interfaces

Une fois qu'on a rendu son code accessibles via les signatures qui vont bien et qu'on a apporté des points d'entrée à notre processus, on a déjà un module qui permet les évolutions et les altérations. Cependant il est préférable de proposer des outils aux développeurs tiers qui permettent de structurer leurs évolutions pour correspondre à la structure de votre module. Pour cela on va s'appuyer sur les classes abstraites et les interfaces.

Dans le contexte de création de module, les classes abstraites permettent de proposer un cadre aux développeurs tiers. Partons du principe que votre module utilise une instance d'un certain type avec lequel votre module va interagir à plusieurs endroits (sous entendu, plusieurs méthodes vont être appelées par votre module). La définition d'une classe abstraite dans votre module va permettre de forcer le développeur à redéfinir les interactions particulières via des méthodes abstraites, tout en proposant des méthodes ayant des fonctionnalités par défaut. Autrement dit, les méthodes abstraites vont permettre au développeur de gérer son cas métier entièrement, tout en s'appuyant sur une base fonctionnelle accessible et modifiable (en fonction des signatures que votre module proposera).

Se baser sur une classe abstraite peut être contraignant pour les développeurs tiers puisque ça nécessite d'avoir une classe d'un type qui hérite de cette classe abstraite. Parfois, il va mieux falloir proposer une structure simple sans proposer de fonctionnalité par défaut. C'est dans ces cas que les interfaces prennent tout leur sens. En effet, votre module peut n'avoir besoin d'interagir que sur certaines méthodes peu importe le pocessus de la méthode, ainsi la définition d'une interface listant toutes les méthodes utilisées pour les interactions peut suffire. Dans ce cas, le développeur tiers pourra utilisé un objet d'un type tout à fait indifférent à partir du moment ou cet objet implémente bien les méthodes que l'interface définit.

En proposant ce genre d'outils et en vérifiant la validité des éléments que votre module va récupérer d'autres modules, vous vous assurez du bon fonctionnement et de la fiabilité de votre fonctionnalité de base. Il faut en revanche bien vérifier la validité des éléments que vous ouvrez afin de pouvoir gérer des exceptions contrôlées. Et bien que ceci limite considérablement les bugs causés par une mauvaise utilisation, ça n'empêchera pas les développeurs tiers de causer des erreurs dans son contexte, mais votre module n'en sera pas la cause, ce sera son implémentation.

Conclusion

En prenant en compte tous ces éléments, à savoir l'ouverture du code (les signatures), la proposition de points d'entrée (hooks, events, plugin) ainsi que les outils permettant de structurer les interactions (classes abstraites et interfaces), on obtient un module qui est complet et utilisable par la communauté. Pour autant, ces éléments sont des aspects techniques et il ne faut pas négliger la logique dans tout cela, et bien sûr la documentation. Un module qui n'est pas pensé pour les utilisations par des développeurs tiers peut fonctionner, mais à mon sens il ne respecte pas la charte de base d'un bon module, réutilisable et évolutif. C'est pourquoi dès la phase de conception, une approche avec une vision d'interface pour les utilisateurs doit être apportée à tout module.

 

Ajouter un commentaire

HTML restreint

  • Balises HTML autorisées : <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Les lignes et les paragraphes vont à la ligne automatiquement.
  • Les adresses de pages web et les adresses courriel se transforment en liens automatiquement.
Votre email ne sera pas publié mais permettra à l'administrateur de vous recontacter en cas de problème