Le wrapping consiste à envelopper (to wrap en anglais) un code dans un autre code.

Le design pattern « Façade », illustré dans l’exemple ci-dessous, est un exemple de Wrapper.

Voici une petite illustration IRL du wrapping (tirée de Façade)

Quand nous passons une commande chez un distributeur, plusieurs actions s’enchainent :

  • fabrication
  • emballage
  • prise en compte des taxes
  • livraison…

La complexité de cet enchaînement nous est invisible puisqu’elle ne se réduit qu’à l’action de “commander” (Wrapper)

Exemple de Wrapper dans la vie de tous les jours
Wrapper IRL

Il y a un an, dans notre application, nous avions voulu remplacer la bibliothèque de gestion de date js, moment dépréciée alors. Or, les appels aux fonctions de moment étaient éparpillés dans notre application. Faire la migration au cas par cas, dans les différents fichiers de notre base de code aurait été pénible.

Pour s’assurer d’une harmonie dans le comportement des fonctions de date et pour faciliter le remplacement de la bibliothèque externe, nous avons opté pour l’utilisation d’un fichier dans lequel seront répertoriés des Wrappers qui appellent les fonctionnalités de moment, utilisés dans notre code. Cet exemple illustrera la suite de l’article.

Les avantages à wrapper

Centraliser les appels

Pour formater une date avec moment, on peut utiliser le code suivant :

moment(date).format(format)

Maintenant, encapsulons cette fonctionnalité dans une fonction que nous créons :

// time-wrapper.js

const  formatDate = (date, format) => {
  return  moment(date).format(format)
}

De cette façon, nous avons créé une fonction qui utilise une fonction de moment. On peut donc répéter ce processus et compiler en un seul fichier time-wrapper.js les fonctions de moment que nous souhaitons utiliser dans notre application.

Effectivement, lorsqu’on wrappe une bibliothèque externe, le reste du code ne doit pas directement appeler cette bibliothèque. Sinon, nous nuirons à la centralisation des appels. Le code doit absolument passer par notre Wrapper.

Ainsi, un premier avantage du Wrapper est l’inventaire des fonctionnalités, d’une source tierce, que nous utilisons.

Lisibilité et dé-complexité

Nous avons vu plus haut que nous pouvons réduire des appels de fonction successifs, à une méthode. Notre Wrapper nous a donc permis de gagner en lisibilité.

Ensuite, la prise en main du Wrapper est plus simple car il « cache » la complexité des fonctions qu’il utilise. En effet, la fonctionnalité étant implémentée qu’une seule fois à son niveau, celui-ci la découple de sa complexité.

De plus nous avons la main sur les déclarations des fonctions du Wrapper. (Logique, nous les créons)

On peut donc avoir un objet en paramètre de nos fonctions. Cela est pratique:

  • lorsqu’un ou plusieurs paramètres peuvent être non-définis
  • documenter : lorsqu’on appelle la fonction, ses arguments seront explicites

De cette manière, on peut réecrire la fonction formatDate qu’on a vue plus haut comme suit:

// time-wrapper.js

const  formatDate = ({ date, format }) => {
  return  moment(date).format(format)
}
/* au lieu de
const  formatDate = (date, format) => {
  return  moment(date).format(format)
}
*/

Qu’on appellera alors en explicitant chaque attribut de l’objet en paramètre:

formatDate({date: 1698942175420, format: "LLL"})
/* au lieu de
const  formatDate(1698942175420, "LLL") 
*/

Et si date est non défini, nous n’avons plus besoin de le préciser.

formatDate({format: "LLL"}) // date est undefined
/* au lieu de
const  formatDate(undefined, "LLL") 
*/

La centralisation des fonctionnalités qu’on utilise dans un Wrapper contribue aussi à faciliter leur compréhension. Les moyens suivants permettent la documentation des fonctionnalités wrappées:

  • les tests
  • les commentaires
  • un fichier de documentation créé dans le même dossier que le Wrapper
  • le nom des fonctions
  • la déclaration des fonctions et leur typage

Étendre une fonctionnalité

