La programmation orientée objet a ses avantages, mais il est parfois difficile d'en tirer parti. Si vous vous obstinez à vous en tenir aux idées de base, vous vous donnez du travail supplémentaire !

Nous avons tous appris cela à un moment donné, lorsque nous avons commencé à programmer : Le chien descend de l'animal, tout comme le poisson et l'oiseau. Le chien a de la fourrure et des dents, l'oiseau a des plumes à la place. Tous les animaux (ou du moins tous dans cet exemple) peuvent bouger, mais le type et l'exécution du mouvement sont spécifiques. Ainsi, l'oiseau peut voler et le chien peut courir ; la natation est possible pour le chien et le poisson, mais sa mise en œuvre est très différente.

C'est ainsi que la programmation orientée objet est expliquée au programmeur débutant. Sur cette base, on construit des systèmes de classes qui, à l'époque où j'utilisais Turbo Pascal, ressemblaient à la liste 1. Je ne me souviens que vaguement de la syntaxe de Turbo Pascal, je dois l'admettre. Mais ça n'a pas d'importance. Le point ici est que les premiers pas dans la programmation orientée objet ont enseigné plusieurs détails importants. Tout d'abord, une classe représente un mécanisme d'encapsulation. La couleur de la fourrure du chien, par exemple, est marquée comme privée dans la liste 1 et n'est donc connue que du chien lui-même. Cette modélisation n'est probablement pas très réaliste, mais elle sert d'exemple d'information encapsulée par la classe qui n'est accessible de l'extérieur que par une interface, le cas échéant.

Listing 1
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
type
  Animal = class
  public
    constructor Create();
    procedure Move();
end;
 
type
  Dog = class(Animal)
  private
    furColor: String;
 
  public
    constructor Create(); overload;
    procedure Move(); overload;
    procedure Run();
end;

L'encapsulation est importante


L'encapsulation était quelque chose de très important pour les adeptes de la programmation procédurale au moment de l'invention de la programmation orientée objet. Peut-être vous souvenez-vous, comme moi, de vos premiers pas en Basic ou même de programmes en C : là, l'encapsulation était difficile, voire impossible, et la protection contre la modification accidentelle des variables globales ne pouvait être obtenue que par la discipline du programmeur. Plus précisément, une telle protection était pratiquement impossible, et certaines applications développées par une équipe souffraient en conséquence de problèmes de stabilité drastiques.

Le deuxième point important dans l'exemple simple ci-dessus est que les classes combinent information et comportement. Cela a toujours été souligné : Les classes représentent des éléments de la vie réelle. Un chien "a" certaines propriétés, c'est-à-dire des informations ou des données, mais il "peut" aussi faire quelque chose, par exemple courir. De cette façon, les classes combinent les données avec la logique qui fonctionne avec ces données.

Maintenant, comme nous le savons tous, ce point de vue est devenu très largement accepté en principe. C'est du moins le cas dans le courant dominant, où les applications professionnelles sont conçues pour Windows, par exemple. L'ensemble du monde Microsoft repose sur l'orientation objet, même dans les domaines moins traditionnels (pour Microsoft) comme les API Web. JavaScript est un langage qui peut être utilisé de manière orientée objet, mais il utilise pour cela un concept bien différent de celui de Java, C++, Delphi ou C#, par exemple. Dans cet environnement, Microsoft souscrit largement à sa propre invention, TypeScript, qui, du moins au départ, était particulièrement axée sur les aspects orientés objet.

Pour être franc : Je pense personnellement que l'importance de l'orientation objet dans la programmation d'aujourd'hui est très surfaite. En outre, je connais de nombreux projets dans lesquels certains inconvénients de cette approche sont apparus clairement sans que les planificateurs et les développeurs n'y réagissent de manière tangible. L'orientation objet est considérée comme allant de soi dans de nombreuses équipes, comme un mal nécessaire. Cela peut être techniquement correct si vous travaillez dans un environnement comme .NET, où rien ne fonctionne sans classes et objets. Mais même dans cette situation, vous pouvez encore faire beaucoup pour rendre votre travail quotidien plus productif. Un écart structurel par rapport à certains concepts strictement orientés objet est utile à cet égard.

