Concevoir une application en programmation orientée objet
Cet article fait suite à une série d’expériences que j’ai pu avoir dans la gestion de projets de petite taille en informatique. Il présente les quelques points importants qui dirigent actuellement ma vision de la conception d’une application, notamment en programmation orientée objet, et qui me permettent d’appréhender les problèmes qui peuvent être rencontrés. Ces règles de “bonne conception” sont les résultats d’analyses personnelles sur les raisons des succès ou des échecs de certaines solutions conceptuelles que j’ai été amené à utiliser et sont également le reflet de ma vision actuelle de la programmation orientée objet.
1. Prototyper dans le but de réajuster la conception.
Même avec plusieurs années d’expériences, il est impossible de concevoir efficacement une application du premier coup, sans tester le modèle par un prototype. À l’inverse, il ne faut pas non plus se lancer dans le prototypage sans avoir réfléchi suffisamment à la solution. Un équilibre est donc à trouver.
On effectue tout d’abord une phase de conception relativement conséquente qui a pour buts de décomposer l’application en sous-ensembles et de dégager les problèmes principaux en termes d’intégration et de communication entre ces sous-ensembles. Cette phase entraîne la réalisation d’un prototype à l’échelle de la conception, c’est-à-dire qui met en œuvre les différentes parties de l’application et la communication entre ces parties, sans rentrer davantage dans le détail. Ce prototype permet de valider ou de remettre en question la première solution trouvée. S’enchaînent ensuite une série de raffinements de conception et de prototypes qui amène à une conception valide, qui répond au problème. Les différents prototypes réalisés permettent de surcroît de tester une première fois l’intégration de chaque sous-ensemble et peuvent servir de base pour l’élaboration de procédures de test plus spécifiques.
2. Ne pas perdre de vue que le langage orientée objet est avant tout un modèle.
Le langage objet s’apparente au langage machine mais y ajoute des structures abstraites compréhensibles par l’Homme. Il s’apparente donc plutôt à un modèle qu’à une mise en œuvre du programme, même si ce modèle a une granularité suffisamment fine pour que sa traduction en langage machine soit complètement définie. Lors de la conception d’une application, il faut utiliser le plus possible ces spécificités du modèle (typage, héritage, …) pour décrire de la manière la plus précise et la plus cohérente possible l’application.
3. Utiliser au maximum les notions de programmation orientée objet.
Si l’application doit être réalisée en programmation orientée objet, autant que la conception utilise ce modèle au maximum ! Sans tomber dans l’excès, il ne faut pas hésiter à définir des classes propres à l’application plutôt que d’utiliser les structures génériques du langage. Classiquement, si le nom d’un article d’un magasin peut être représenté par une simple chaîne de caractères (String), sa référence est plus abstraite : il peut s’agir d’un numéro, d’une série de caractères alphabétiques ou encore d’un mélange de caractères de toutes sortes. Une classe spéciale pour désigner cet attribut est alors tout à fait justifiée.
La programmation orientée objet apporte en plus des solutions pour résoudre des problèmes de conception. Les modèles de conception existants (Design Patterns) permettent de se rendre compte de l’étendue des possibilités. L’héritage permet notamment d’ajouter des attributs et méthodes spécifiques à une classe, et donc de définir un ensemble de types qui correspondent précisément aux besoins. La surcharge peut quant à elle ajouter des comportements ou des fonctionnalités de manière transparente.
4. Donner du sens à chaque structure et chaque choix conceptuel.
Une classe possède des propriétés et des fonctionnalités qui permettent d’agir sur son comportement interne. De la même façon qu’une voiture ne sait pas où et comment se garer sans conducteur, un objet n’a pas à savoir comment s’enregistrer dans une base de données ou un fichier. De plus, les propriétés d’une classe sont supposées définir les objets complètement : un attribut qui n’est jamais instancié ou utilisé dans un certain cas d’utilisation doit être remis en question. Aussi, la décomposition en classes et méthodes doit éviter de donner des rôles superflus à des classes.
Enfin, si une classe a pour objectif de n’être instanciée qu’une seule fois, autant définir ses attributs et méthodes de manière statique ! Cela évite les erreurs de multiple instanciation et les lourdeurs d’utilisation.
5. Considérer et utiliser les interfaces comme des contrats.
En programmation orientée objet une interface a pour objectif d’indiquer qu’une classe respecte un certain contrat et peut être utilisée de la manière prévue. La définition d’une interface permet donc d’envisager d’autres implémentations pour une certaine fonctionnalité, sans modifier les signatures des méthodes. On peut ainsi utiliser une interface plutôt qu’une classe pour définir une entrée ou sortie d’une méthode si on envisage à terme la généricité de cette dernière. De plus, l’utilisation d’une interface a peu d’impact en terme de performance puisque la vérification du respect du contrat qu’elle définit est effectuée à la compilation.
6. Choisir les structures de collections génériques en leur donnant un sens.
Seule la méthode ou la classe qui gère un groupe de données de même type est supposée connaître la structure précise de ce groupe. Du point de vue du modèle, et donc des entrées et sorties de cette méthode ou cette classe, seules les propriétés de ce groupe doivent être indiquées : est-il ordonné ? Y a-t-il possibilité de doublons ? Comment s’effectue l’accès aux données ? De cette manière, il n’y a que l’information pertinente qui est indiquée. De plus, s’il y a changement de l’implémentation pour une quelconque raison, celle-ci est transparente pour l’extérieur.
Le langage Java possède par exemple une multitude d’interfaces permettant de décrire des propriétés spécifiques pour un groupe de données, sans dévoiler la classe spécifique qui l’implémente. Pour indiquer qu’une méthode renvoie un groupe d’objets de manière générale on utilisera l’interface Collection. Si on veut préciser que chaque objet n’apparaît qu’une fois dans le groupe, on écrira Set, et si la collection est ordonnée alors on pourra employer List.
7. Décomposer l’application en modules aux rôles clairement définis et aux entrées et sorties génériques.
Une application met généralement en jeu plusieurs sous-ensembles de fonctionnalités. La décomposition de cette application par ces sous-ensembles a plusieurs conséquences bénéfiques :
- parallélisation du travail par séparation de l’équipe en petites groupes ;
- planification, conception et développement indépendants ;
- développements autonomes en matière de technologies.
Cependant, pour pouvoir réaliser cette décomposition, la définition précise des rôles et limites de chaque module ainsi que les structures de données qui permettent la communication entre les modules est nécessaire. Il faut notamment veiller à ce que les entrées et les sorties soient, dans la mesure du possible et selon les contraintes d’intégration et de coût (ressources, performances…), les plus génériques possibles, c’est-à-dire les plus indépendantes du langage de programmation. Cela permet alors de modifier éventuellement la partie communication entre les modules en réalisant par exemple une partie de l’application en code natif, ou une autre disponible sous forme de service Web, sans avoir à redéfinir un ensemble de classes ayant pour objectif d’adapter les structures choisies au protocole de communication. On préfèrera donc l’utilisation des types classiques tels que les entiers ou les chaînes de caractères et les classes qui ne sont que des agrégations de ces types. On évitera plus particulièrement l’utilisation de structures de collections propres au langage. On utilisera donc plutôt des échanges de données sous forme de tableaux. On peut cependant s’autoriser quelques libertés dans des cas spécifiques.
8. Ne pas résoudre un besoin technique ou un problème par un biais conceptuel.
Le cas est typique : lors de la phase de développement on s’aperçoit qu’il manque une certaine fonctionnalité pour une classe ou un module. Il faut alors chercher une solution pour répondre à ce manque. Si la solution trouvée est une “astuce”, c’est-à-dire l’utilisation d’une partie de l’application (module, classe, méthode, attribut…) d’une manière différente (même légèrement) de ce pourquoi elle a été pensée et conçue, alors il faut très certainement continuer à chercher ! En effet, très généralement une “astuce” en entraîne une autre et la solution finale n’a plus aucun sens au niveau du modèle. Reprendre la conception, éventuellement le prototypage, et modifier le code existant pour suivre la nouvelle conception n’est pas du temps de perdu, à plus forte raison pour des applications de grande taille ou qui exigent une certaine robustesse.
9. Ne pas se contenter d’une solution à moitié convaincante et ne pas mélanger deux solutions fonctionnelles.
Une solution qui n’est pas formalisée complètement, donc une solution partielle, ne doit en aucun cas servir de base pour le développement de l’application. À coup sûr des problèmes apparaîtront et l’invalideront. Elle servira éventuellement – et seulement – de support pour la réalisation d’un premier prototype.
Si deux solutions pour un même problème sont valides, il faut éviter de prendre les avantages de chacune et en faire une combinaison. Le risque est de rendre deux sous-parties incompatibles et de trouver alors des solutions conceptuelles qui finalement rendent la qualité du modèle final inférieure à celle des modèles initiaux. En somme, lorsque plusieurs solutions sont possibles, on évitera de les mélanger sans précaution.
10. Ce qui a fonctionné pour une application ne fonctionnera pas pour une autre.
C’est la règle de base. Il n’y a pas de solution conceptuelle magique qui fonctionne pour tout les cas de figure. Même s’il existe des architectures qui peuvent être appliquées à beaucoup de cas, il subsiste toujours des exceptions. Il faut donc à chaque nouveau projet passer par une phase de conception conséquente pour éviter de remettre en cause la solution à chaque problème rencontré.
le 13 janvier 2008 à 19:17
I don’t have any idea about your code, but in my case I solved the problem by removing the Log object from my callee. As I explained in the post, http://www.jroller.com/hashem/entry/jboss_as_seam_spring_rmi,
My remote object contained a logger property, It wasn’t a must for my code, so I removed it and it worked properly. I think an alternative approach is to use the same Log class in both sides (in both remote and client side) .
le 13 janvier 2008 à 20:44
I finally fixed the problem using the same method as you: I removed an useless Log attribute in a class. Thank you for your blog post, I think I’d have spent few more hours on the problem otherwise