Background Image
TECHNOLOGIE

Se pencher sur les types

January 10, 2024 | 5 Lecture minute

Dans cet article, j'utilise du pseudocode inspiré de F# pour mes exemples. Toutefois, bon nombre de ces stratégies peuvent fonctionner avec un peu d'huile de coude dans d'autres langages.

Les types nous aident à détecter les erreurs, à concevoir nos interfaces et à créer un code lisible. Cependant, nous pouvons en tirer encore plus d'avantages si nous envisageons de remplacer nos drapeaux par des types. Prenons un exemple de sécurité. Lorsque nous lisons une chaîne de caractères à partir d'une source non fiable, du code peut y être injecté. Par conséquent, nous devons assainir la chaîne avant de l'utiliser. Disons que nous codons cela de la manière la plus simple qui soit :

let readQuery (source:Datasource) : string = ...
let someLogic (x:string) : Results = ...
someLogic (readQuery …)

Nous n'avons donc aucune protection contre l'utilisation de chaînes non assainies. Par exemple, si readQuery renvoie une chaîne de caractères contenant du code malveillant, someLogic l'utiliserait, avec un effet désastreux. Nous voulons nous assurer que someLogic ne puisse pas utiliser une chaîne de caractères non nettoyée. C'est un travail pour les types, mais QUELS types ? Par exemple, que se passerait-il si nous enveloppions notre chaîne dans un type et si nous avions un drapeau d'assainissement, ainsi que le code approprié, comme dans le cas suivant ?

type ExternalString = {
      value: string;
      isSanitized: bool;
}

let sanitize (x:string) : ExternalString = {
      value = …;
      isSanitized = true;
}

let readQuery (source:Datasource) : string = …
let someLogic (x:ExternalString) : Results = …
let results = someLogic (sanitize (readQuery …))

Cette approche nous oblige à ajouter de la logique à someLogic pour vérifier l'indicateur d'assainissement avant d'utiliser la chaîne. C'est un travail supplémentaire et nous pouvons oublier de le faire. Cependant, que se passerait-il si, au lieu d'utiliser un drapeau, nous faisions des chaînes assainies un type différent ?

type SanitizedString = Sanitized of string
let readQuery (source:Datasource) : string = …
let sanitize (x:string) : SanitizedString = Sanitized (...)
let someLogic (x:SanitizedString) : Results = …
let results = someLogic (sanitize (readQuery …))

Aujourd'hui, la fonction someLogic n'accepte que le type Chaîne assainie. Et puisque la seule façon de l'obtenir est d'appeler sanitizeil faut donc assainir la chaîne avant de l'utiliser.

Plutôt que de mettre en place un drapeau pour indiquer que la chaîne a été assainie, nous nous assurons que les chaînes assainies sont d'un type entièrement différent. L'idée clé est qu'un drapeau marque souvent les données comme étant d'un type différent. Alors pourquoi ne pas simplement créer un nouveau type ?

Cela permet d'éviter les cas où l'on oublie d'appeler l'assainissement des chaînes de caractères. De plus, en n'exigeant pas que nous vérifiions un drapeau d'assainissement dans notre logique, cela élimine les erreurs résultant d'un oubli. Ce n'est pas à l'épreuve des balles, mais il y a moins de possibilités de se mettre en difficulté.

Nous n'avons pas besoin de types de données algébriques pour cela. Nous pourrions déclarer un nouvel enregistrement, une nouvelle classe, etc... en tant qu'enveloppe pour une chaîne assainie dans n'importe quel langage avec un contrôle de type décent. Mais ce qui suit est (pour autant que je sache) propre aux langages qui supportent les types algébriques.

