Dans cette session de l’événement NCrafts 2016, Matthias Noback s’intéresse aux fondamentaux d’un code de qualité, trop souvent ignorés dans la littérature. Ces concepts vous aideront à prendre les bonnes décisions et à identifier les problèmes lors des revues de code.

L’objet

Les développeurs connaissent les design pattern, ces solutions clés en main pour résoudre des problèmes récurrents que l’on rencontre en développement logiciel : Abstract factory, Singleton, Observer, etc. Parfois complexes, souvent mal compris, les design pattern sont très mis en avant dans la littérature. Autre élément de réflexion, les cinq principes SOLID. Ils se veulent les piliers d’un système maintenable, clair et évolutif. Mais ceux-ci sont abstraits et difficile à implémenter. Il faut revenir au sujet qui nous intéresse, la programmation orientée objet est avant tout une histoire d’objet.

Tous les objets se construisent à partir des types primitifs du langage : entiers, booléens, chaines de caractères, etc. Une différence fondamentale entre les types de base et les objets sont que ces derniers ont un sens, ils véhiculent un concept, une idée. Ils contiennent des données, aussi appelé état, et des comportements, des traitements. On peut catégoriser les objet selon la proportion de données/comportements qu’ils contiennent : un objet dit “anémique” ne contiendra que des données, un “service” ne contiendra que des traitements, etc.

Différents types d'objets

Différents types d’objets

Chaque développeur qui crée un nouvel objet se doit de trouver le bon équilibre entre données et comportements selon son besoin.

Bonnes pratiques de programmation orientées objet

Les services doivent être conçus comme des fonctions

Un service est un objet qui met à disposition de ses clients un traitement. Ce traitement doit répondre à un besoin bien défini.

Si l’on pousse les principes de responsabilité unique (SRP) et de ségrégation des interfaces (ISP) à leur paroxysme, chaque service ne devrait exposer qu’une unique méthode (il ne résout qu’un seul problème). On obtient un objet qui ressemble à une simple fonction sans état.

class UserService
{
  void RegisterUser(string userName);
}

Ce type de construction permet de travailler avec des éléments très petits, contenant peu de chemins d’exécution et est donc facilement testables.

Les effets de bord doivent être explicites

Une cause fréquente d’un code difficile à comprendre sont les effets de bord des classes et méthodes. Ce sont des comportements inattendus qui sortent du cadre du problème pour lequel le service est conçu.

void RegisterUser(string userName)
{
  ...
  _emailSender.NotifyUser(); // Envoi d'un email, effet de bord
}

On peut rendre ces effets de bord explicites en passant en paramètre des classes et méthodes les services responsables. Pour être parfaitement prédictibles, ces services devraient eux aussi ne contenir qu’une seule méthode.

void RegisterUser(string userName, EmailSender sender)
{
  ...
  sender.NotifyUser();
}

En injectant des services à méthode unique en paramètre, on s’interdit l’utilisation d’objets de type Service Locator. Ceux-ci donne un accès à n’importe quel service de l’application, ils masquent donc les dépendances et les effets de bords.

Cette pratique d’injection rend explicite les impacts du code, mais également les responsabilités de chaque service. A nouveau, ceux-ci deviennent facile à tester car facile à “mocker”.

Les objets doivent toujours exister dans un état valide

Une autre difficulté récurrente en programmation est que le développeur ne peut pas être certain que l’objet qu’il manipule va se comporter correctement. A-t-il été bien construit ? Est-ce que son état a été changé depuis la dernière utilisation ? Est-il capable de traiter le paramètre que je lui envoie ?

On se retrouve alors à tester l’état de l’objet avant de l’utiliser. Par exemple, vérifier la validité d’une adresse email avant de demander l’envoi du message. Si on passe notre temps à tester l’état des objets, on s’immisce dans leur logique interne et on pollue notre code. On dira qu’on viole le principe du Tell, don’t ask.

