[DUMD 5/24] Utilisez correctement l'orienté-objet

Auteur du billet de blog : Nicolas Hilaire - Neotech Solutions

Nicolas Hilaire

Consultant .NET
  Publié le jeudi 27 octobre 2016

Artisan logiciel particulièrement intéressé par les technologies .NET. Polyvalent et curieux, je suis néanmoins à l'écoute des autres technologies du marché. MVP (Microsoft Most Valuable Professional) de 2007 à 2014, je suis également auteur d'un ouvrage pour apprendre le C#, à destination des débutants et de plusieurs MOOCs sur le C#, Windows Phone ou ASP.NET MVC...

Cinquième billet sur comment devenir un meilleur développeur. Pour retrouver le sommaire ainsi que tous les liens, rendez-vous sur le premier billet.

 

Ce n'est pas parce que l'on utilise un langage orienté-objet, comme le C#, le Java ou autre, que l'on fait correctement de la programmation orientée objet (POO). J'ai souvent pu observer dans les différents codes sur lesquels j'ai pu travailler que les développeurs font généralement de la programmation procédurale avec des objets.

En général, cela vient de l'architecture mise en place, très souvent la fameuse architecture en couches ; on se retrouve avec des couches contenant des classes sans états (très souvent appelées "services") qui ont des méthodes qui pourraient presque être statiques. On est très proche de la programmation procédurale avec des "fonctions" mais assez loin d'un code vraiment orienté objet.

Dans ce chapitre je vais faire quelques rappels sur la programmation orientée objet et son véritable intérêt.

 

L'encapsulation - souvent mal comprise

Vous savez sans doute déjà que l'encapsulation est la particularité de pouvoir masquer l'implémentation interne (d'une classe par exemple) à ses consommateurs... Ainsi, on va plutôt manipuler les comportements d'une classe (ses méthodes) que ses propriétés internes. Et tout ça grâce à la magie des mots-clés public, private, etc.

Sauf que la plupart des développeurs que j'ai rencontré croit que l'encapsulation consiste à avoir des champs privés et à les rendre accessibles avec des getter et des setter (accesseurs / mutateurs en français), voire par des auto-propriétés publiques en C#.

Ceci, ce n'est pas de l'encapsulation :

 

 

 Et les propriétés automatiques de C# ne le sont pas non plus :

 

 

L'encapsulation - en vrai - consiste à exposer une solution à un problème sans que le consommateur n'ait besoin de comprendre complètement le domaine du problème.

 

Par exemple en utilisant une liste ou un tableau, vous n'avez pas forcément besoin de connaître les détails d'un algorithme de tri optimisé pour pouvoir trier une liste ou un tableau.

Le fait de rendre des éléments privés - et donc non accessible aux utilisateurs - permet de faire en sorte que la classe ne puisse pas se retrouver dans un état incorrect en attribuant des valeurs qui ne sont pas cohérentes ou en ne faisant pas les affectations dans le bon ordre... L'encapsulation ne masque pas forcément la complexité mais plutôt expose la complexité avec une sécurité intégrée.

La prochaine fois que vous créerez une propriété dans une classe, posez-vous la question. Est-ce que je fais de l'encapsulation correctement ?

 

L'héritage - mal utilisé

L'héritage est un concept très facile à appréhender et la plupart des développeurs le comprends plutôt facilement. Sauf que l'héritage ne sert pas à grand chose sans le polymorphisme dont je vais reparler juste en dessous. Je vais donc être relativement bref ici.

On a tendance à expliquer l'héritage en disant que l'héritage s'applique quand on peut remplacer par "est-un" (je le sais, je l'ai fait !). Par exemple un chien "est un" mammifère, qui d'ailleurs "est un" animal, qui "est un" être vivant, etc. Donc, on pourrait construire une hiérarchie de classe où Chien dérive de Mammifère, qui dérive d'Animal, etc.

C'est un moyen mémo-technique très puissant et c'est bien beau sur un diagramme UML, mais c'est rarement le cas dans un vrai programme informatique et nous verrons un exemple où cet héritage n'est pas bon du tout dans un prochain billet.

L'héritage ce n'est pas "est-un" ou "est-une sorte de". L'héritage c'est décorer les méthodes, les variables, les attributs dans une sous classe.