Que peut faire la classe ?


Passons maintenant aux détails : quels sont les problèmes rencontrés au quotidien lors du transfert d'animaux, de chiens et de poissons vers des clients, des produits, des réservations, des niveaux de stock et des soldes de comptes ? Les problèmes se trouvent le plus souvent, et même très tôt, dans la mise en œuvre du comportement, c'est-à-dire de la fonctionnalité. Le développeur y rencontre rapidement des situations difficiles. Par exemple, il est possible de dire : "Le client achète le produit." Donc, comme dans le Listing 2.

Listing 2
1
2
3
4
5
6
7
class Customer {
  public Buy(Product p) { ... }
}
 
class Product {
  ...
}

 

C'est un comportement que nous avons finalement voulu encapsuler dans une classe pour refléter le monde réel. Mais, malheureusement, il s'avère rapidement que cette vision est techniquement beaucoup trop superficielle. En fait, nous devons d'abord créer une commande dont les éléments individuels contiennent une référence au produit quelque part (Listing 3).

Listing 3
1
2
3
4
5
6
7
class OrderLine {
  public Product { get; set; }
}
 
class Order {
  public AddLine(OrderLine ol) { ... }
}

S'il y a deux types A et B qui se rapportent l'un à l'autre, deux développeurs sont rarement d'accord sur l'endroit où le comportement doit être accommodé. Vous pouvez dire : "Le client emprunte un livre à la bibliothèque." Mais l'inverse est également vrai : "La bibliothèque prête le livre au client". Il est amusant de constater que, dans le langage courant, il semble tout à fait légitime qu'un client emprunte un livre. Lorsqu'il est question d'argent et de banque, plutôt que de livres et de bibliothèque de prêt, la plupart des gens seraient probablement plus enclins à dire "La banque prête de l'argent au client".


En général, il est clair que même pour les opérations à deux éléments, il n'est déjà pas clair où classer le comportement assigné. Dans les applications commerciales, bien sûr, les transactions aiment être beaucoup plus complexes et travailler avec beaucoup plus d'éléments, de sorte que le problème est infiniment plus difficile. Que faire ?

Qui est l'acteur ?


J'ai vu des projets où la discipline est utilisée pour essayer de contourner le problème. C'est là que sont créées des règles comme "Si le rapport est de 1:n, créer des méthodes d'aide du côté n". Donc : Order.AddLine au lieu de OrderLine.AddToOrder. Bien sûr, de nombreuses règles de ce type sont nécessaires, et dans les situations 1:1, par exemple, elles ne sont pas forcément utiles. Si un développeur doit écrire du code pour une sous-section de l'application qu'il ne connaît pas encore très bien, il doit être évident de savoir quel comportement se trouve à quel endroit. Si cela n'est pas assez simple, des erreurs seront commises - jusqu'à réimplémenter une logique qui existait déjà mais qui était introuvable. La discipline est une bonne chose, toujours utile, et généralement nécessaire à un moment donné. Cependant, un ensemble plus large de règles ne garantit pas que les développeurs fassent tout correctement.

Dans d'autres projets, vous essayez de rendre le comportement disponible à tous les points significatifs. Il y aurait donc à la fois Customer.BorrowBookFromLibrary et Library.LendBookToCustomer. Bien entendu, pour les opérations complexes, vous devez inclure non pas deux, mais plusieurs variantes de la méthode. Certains programmeurs écrivent ensuite ces méthodes de manière à ce que l'une accède à l'autre et que la logique réelle ne soit programmée qu'une seule fois. Mais cette stratégie a également besoin d'un concept, sinon elle échouera tôt ou tard. Cette approche crée un énorme travail supplémentaire dans l'ensemble et ne garantit pas une meilleure maintenabilité à long terme.

Voici ma recommandation : Laissez les idées de la programmation orientée objet derrière vous lorsqu'il s'agit de mettre en œuvre la logique des applications d'entreprise ! Considérez les classes comme des types de données étendus. Mettez en œuvre la logique métier sous forme de processus, et non de comportements dans des classes de données.

