https://d226lax1qjow5r.cloudfront.net/blog/blogposts/lessons-learned-along-the-way-with-static-type-checking-in-ruby-dr/E_Static-Type-Checking_1200x600.png

Perspectives de l'intégration de la vérification statique de type dans Ruby

Publié le May 24, 2021

Temps de lecture : 9 minutes

Nexmo propose des SDK dans différents langages pour aider la communauté des développeurs à travailler avec nos diverses offres d'API. Il est tout à fait possible d'interagir directement avec chaque API REST par le biais d'appels HTTP qu'un développeur construit sur mesure. Cependant, l'utilisation d'un SDK permet au développeur d'atteindre ses objectifs plus rapidement et avec moins de frais généraux.

L'équipe prend donc très au sérieux la tâche qui consiste à concevoir et à développer chaque SDK. Des dizaines de millions d'appels d'API sont effectués chaque mois via les SDK par des utilisateurs du monde entier travaillant sur des initiatives allant de petits projets de loisir à des infrastructures d'entreprises multinationales.

C'est pourquoi nous cherchons en permanence à améliorer l'expérience des développeurs avec chaque SDK. Chaque SDK s'efforce d'atteindre les objectifs définis dans la Spécification de la bibliothèque du serveur en tenant compte des contraintes propres à chaque langage.

L'un des principes généraux énoncés dans la spécification est le suivant :

Nos bibliothèques devraient être explicites.

Cela signifie qu'idéalement, chaque classe, méthode, constante et autre devrait être définie et que sa valeur et ses paramètres devraient être connus des développeurs qui utilisent le SDK. Un code explicite est plus facile à incorporer, ce qui conduit à un code plus facile à déboguer et à résoudre les problèmes inévitables lorsqu'ils surviennent.

Pour atteindre cet objectif dans le Ruby SDK nous avons commencé à incorporer la vérification statique des types dans notre base de code à l'aide de l'outil Sorbet pour vérifier les types. La version v6.3.0 du SDK inclut l'installation et l'initialisation de la gem et des signatures de méthodes pour la classe SMS.

Y a-t-il eu des moments instructifs tout au long de ce processus ? Refondre une base de code à typage dynamique dans un langage qui a toujours été traditionnellement à typage dynamique produit quelques observations qui valent la peine d'être partagées. Au cours du travail pour la version v6.3.0, nous avons découvert les deux joyaux suivants :

Penser l'interface

Le SDK Ruby s'appuie sur les éléments suivants private et protected pour distinguer les différents composants de l'architecture de la bibliothèque. Le code qui n'est pas défini par l'un des deux mots-clés susmentionnés fait partie de l'interface publique.

Qu'est-ce que cela signifie concrètement pour vous en tant qu'utilisateur du SDK ? Il est important de définir ces différences pour plusieurs raisons.

En effet, le code défini à l'intérieur de l'interface public est un code dont l'utilisateur peut s'attendre à ce qu'il reste stable et tout remaniement ne doit pas entraîner de changements radicaux dans ce code sans que les utilisateurs en soient avertis et sans changement de version sémantique. Les classes et les méthodes présentées dans l'interface public du SDK sont les mécanismes sur lesquels vous vous appuyez directement pour effectuer votre travail dans vos cas d'utilisation. Il s'agit du code avec lequel vous interagirez directement en l'invoquant par son nom dans vos appels de méthode, c'est-à-dire client.sms.send.

Lorsque nous examinons notre utilisation des private et protected nous devons comprendre quand nous devons utiliser l'un ou l'autre.

Classiquement, les rubyistes n'ont pas passé beaucoup de temps à se préoccuper de ces distinctions. En fait, pour beaucoup d'entre eux, leur utilisation même était souvent considérée comme une " bonne pratique " et non comme une " obligation ", comme c'est le cas dans d'autres langages, tels que Java. Après tout, l'utilisation de la méthode #send permet au développeur de contourner la définition de l'interface et d'accéder directement aux méthodes qui y sont définies. Cependant, lorsque nous commençons à intégrer le typage statique dans Ruby, ces définitions d'interface prennent plus d'importance et exigent de nous plus d'exactitude dans leur application.

En Ruby, la différence entre private et protected est de savoir si une méthode peut être accédée en dehors de la portée de la classe dans laquelle elle a été définie. Prenons un exemple en utilisant le mot-clé private pour illustrer notre propos :

class MyExample
  def public_method
    puts "This is public"
  end

  private

  def private_method
    puts "This is private"
  end
end

Dans l'exemple ci-dessus, je peux appeler la fonction #private_method à partir de la classe MyExample mais si j'avais une autre classe, même si elle héritait de MyExamplela méthode ne lui serait pas accessible. Par exemple, si j'avais une classe définie comme suit :