Je dirais que globalement l'héritage est souvent mal utilisé, surtout l'héritage entre les classes. Ce que j'ai pu voir c'est qu'on se sert de l'héritage pour partager des comportements et pour factoriser du code. (factoriser est une bonne chose, voir le principe DRY). Voici un exemple de chose que l'on rencontre souvent :

Imaginons que je veuille afficher un panier dans un site d'e-commerce, en version très simplifiée et pas beau. Je pourrais faire un truc comme ça :

 

 

Oui, c'est un site web qui fonctionne dans une console ^^.

Et puis je dois travailler sur la classe Commande qui doit calculer aussi le montant du panier :

 

 

Comme nous sommes des développeurs avertis et sensibles au principe DRY, nous voyons bien que la méthode GetMontantPanier pourrait (devrait !) être factorisée. Voici comment est (mal) utilisé l'héritage pour faire ça :

 

 

On introduit une classe abstraite dont dérive le Panier et la Commande et qui contient la méthode GetMontantPanier. Comme ça, les deux peuvent l'utiliser => partage de méthode => réutilisabilité => factorisation => youpi !

Et cela est d'autant plus tentant lorsqu'on hérite déjà d'une classe de base, par exemple dans un framework où on peut dériver d'une classe comme Controller. On crée une classe PermissionController qui dérive de Controller et qui contient ce qu'il faut pour gérer les droits d'accès ou autre.

Je ne dis pas que ce n'est pas bien, je dis qu'on peut faire mieux et que l'héritage ne sert pas à ça (pas de polymorphisme là !). Typiquement, nous aurions très bien pu faire une méthode statique type "helper", par exemple :

 

 

Ou encore mieux, injecter une interface dans les constructeurs des classes et avoir une classe qui implémente le comportement de calcul de produit ; mais n'anticipons pas sur les prochains billets :)...

A chaque fois que vous voulez utiliser l'héritage, dites vous d'abord que c'est sans doute une mauvaise idée (sauf cas particuliers que vous découvrirez dans la suite des billets) et réfléchissez. Seulement après avoir réfléchit et quand il s'agira de la meilleure solution, alors seulement vous pouvez utiliser l'héritage.

 

Le polymorphisme - la clé pour gérer les dépendances

Voici le concept le plus important de la programmation orienté-objet. Sans le polymorphisme, la POO ne servirait à rien. Il existe plusieurs formes de polymorphisme, que vous pouvez aller voir dans wikipedia si vous avez envie de creuser le sujet.

Le seul élément qui va vraiment nous intéresser est la capacité que nous offre le polymorphisme à gérer les dépendances entre les classes. Le polymorphisme nous permet d'appeler une fonction dont on ne sait rien, à part sa signature. Grâce à cela, nous allons pouvoir mettre une interface entre une classe A et une classe B pour que A ne dépende plus de B. Et ça, c'est top ! :)

Ceci vous permettra de créer des architectures robustes en contrôlant les dépendances et en limitant le couplage entre les classes. Ainsi, votre cœur de métier pourra être indépendant de la base de données, ou de l'IHM, ou d'autres services tiers. Vous pourrez ainsi développer sous la forme de "plug-ins".

Pensez à votre IDE favori. Vous pouvez lui greffer des plug-ins qui lui ajoutent des fonctionnalités ; mais en aucun cas ces plug-ins ne peuvent venir perturber le cœur de métier de votre IDE.

 

Merci le polymorphisme.


Nous en reparlerons plus en profondeur lorsque nous aborderons l'inversion de dépendances.

 

Le mythe de la modélisation et de la réutilisation

En général, lorsqu'on vous enseigne la programmation objet, on vous dit que les objets peuvent modéliser le monde réel (je sais, je l'ai fait !). Vous pouvez modéliser la table qui est devant vous, avec ses propriétés et ses actions ; pareil avec une chaise. Ces objets peuvent interagir entre eux...

Allez-y, vous pouvez faire agir la chaise et la table en invoquant la méthode chaise.JeterSur(table) pour exprimer votre rage face à ce mensonge éhonté !

En général on ne peut pas et on ne doit pas modéliser le monde entier. Il faut vous concentrer sur les éléments qui correspondent à votre domaine. Imaginons que vous modélisiez un client. Vous allez surement commencer par créer une classe avec un nom, un prénom, un âge, une adresse, etc. Est-ce que c'est un client qui doit pouvoir être livré ? Dans ce cas, il faut gérer les adresses de livraisons. Est-ce qu'un client peut-être une entreprise ? Dans ce cas, vous allez gérer une notion d'adresse de facturation, ajouter un numéro Siret, etc. Mais si c'est un client dans la santé, il va avoir un numéro de sécurité sociale. Et si nous sommes dans la relation commerciale, peut-être que votre client n'est qu'un prospect...

