Rendre l'implicite explicite
Quand on développe, on a souvent tendance à penser qu’on le fait avant tout de manière à ce que l’ordinateur nous comprenne. On oublie souvent cette citation que j’adore sortir à toutes les sauces :
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Et dès lors qu’on comprends que nous développons aussi pour nos pairs, il devient important de savoir comment donner le plus de sens à notre code de manière à véhiculer le maximum d’informations au prochain développeur qui devra passer sur ce que nous avons produit ou tout simplement notre futur nous qui nous remerciera alors.
Mais trève de bavardage, attaquons avec un cas concret et voyons ensemble comment enrichir notre code au fur et à mesure !
Notre point de départ
Partons d’un petit exemple en langage Go mais qui peut être retranscrit dans n’importe quel autre langage.
package main
// User représente un utilisateur au sens métier.
type User struct {
phone string // Numéro de téléphone
email string // Email de l'utilisateur
}
func NewUser(phone, email string) (*User, error) {
// La validation du numéro de téléphone et de l'email se ferait
// ici et retournerait une erreur si besoin.
return &User{phone, email}, nil
}
// Beaucoup plus loin dans notre code ...
func DoSomethingWith(phone, email string) {
// Et ici, on utiliserait le numéro de téléphone ainsi que l'email
// tirés d'un utilisateur.
}
J’ai volontairement remplacé les éventuelles validations par des commentaires car ce n’est pas ce qui nous intéresse aujourd’hui mais partez du principe qu’elles sont présentes.
Le constat
Plusieurs petites choses. Premièrement, dans notre entité User
, nos propriétés phone
et email
sont de simples types primitifs string
, on ne sait donc pas si les valeurs sont obligatoires ou non ni même quelles sont les conditions pour qu’un email soit valide. En parlant de validation, ici la responsabilité incombe à la classe contenante, pas très évident et on peut vite se retrouver avec une tonne de validations quand le nombre de propriétés augmente.
Ensuite, dans la méthode DoSomethingWith
, on demande un numéro de téléphone et un email sous forme de chaîne de caractères. On voit donc qu’il est extrèmement simple d’intervertir les deux au moment de l’appel et dans les yeux d’un autre développeur, nous n’avons pas communiqué correctement notre intention.
Au final, ce code fonctionne sans soucis mais sur un simple exemple, on voit déjà pas mal de petites choses qui pourrait aider à la compréhension du code et à la maintenabilité.
Donner du sens
Utiliser des types forts
Première étape, pour chaque concept que nous avons identifié, qui possède des règles qui lui sont propres, on crée un type. Alors évidemment cela suppose que vous utilisez un langage avec typage statique.
En Go, c’est particulièrement simple :
package main
type (
// Phone représente un numéro de téléphone.
// Si vous ne connaissez pas le Go, il s'agit ici de définir un nouveau type
// basé sur une chaîne. Il sera donc impossible de substituer une `string` à
// un Phone.
Phone string
Email string
// User représente un utilisateur au sens métier.
User struct {
phone Phone
email Email
}
)
func NewUser(phone Phone, email Email) (*User, error) {
// Partons du principe que la validation reste inchangée et s'effectue ici.
return &User{phone, email}, nil
}
// Beaucoup plus loin dans notre code ...
func DoSomethingWith(phone Phone, email Email) {
/// ...
}
Introduire de nouveaux types (Phone
et Email
) nous a permis ici de montrer clairement notre intention, pas d’erreur possible. Un simple coup d’oeil à notre entité User
et on comprend tout de suite de quoi il s’agit.
Notre méthode DoSomethingWith
aussi devient beaucoup plus claire et il nous est désormais impossible d’intervertir les paramètres lors de l’appel.
Forcer les règles à la construction
Nous avons donner beaucoup de sens mais ce n’est pas suffisant. La validation incombe encore à l’entité User
. Pour régler ce soucis, rien de plus simple, il suffit de créer des constructeurs (oui je sais en Go cette notion n’existe pas) pour nos nouveaux types qui s’assureront que les règles sont respectées.
package main
type (
Phone string
Email string
// User représente un utilisateur au sens métier.
User struct {
phone Phone
email Email
}
)
func NewEmail(value string) (Email, error) {
// La validation de `value` se trouverait ici et retournerait une erreur si
// on considère que ce n'est pas un email valide (via regex ou autre).
return Email(value), nil
}
func NewPhone(value string) (Phone, error) {
// Idem, la validation du numéro de téléphone viendrait se positionner ici.
return Phone(value), nil
}
func NewUser(phone Phone, email Email) *User {
// Plus besoin de valider les paramètres puisque si les instances existent,
// c'est qu'elles sont valides !
return &User{phone, email}
}
// Beaucoup plus loin dans notre code ...
func DoSomethingWith(phone Phone, email Email) {
// ...
}
Et là c’est beaucoup mieux ! Notre code est beaucoup plus clair sur ce qu’il manipule, la validation incombe aux types qui vont bien et on a rendu réutilisable nos types Email
et Phone
, sans compter que les tester devient un jeu d’enfant ! Notre entité User
ne peut plus lever d’erreurs et devient naturellement plus simple à utiliser et à tester elle aussi.
Ne serait-ce pas des … ?
Objets-valeurs ! Et bien en quelque sorte oui même si en Go l’immutabilité n’est pas tout à fait respectée ici. Mais dans l’idée, c’est exactement ça, on a sorti des concepts dans des types spécifiques, déplacer les règles dans des constructeurs de manière à ne manipuler que des objets valides et simplifier notre entité User
tout en la rendant plus expressive.
Et voici comment rendre votre code plus expressif et plus robuste 🎉 !