class MySecondExample < MyExample
end

La méthode privée MyExample.private_method ne serait pas accessible à la portée de la MySecondExample classe. Ceci est vrai même si la seconde classe est une sous-classe de la MyExample classe.

Alors que les méthodes définies à l'intérieur du mot-clé protected sont accessibles aux sous-classes qui héritent de la classe mère. Par conséquent, si le mot-clé private dans l'exemple ci-dessus était reclassé en protectedles méthodes qui y sont écrites seraient accessibles dans la portée de la classe MySecondExample de la classe.

Peu importe que la méthode se trouve à l'intérieur du protected ou private le message qu'elle transmet aux développeurs utilisant le SDK est que ces méthodes sont susceptibles d'être modifiées sans que le monde extérieur en soit averti. Toute modification apportée à ces méthodes ne devrait pas avoir d'incidence sur le comportement public de l'application. Si c'est le cas, on peut se demander si cette méthode a vraiment sa place dans une interface non publique.

Lorsque nous avons commencé à intégrer la vérification statique des types dans la gemme Sorbet, l'un des premiers problèmes que nous avons rencontrés était que le vérificateur de types signalait des erreurs indiquant que les méthodes n'étaient pas accessibles.

Par exemple, la classe SMS comme beaucoup d'autres classes du SDK, tire parti de la méthode Nexmo::Namespace#request pour envoyer la requête à l'API. En raison de la flexibilité inhérente à la rigueur des définitions d'interface en Ruby, le fait que cette méthode soit définie sous le mot-clé private et qu'elle soit utilisée dans une sous-classe ne l'empêche pas de s'exécuter de la manière dont elle a été conçue. Pourtant, dans les meilleures conventions de conception d'interface, puisque cette méthode est utilisée dans une sous-classe, elle devrait implicitement être définie à l'intérieur du mot-clé protected . Ainsi, avant de redéfinir l'interface en protected le vérificateur de type a signalé l'erreur suivante :

lib/nexmo/sms.rb:109: Method request does not exist on Nexmo::SMS https://srb.help/7003

L'une des caractéristiques utiles de Sorbet est que chaque erreur est accompagnée d'une URL renvoyant à la documentation relative à ce code d'erreur. Dans le cas présent, la documentation relative à l'erreur 7003 indique ce qui suit : This error indicates a call to a method we believe does not exist (a la Ruby’s NoMethodError exception). La documentation continue en fournissant des exemples de code de problème et des moyens de les résoudre, ainsi que des explications supplémentaires sur l'erreur. Dans notre cas, je pense que la deuxième raison pour laquelle Sorbet peut générer cette erreur s'applique à notre code :

Même si la méthode existe au moment de l'exécution, Sorbet peut toujours signaler une erreur parce que la méthode n'est pas toujours présente.

Une méthode définie à l'intérieur de l'interface private est invisible en dehors de la classe dans laquelle elle a été définie. Bien qu'il soit possible de profiter de la flexibilité de Ruby pour l'invoquer, cela n'améliore pas son invisibilité inhérente. Ainsi, Sorbet insiste sur le fait que le code doit être visible à l'endroit où il est appelé. Cela garantit une base de code explicite.

Suivre chaque méthode jusqu'au bout

La deuxième perle que nous avons découverte au cours du processus d'introduction de Sorbet dans la base de code a été la réflexion approfondie sur toutes les implications de chaque méthode appelée et utilisée dans le code.

Souvent, même si une application est bien conçue, certains éléments peuvent échapper à notre attention. Une tentative sérieuse peut être faite pour tester à la fois les voies de succès et d'échec de l'application. Le code est conçu pour gérer la plupart des cas de figure courants qui peuvent survenir, mais il peut néanmoins y avoir des conséquences inattendues.

La valeur de retour par défaut pour la récupération d'un objet à partir d'un hachage de paramètres est l'un des domaines où cette question s'est posée pour nous. Le code invoquait #unicode?une petite méthode qui vérifie si la valeur de l'objet est au format Unicode ou non :

if unicode?(params[:text]) && params[:type] != 'unicode'

...

private

def unicode?(text)
  !GSM7.encoded?(text)
end

La méthode #unicode? renvoie un booléen en fonction de la valeur du paramètre. Mais que se passe-t-il s'il n'y a pas d'objet :text dans les paramètres ? La possibilité que cela se produise est incroyablement faible lors de la mise en œuvre, mais néanmoins, du point de vue du code, cela a un impact sur la valeur de retour de la méthode #unicode? méthode.