Les langages qui supportent les types de données algébriques (comme F#) vérifieront également si nous avons manqué certains cas. Par exemple, disons que nous voulons modifier notre fonction d'assainissement pour qu'elle vérifie notre chaîne de caractères et que si elle contient du code malveillant, elle la marque comme telle, sinon elle la marque comme une chaîne sûre. Nous avons maintenant affaire à deux types. De plus, si une chaîne est malveillante, nous ne nous soucions pas de ce qu'elle contient, nous voulons juste indiquer qu'une chaîne malveillante a été reçue. En fait, nous ne voulons pas la garder car nous voulons éviter que quelqu'un l'utilise par erreur. Cela nous donne maintenant ceci :

type SanitizedString =
      | Safe of string
      | Malicious

En plus de nos autres changements, someLogic doit gérer cela :

let someLogic (x:SanitizedString) : Results =
      match x with
      | Safe x -> ...

Mais lorsque j'essaie de construire en F#, j'obtiens ceci :

Incomplete pattern matches on this expression. Par exemple, la valeur "Malicious" peut indiquer un cas non couvert par le(s) modèle(s).

Oui, j'ai oublié d'ajouter la logique pour gérer le cas de Malveillant! Et comme je l'ai déclaré comme un type de données algébrique, F# l'a vu et m'en a informé. Je n'aurais pas eu cette vérification avec une déclaration ordinaire sur un drapeau. Je corrige donc le code :

let someLogic (x:SanitizedString) : Results =
      match x with
      | Sanitized x -> ...
      | Malicious -> ...

Maintenant, tout va bien dans le monde.

L'utilisation de types a donc également permis de vérifier l'exhaustivité de notre logique. Incidemment, il existe au moins un langage (Idris) qui va plus loin et génère le squelette couvrant tous vos cas pour vous si vous le demandez. Là encore, il utilise des types de données algébriques pour connaître toutes les possibilités.

Prenons maintenant un exemple non lié à la sécurité. Imaginons que nous écrivions un code pour traiter des candidats à l'emploi. Ces candidats ont une variété d'états, avec des données supplémentaires qui varient avec chaque état. Par exemple, un candidat qui...

1. A soumis un curriculum vitae doit inclure le curriculum vitae.

2. A programmé un entretien doit avoir une date et une heure pour l'entretien.

3. Rejeté doit avoir une raison pour le rejet.

4. A reçu une offre d'emploi doit être accompagné d'une offre de salaire.

Ce sont les seuls états légaux ; ainsi, par exemple, un candidat rejeté ne peut pas avoir d'offre de salaire. Cela signifie que nous ne pouvons pas faire cela :

type Resume = ...
type RejectionReason = ...

type StatusType =
      | SubmittedResume
      | ScheduledInterview
      | Rejected
      | MadeOffer

type ApplicantStatus = {
      firstName: string;
      lastName: string;
      ...
      statusType: StatusType;
      resume: Resume;
      offer: decimal;
      rejectionReason: RejectionReason;
      interviewDate: DateTime;
}

Cela autoriserait des états illégaux et nous devrions ajouter une logique pour gérer ces états. Au lieu de cela, nous pouvons empêcher la représentation de tels états avec ceci :

type Resume = ...
type RejectionReason = ...

type Applicant = {
      firstName: string;
      lastName: string;
      ...
}

type ApplicantStatus =
      | SubmittedResume of Applicant * Resume
      | ScheduledInterview of Applicant * DateTime
      | Rejected of Applicant * RejectionReason
      | MadeOffer of Applicant * decimal

Chaque état est accompagné d'un tuple, et un seul type de tuple est autorisé pour chaque état. Comme vous pouvez le voir, ce type applique les états listés ci-dessus sans une seule ligne de logique ; tout est fait à travers le système de types. Nous avons dû réfléchir un peu plus à nos types, mais nous avons récupéré nos efforts en réduisant la logique à écrire et en signalant les erreurs au moment de la construction et pour tous les chemins de code.

En conclusion, il ne suffit pas d'avoir un système de types fort. Nous devons choisir d'utiliser le système de types même lorsque nous n'y sommes pas obligés et nous devons réfléchir soigneusement à nos types. Une astuce pour y parvenir est de remplacer les drapeaux par des types. En s'appuyant plus fortement sur les types, notre système de types devient un meilleur allié, plutôt que l'obstacle que trop de développeurs pensent qu'il est.

Technologie

Dernières réflexions

Explorez nos articles de blog et laissez-vous inspirer par les leaders d'opinion de nos entreprises.
What is the Core of Agile - Preview Image
AGILE

Le plan de Drucker : Du propriétaire du produit au dirigeant efficace Pt. 3

Comment préparer la prochaine génération de dirigeants à acquérir les compétences, l'expérience et la formation nécessaires pour diriger des entreprises en toute confiance.