Bref, vous avez compris ! Tout ça pour dire que vous devez modéliser votre client uniquement dans le domaine métier correspondant à votre besoin. Vous ne pouvez pas être exhaustif et même surtout, cela sera totalement contre-productif. En plus, il est fort possible que dans votre domaine métier, un client puisse avoir différentes significations. Imaginons encore un site d'e-commerce. Le client qui est en train de naviguer sur le site n'est pas le même concept que le client qui fait une réclamation au service après-vente ou encore celui qui est en train d'être livré par le prestataire de service. Dans ce cas, dans votre code, vous aurez sans doute des classes Client différentes qui sont un dans domaine borné spécifique.

 

En DDD (Domain Driven Design), on appelle cela des bounded contexts.


De la même façon, on vous dit que l'orienté objet va vous permettre de réutiliser les classes et les composants. C'est un peu vrai mais c'est souvent faux. Ce n'est pas parce que vous avez créé une classe Client dans un logiciel d'e-commerce que vous allez pouvoir la réutiliser dans une application bancaire.

Je suis certain que vous avez déjà rencontré des cas où vous auriez aimé réutiliser une classe ou un ensemble de classes réalisés par un de vos collègues, mais vous n'avez pas réussi car il ne l'avait pas écrit en pensant à vous et cela ne correspondait pas exactement à votre besoin.

Ecrire des composants réutilisables est très complexe et c'est un métier à part entière. On peut arriver à créer des composants techniques réutilisables (un composant qui envoi des mails, ou un composant qui permet de générer des PDF, etc.) mais il s'agit d'un travail assez conséquent et qui impose d'avoir une vision globale de tous les cas d'usages qui peuvent se présenter.

Pour les autres cas, rappelez-vous que c'est un mythe, et ne tentez pas d'utiliser l'orienté objet dans le seul but de pouvoir faire de la réutilisation. Vous allez être déçu et vous risquez de perdre votre temps.

 

Un objet = des données + des comportements

 Vous avez compris que votre domaine métier doit être représenté sous la forme d'objets. Alors, pourquoi voit-on souvent du code de ce genre ?

 

On voit ici qu'on utilise une classe dite "service" qui possède une méthode pour préparer un cocktail. La méthode récupère un client dans un annuaire, peu-importe où, et vérifier si son âge est inférieur à 18 afin d'être sûr qu'il est bien majeur. 

Oui, vous avez bien vu, la règle métier associée à la vérification de la majorité du client est dans le service. Personne ne fait jamais ça, non ! Jamais ! On le voit très souvent pourtant. Alors, avec un peu de chance, le 18 est une constante ce qui fait que si cette règle métier est à différents endroits dans le code (oui, elle sera à différents endroits dans le code, c'est certain), au moins si on change la règle et qu'on passe à 21 ans, il sera facile de changer.

Ahh, ça y est, je le vois dans vos yeux. Vous avez déjà vu ce code, sans doute que cette valeur est dans la configuration ou récupérée de la base de données. Les règles métiers sont implémentées dans le service, oui sans doute. :(

Alors qu'il s'agit d'une règle propre au client. Pourquoi ne pas simplement ajouter une méthode dans la classe Client pour savoir s'il est majeur  :

 

 

C'est ce qu'on apprend à l'école et pourtant, force est de constater qu'on le désapprend en faisant de la POO en mode procédural.

 

Conclusion

L'encapsulation et l'héritage sont souvent mal utilisés en POO. Par contre le polymorphisme est la clé qui va vous permettre de faire plein de choses et de gérer les dépendances entre vos classes. Nous l'utiliserons tout au long de ce cours.

En attendant, réfléchissez bien à vos encapsulations et voyez si vous pouvez vous passer de l'héritage pour résoudre vos problèmes. Le dieu de la maintenance d'application vous en remerciera.

De même, faites en sorte que vos objets modélisent correctement vos domaines et n'hésitez pas à ajouter des comportements à vos objets (avec toutefois une certaine mesure, que nous découvrirons dans les billets suivants).

 

Le prochain billet sera consacré aux pratiques agiles.

Commentaires