Blog 5 min de lecture

Domain-Driven Design, immutabilité et C++

Share this article
Domain-Driven Design, immutabilité et C++

Il existe un concept de programmation simple mais puissant qui reste largement sous-utilisé : l'immutabilité.

Fondamentalement, un objet est immuable si son état ne change plus une fois l'objet créé. Par conséquent, une classe est immuable si ses instances sont immuables.

Il y a un argument important en faveur de l'utilisation des objets immuables : cela simplifie radicalement la programmation concurrente. Réfléchissez-y : pourquoi est-il si difficile d'écrire du code multithread correct ? Parce qu'il est difficile de synchroniser l'accès des threads aux ressources (objets ou autres ressources de l'OS). Pourquoi synchroniser ces accès est-il difficile ? Parce qu'il est difficile de garantir qu'il n'y aura pas de situations de compétition (race conditions) entre de multiples opérations de lecture et d'écriture effectuées par plusieurs threads sur plusieurs objets. Et s'il n'y avait plus d'accès en écriture ? Autrement dit, si l'état des objets accédés par les threads ne changeait pas ? Il n'y aurait alors plus besoin de synchronisation !

La classe de l'objet référencé est immuable si elle n'a pas de champs publics, pas de méthodes capables de modifier ses données internes, et aucun moyen pour les méthodes des classes dérivées de modifier ses données internes. Et comme la valeur ne peut pas changer, il est possible de référencer le même objet dans tous les cas. Il n'y a pas besoin de constructeur de copie ni d'opérateur d'affectation. Pour cette raison, il est recommandé de rendre le constructeur de copie et l'opérateur d'affectation privés, d'hériter de boost::noncopyable, ou d'utiliser la nouvelle fonctionnalité C++11 des «fonctions membres spéciales explicitement defaultées ou supprimées».

Même si une classe C++ n'est pas conçue pour être immuable, ajouter le mot-clé const aux variables de son type fonctionnera. Cependant, cette solution a quelques inconvénients :

  • Vous devez ajouter const à chaque déclaration de variable que vous voulez rendre immuable ; l'oublier parfois générera des bugs insoupçonnés dans un contexte multithread.
  • Même si une variable est déclarée const, il peut encore être possible de muter l'objet au sein d'une méthode const si certains champs sont déclarés mutable. Dans ce cas, l'état de l'objet peut toujours être modifié, ce qui signifie que l'objet est muable.

Par exemple, la classe String est bien connue pour être immuable dans plusieurs autres langages, comme C#, Java et D. Les chaînes sont massivement utilisées par nature, et les rendre immuables est particulièrement adapté aux environnements multithread. En C++, std::string n'est pas immuable, l'alternative consiste donc à utiliser le mot-clé const.

Domain-Driven Design et immutabilité

Le domain-driven design est une approche de conception logicielle fondée sur deux prémisses :

  • Les conceptions de domaines complexes doivent reposer sur un modèle, et
  • pour la plupart des projets logiciels, l'attention doit se porter en priorité sur le domaine et la logique métier (par opposition à la technologie particulière utilisée pour implémenter le système).

En d'autres termes, le cœur du DDD est le modèle, et l'une des premières choses à faire au démarrage d'un développement est de définir le modèle. Le modèle et la conception que vous créez doivent se façonner mutuellement. Le modèle doit représenter la connaissance du domaine métier.

En général, les données partagées entre les threads concernent des entités du modèle, et rendre ces entités immuables aide à éliminer les effets de bord. Je ne saurais le dire mieux que Wes Dyer, alors je le cite :

We all know that generally, it is not a good idea to use global variables. This is basically the extreme of exposing side-effects (the global scope). Many of the programmers who don’t use global variables don’t realize that the same principles apply to fields, properties, parameters, and variables on a more limited scale: don’t mutate them unless you have a good reason.(…)

Une façon d'augmenter la fiabilité d'une unité est d'éliminer les effets de bord. Cela rend la composition et l'intégration des unités entre elles beaucoup plus faciles et plus robustes. Comme elles sont sans effets de bord, elles fonctionnent toujours de la même manière, quel que soit l'environnement. C'est ce qu'on appelle la transparence référentielle.

Un autre avantage des classes immuables est qu'elles ne peuvent pas violer le principe de substitution de Liskov (LSP). Voici une définition du LSP tirée de sa page Wikipédia :

Liskov’s notion of a behavioral subtype defines a notion of substitutability for mutable objects; that is, if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program (e.g., correctness).

Pour les objets immuables, l'état ne peut pas changer, donc les propriétés ne peuvent pas être altérées.

Détecter les classes immuables dans le code source C++

L'immutabilité est une propriété qui peut être vérifiée à la compilation — autrement dit, elle peut être vérifiée par des outils d'analyse statique. CQLinq, qui est fourni avec l'outil d'analyse statique CppDepend, propose une condition IsImmutable qui s'applique aux types. Pour savoir quels types de votre base de code sont immuables :

from t in Types where t.IsImmutable select t

Outre la condition IsImmutable sur les types, nous pouvons aussi utiliser deux conditions intéressantes :

ChangesObjectState et ChangesTypeState. Comme leurs noms le suggèrent, ChangesObjectState correspond aux méthodes qui assignent un champ d'instance de leur classe, tandis que ChangesTypeState correspond aux méthodes qui assignent un champ statique de leur classe.

La condition ChangesObjectState correspond aux méthodes qui assignent un champ d'instance de leur type parent.

La condition ChangesTypeState correspond aux méthodes qui assignent un champ statique de leur type parent.

Ces deux conditions facilitent l'identification des méthodes susceptibles de changer l'état du programme — autrement dit, des méthodes qui provoquent des effets de bord.

Une méthode qui ne provoque pas d'effets de bord est généralement appelée méthode pure. Pour trouver les méthodes pures avec CQLinq, vous pouvez écrire :

from m in Methods

where !m. ChangesObjectState && !m. ChangesTypeState && !m.IsConstructor

select m

Conclusion

Éliminer les effets de bord rend la composition et l'intégration des unités entre elles beaucoup plus faciles et plus robustes, et simplifie la programmation multithread. Les classes immuables et les méthodes pures sont la clé pour éliminer les effets de bord.

Share this article