Si nous simulons cette action sans valeur pour params[:text] voyons ce que cela donne :

params[:text]
=> nil

Ruby renvoie nil lorsque la clé est introuvable dans un hash. Cela expliquerait donc pourquoi Sorbet a retourné une erreur lorsque cette méthode a été vérifiée. La signature de méthode créée pour la méthode #unicode? indique :

sig { params(text: String).returns(T::Boolean) }

La signature ci-dessus déclare que la méthode accepte une entrée du type String et renvoie une valeur de type Boolean type. Cependant, dans le cas où le paramètre est nil l'entrée devient nil et non un String.

À ce stade, il existe plusieurs options. L'une d'entre elles consiste à réécrire la signature de la méthode afin d'autoriser l'utilisation de Nilable comme entrée de méthode. Cela éliminerait le problème technique. Elle n'éliminerait cependant pas le problème architectural sous-jacent que Sorbet a mis en évidence.

Le code ne veut pas d'une situation où l'entrée est nil jamais. Si le paramètre est nilil y a un problème. Dans ce cas, le code doit en fait signaler une erreur à l'utilisateur au lieu de continuer à fonctionner normalement. Cette erreur améliorera l'expérience d'utilisation du SDK parce qu'elle aidera ceux qui développent avec lui à détecter, diagnostiquer et traiter les bogues dans leur code plus rapidement et plus tôt dans leur processus itératif.

Puisque l'objectif est ici de traiter le problème architectural sous-jacent et non le symptôme, la solution est d'utiliser une méthode qui ne renvoie pas nil lorsqu'aucune valeur n'est fournie. L'appel aux données des paramètres est refactorisé en :

params.fetch(:text)

La méthode #fetch telle qu'elle est décrite dans la documentation de l'API Ruby API docs lèvera une KeyError si la clé de l'objet est introuvable :

KeyError (key not found: :text)

Cette exception, lorsqu'elle est renvoyée à l'utilisateur, est informative et peut l'aider à améliorer son code dès le début de son développement.

Prochaines étapes

Avant de nous embarquer dans le processus d'incorporation de la vérification statique des types dans notre SDK Ruby, nous avons beaucoup discuté des mérites et des inconvénients d'une telle démarche. Une inconnue avant de s'engager dans cette voie était de savoir si cela conduirait à des avantages concrets dans le développement de notre SDK, et quels seraient ces avantages. À ce stade, la résolution de cette question inconnue est un oui catégorique.

L'introduction du typage statique dans notre base de code Ruby a aidé à concentrer notre travail en tant que développeurs du SDK sur une réflexion approfondie sur les implications de chaque choix de conception, d'utilisation de méthode et plus encore. Nous avons maintenu un processus d'examen approfondi de chaque demande de téléchargement dans notre équipe. Nous profitons de l'exécution de tests d'intégration automatisés et nous construisons des tests qui couvrent les voies du succès et de l'échec. L'ajout du typage statique est une nouvelle couche de garantie de la qualité du code et de l'expérience positive du développeur.

Le typage statique en Ruby ou dans tout autre langage à typage dynamique entraîne également des changements paradigmatiques dans la manière dont le code est écrit. Il impose une standardisation là où il y avait auparavant beaucoup plus de flexibilité. Ce point est controversé au sein de la communauté Ruby. Quelle est l'approche préférée ? La réponse à cette controverse est peut-être qu'elle se situe quelque part au milieu des deux extrêmes. Une certaine flexibilité préserve la magie de Ruby, tandis qu'une plus grande standardisation et des conventions réduisent la possibilité de découvrir des bogues ou des cas de figure jusqu'alors inconnus, bien plus tard dans le processus.

En ce qui concerne le SDK Ruby de Nexmo, nous continuerons à implémenter progressivement les types dans la base de code au cours des prochains mois. L'objectif est de parvenir à une base de code typée à 100% et de le faire de manière incrémentale.

Nexmo Ruby est un logiciel libre et les contributions sont les bienvenues ! Si vous souhaitez vous impliquer, vous pouvez nous trouver sur GitHub

Partager:

https://a.storyblok.com/f/270183/384x384/e5480d2945/ben-greenberg.png
Ben GreenbergAnciens de Vonage

Ben est un développeur en seconde carrière qui a auparavant passé une décennie dans les domaines de la formation pour adultes, de l'organisation communautaire et de la gestion d'organisations à but non lucratif. Il a travaillé comme défenseur des développeurs pour Vonage. Il écrit régulièrement sur l'intersection du développement communautaire et de la technologie. Originaire de Californie du Sud et ayant longtemps vécu à New York, Ben réside aujourd'hui près de Tel Aviv, en Israël.