Parfois, les fonctionnalités proposées par une bibliothèque ne sont pas suffisantes pour un besoin particulier.

Avec un Wrapper, nous pourrions ajouter des fonctionnalités en enrichissant ce que nous propose la bibliothèque. Prenons l’exemple suivant :

const  logDate = (a) => {
  console.log(`Date input is a=${a}`)
  const  result = moment(a)
  console.log(`Result Date : ${result}`)
  return  result
}

La fonction logDate wrappe la fonction moment en lui ajoutant de la journalisation.

Par ailleurs, un autre exemple commun de fonctionnalité additionnelle que nous pourrions implémenter est une gestion d’erreur.

En effet lorsque cette dernière n’est pas proposée par l’existant, nous pouvons la créer dans un Wrapper.

  
const  createDate  = (date)  => {
  try {
  const  res  =  moment(date)
  if (!moment(res).isValid()) {
    throw  new  Error(`Mmmh il y a un souci de Date : ${date} semble être un mauvais input`)
  }
  return  res
  } catch (error) {
    if (error  instanceof  Error) throw  error
  }}

Ainsi, notre createDate gère un cas, comme étant une erreur, alors que moment ne le considère pas comme tel.

Modularité

L’utilisation d’un Wrapper nous donne la main pour étendre des fonctionnalités qui sont proposées par une entité.

Pour faire évoluer ces fonctionnalités, nul besoin de les traiter partout dans une application, mais uniquement au sein du Wrapper.

Reprenons l’exemple de la fonction formatDate. Si nous décidions de changer de bibliothèque de gestion de dates et utiliser day.js au lieu de moment, nous n’aurions qu’à modifier le fichier time-wrapper.js.

// time-wrapper.js

const formatDate = ({ date, format }) => {
  return  dayjs(date).format(format)
}

Et voilà. Ou presque…

Certes, nous avons modifié la fonction formatDate mais nous n’avons aucune certitude sur son comportement.

Ce qui nous amène au point suivant, le Wrapper doit être testé dans l’optique de voir son code évoluer.

L’idée n’est pas de tester l’intégralité d’une source tierce, mais plutôt de tester le Wrapper et ses fonctions qui sont utilisées dans notre application.

Si les tests sont au vert lorsque le code évolue, nous serons plus sereins.

Kairos ou le temps de l’opportunisme

Si on se demande quel est le bon moment pour wrapper, voici quelques raisons de mettre en place un Wrapper :

  • Migration : Abstraire l’utilisation du composant externe simplifie sa migration.
  • Faire l’inventaire des services tiers utilisés dans notre application.
  • Faciliter la documentation d’une fonctionnalité
  • Si on souhaite standardiser l’output d’une fonctionnalité dans nos applications. (Exemples : avoir les mêmes constructions de messages d’erreur, les mêmes formats de date, les mêmes url de base pour effectuer des requêtes etc..)
  • Lorsqu’on sent/sait qu’une dépendance risque d’évoluer et qu’il faudra la remplacer

Conclusion

Nous avons vu ensemble ce qu’est un Wrapper et quels intérêts peut présenter sa mise en place. Effectivement, il facilite la clarté des fonctionnalités qu’il enveloppe ainsi que l’évolution de celles-ci.

Pour ma part, le gros atout d’un Wrapper est la facilitation de la maintenance de fonctionnalités : si une source externe du projet est amenée à changer, la wrapper est une étape qui facilite assurément ces évolutions.

Attention toutefois à son utilisation car cela a un coût. C’est donc après analyse qu’il faudra mettre en balance la création ou non d’un Wrapper. Appliquer le principe YAGNI (“you ain’t gonna need it” ou “vous n’en aurez pas besoin”) nous évitera de produire du code inutile.

Par exemple : si une fonctionnalité ne changera pas ou peu, ou si elle est peu utilisée dans le code, on ne devrait pas investir dans la mise en place d’un Wrapper.

Dans cet article, nous avons discuté de Wrapper de fonction, néanmoins, il ne se limite pas seulement aux fonctions : on peut par exemple wrapper des classes, des types ou encore des variables…

Références