// On peut vérifier les paramètres avant de les envoyer
// au service...
if (_emailSender.IsValid(userAddress)
{
  _emailSender.Send(userAddress);
}

Une solution à préférer est de s’assurer que seuls des valeurs autorisées franchissent les frontières de l’objet.

La frontière d’un objet représente la séparation entre sa logique interne et ce qu’il expose à ses clients. Dans un langage classique, seuls trois éléments peuvent franchir cette frontière : les arguments d’une méthode, les valeurs de retour et les exceptions.

Franchir les frontières

Franchir les frontières

On s’efforcera alors de vérifier les préconditions des méthodes à l’entrée de l’objet. On s’assure ainsi que seuls des paramètres valides seront manipulés par les traitements internes. On peut aussi vérifier les postconditions et s’engager à ne renvoyer au code appelant que des valeurs correctes (par exemple, ne jamais renvoyer une valeur null).

// ... mais on préfèrera garantir l'intégrité des méthodes
// directement dans l'objet
class EmailSender
{
  void Send(string userAddress)
  {
    if (IsValid(userAddress))
    {
      ...
    }
  }
}

(Presque) Tous les objets doivent être immutables

Une autre problématique est que les objets sont généralement partagés dans l’application, référencés par plusieurs classes et consommés par plusieurs méthodes. Par exemple, une entité est souvent transmise des contrôleurs vers les services, en passant par des helpers, etc. On se retrouve avec des références multiples vers un objet qui peut être modifié sans prévenir par d’autres portions de code. Dans cette situation, le développeur perd confiance en la validité de l’objet et devient hésitant à l’utiliser.

On peut choisir de rendre notre objet immutable. Cela signifie que son état interne ne changera jamais, il est défini uniquement à sa construction. Cette caractéristique facilite la compréhension de l’objet et de son rôle dans l’application. Inutile de chercher toutes les références qui lui sont faites avant de comprendre dans quel état il se trouve.

L'objet immutable se copie

L’objet immutable se copie

Lorsqu’on veut finalement agir dessus, une copie modifiée de l’objet est renvoyée à l’appelant. L’instance initiale, référencée ailleurs, reste intacte.

class Email
{
  // Ajout d'un destinataire. La liste initiale est conservée
  EmailRecipients AddRecipients(string address)
  {
    var newRecipients = _recipients.Clone();
    newRecipients.Add(address);
    return newRecipients;
  }
}

Deux gros avantages à ce pattern :

  • La différence est clair entre lire l’état et modifier l’état des objets. Les effets de bords sont confinés à la copie retournée. Les clients peuvent choisir de travailler avec l’instance originale ou la copie modifiée.
  • On réduit le risque à passer des références en paramètre à d’autres services ou à utiliser des objets qui nous ont été fournis en paramètre. Le code en devient plus robuste.

Les objets doivent communiquer en utilisant des messages prédéfinis

Le concept d’échange de message fait parti des principes fondateurs de l’orienté objet tel qu’imaginé par Alan Kay. Un appel de méthode ou une valeur de retour peuvent être vus comme des messages échangés entre instances d’objet.

On en distingue trois types :

Trois types de messages

Trois types de messages

  • Les Command, où l’on demande à un objet de réaliser une action
  • Les Query, où l’on demande à un objet de nous fournir une information
  • Les Documents, qui contiennent l’information en elle-même. Ils sont typiquement renvoyés par les Query
Séparation command-query

Une bonne pratique est de respecter la Command-Query Separation (CQS). Cela signifie identifier et séparer au maximum les traitements chargés de modifier un état des traitements chargés de récupérer et de renvoyer l’état. Cela nous amène à la règle bien connue : la lecture d’une information ne doit pas modifier l’état de l’objet (ce qui serait alors considéré comme un effet de bord).


L’application est un objet à part entière

A l’image des objets et des services, une application encapsule des traitements et des données, échange des messages via fichiers ou webservices, et répond à un problème bien défini. C’est donc naturel de vouloir appliquer les principes ci-dessus à l’échelle de notre application.

  • Rester à tout moment dans un état valide en refusant de traiter des entrées incorrectes, comme des saisies utilisateur
  • Respecter la séparation Command-Query et organiser nos classes et nos services en conséquences, peut-être en s’aidant du pattern CQRS
  • Comprendre et maîtriser les effets de bord de l’application en utilisant par exemple une architecture hexagonale

Enfin, l’auteur met en garde contre l’utilisation de la sérialisation et désérialisation. Souvent utilisé par les ORM ou pour les transmission réseaux, ce type d’outil va créer et modifier directement le contenu des objets, sans respecter leurs frontières et leurs interfaces publiques. Une solution sera d’écrire son propre code de sérialisation. On peut citer l’Event Sourcing, un pattern qui permet notamment de reconstruire un objet dans un état valide et sans violer l’encapsulation.


 

Les fondamentaux de l’objet sont des éléments de réflexion essentiels au développeur lorsqu’il rédige son code. Premières briques d’une solution robuste et maintenable, ces pratiques sont à prendre en compte à tous les niveaux, de la méthode jusqu’à l’application. Ils sont souvent considérés comme acquis ou évident et ne doivent pas être négligés pendant les revues de code.

CI-DESSOUS LE REPLAY DE CETTE SESSION :