Temps de comportement ailleurs


J'ai décrit ci-dessus que la possibilité de découvrir une implémentation logique particulière est un critère important pour une approche structurelle. C'est pourquoi je recommande de raisonner sur les structures possibles du côté de l'appel. Cela permet également au client de contribuer à la construction d'une telle structure de manière experte dans son domaine. Cela rappelle la méthode de planification Event Storming utilisée pour créer des systèmes basés sur l'approvisionnement en événements.

Par exemple, il s'avèrerait probablement que la bibliothèque pratique la "location" dans le cadre de ses activités commerciales. Ainsi, une API comme celle-ci serait possible : 

Lending.LendBook(book, customer);
Lending.AcceptReturn(book, customer);

Il est important que les méthodes LendBook et AcceptReturn soient implémentées en tant que méthodes de processus. Pour ce faire, il faut bien sûr commencer par définir les processus, mais cela ne devrait pas être difficile en raison de la référence directe à la vie pratique quotidienne de la bibliothèque. Par exemple, le processus "emprunter un livre" pourrait être défini grossièrement comme suit :

- Vérifier la disponibilité du livre
- Contrôle de plausibilité, tel que : Le client a-t-il déjà emprunté le même livre ?
- Ajouter le livre à la liste des livres empruntés par le client
- Retirer le livre du stock
- Saisir une opération de prêt pour la relance


À partir de cette description de la transaction, vous pouvez estimer combien de types de données différents sont impliqués - bien plus que le livre et le client. Bien entendu, il serait tout aussi difficile de faire correspondre la transaction à ces types en utilisant des méthodes. En tant que méthode de traitement, en revanche, c'est assez simple.

Lorsque vous envisagez pour la première fois d'extraire la fonctionnalité métier de votre application à partir des classes de données, vous constaterez que vous convergez vers de nombreuses approches et modèles communs. Les méthodes de processus de ce schéma sont apatrides (et doivent l'être !), de sorte qu'elles peuvent être mises en œuvre en utilisant, par exemple, le mot-clé static en C#. Il s'agit d'une approche de programmation fonctionnelle : les types de données complexes sont gérés par des fonctions sans état.

Ces fonctions sont agréables et faciles à intégrer avec d'autres patterns, elles peuvent être appelées de n'importe où dans votre application ; par exemple, d'un modèle de vue si vous utilisez MVVM, ou peut-être d'une saga si vous utilisez Redux ou un autre système de type Flux, ou lors du traitement d'une commande ou d'un événement dans le contexte de CQRS et de l'event sourcing. Bien entendu, la logique peut également être mise à disposition très facilement en tant que service, par exemple via l'API Web de Microsoft - les fonctions de service sont sans état, ce qui convient parfaitement.

Suppression des cours en POO?


Enfin, une seule question demeure : avons-nous encore besoin de cours ? La réponse dépend de la plate-forme de votre choix. Sur .NET, par exemple, la classe au niveau du CLR est essentielle. Cependant, avec F#, par exemple, il est possible de construire des logiciels complexes sans utiliser directement les classes. De manière typiquement fonctionnelle, les types de données complexes sont créés dans F# sous forme d'unions discriminées. Bien sûr, F# fonctionne sur le CLR et utilise donc le système de types .NET en interne, mais le développeur n'y est pas confronté.

Dans d'autres environnements, il est tout à fait possible de ne pas utiliser de classes. Dans ECMA JavaScript ou TypeScript, par exemple, il est bien sûr possible d'utiliser des classes. Cependant, dans ces langages, un objet n'a pas nécessairement besoin d'une classe formelle. Même si vous êtes un fan du typage explicite, Flow est un système qui vous permet d'y parvenir sans TypeScript du tout, et heureusement sans classes.

Je ne veux pas faire de recommandation directe sur cette question - les classes peuvent certainement être un bon moyen technique de modélisation des données, même si le comportement est implémenté ailleurs. Toutefois je recommande le cours de Matthieu Gaston sur la Programmation orienté Objet.