Je concède que je suis un peu jusqu'au boutiste - mais c'est principalement parce que je crois sincèrement qu'on peut, en tant qu'architecte logiciel et programmeur, se débarrasser de nos mauvaises habitudes de "passer outre certaines règles histoire d'optimiser le temps de développement ou l'application". J'ai tendance à pousser les concepts à bout, histoire de voir ce qui va en sortir. Généralement, c'est très payant dans le sens ou une architecture correcte diminue énormément le temps de développement et la taille du code à écrire (d'après mon expérience). Mais je m'éloigne effectivement du sujet...
Sinon, cf. le post de pseudocode pour les éclaircissements que je n'ai pas pensé à apporter.
Si je veux être plus clair : dans le paradigme OO, un objet se compose de deux choses : des propriétés et un état. Ces deux éléments sont intrinsèque à l'objet. Ensuite, l'objet offre des services, qui permettent entre autre de modifier son état en fonction de l'action demandée et des propriétés de l'objet.
Si on sépare la notion d'état de la notion d'objet, nous ne somme plus dans le paradigme OO mais dans le paradigme procédural. L'état n'est plus une donnée intrinsèquement liée à un objet mais un ensemble de grandeurs qui sont manipulées par des procédures. La loi de Demeter n'est clairement pas applicable dans ce cas, et l'encapsulation qu'on peut faire est plus que limitée...
Ouh la la. Plein de chose, d'après moi. Si on reprends l'exemple typique que j'ai donné plus haut (un manager == collection + contrôle de la durée de vie + factory), alors découpler ces trois fonctions me permet de:
1) étendre le système en prévoyant de nouvelles factory, et par exemple adapter les factory aux possibilités du système sur lequel s'exécute le programme. Une ressource material peut, selons la carte graphique, récupérer un shader différent ou charger des textures différentes.
2) étendre le système en prévoyant un contrôle de durée de vie différent - tous les programmes n'ont pas besoin du même type de contrôle. Si je prends l'exemple d'un jeu, la gestion de la durée de vie des ressources est très différents selon que toutes les données sont persistantes dans le monde ou non (exemple : une créature tuée disparait du niveau, ou reste en place jusqu'à ce que le niveau soit déchargé). Je peux aussi adapter le contrôle de durée de vie en fonction des ressources que mon "manager" est censé traiter.
3) toute collection n'est pas nécessairement équivalente. Si pour certaines ressources je peux me satisfaire d'un lookup en O(n) (très peu d'accès aléatoire, et ces derniers ne sont pas pénalisant en terme de temps - un std::vector<> ; exemple typique : la gestion de tâche dans un scheduler), d'autres ressources peuvent avoir besoin d'un tout autre type d'accès (accès aléatoire fréquent, par exemple l'accès à une texture qui pourrait se faire via un std::map<> en C++).
4) certaines parties de mon soft n'ont aucune raison de connaître la façon dont je contrôle la durée de vie de mes ressources ; d'autres parties ne sont pas intéressé par l'aspect stockage ; etc. En découplant les différentes parties de mon manager, je diminue les dépendances entre mes modules, ce qui me permet de simplifier et leur développement, et leur maintenance. Ce qui est un point important selon moi.
Si les trois premiers points vous semblent très YAGNIsant, le 4ème point devrait quand même vous faire tilter un peu. Notre métier, ce n'est pas tant écrire du code que de répondre à un service ; et pour ça, on doit écrire du code qui peut évoluer et qui peut avoir besoin de corrections. Plus le code est découplé, plus on gagnera du temps à répondre à ces deux besoins, et donc plus on gagnera d'argent (si l'argent rentre en ligne de compte, bien sûr). Et tout le monde est content
Oui. Un design plus adapté serait de passer par le pattern visiteur. En tout état de cause, il ne faut certainement pas injecter update() et draw() dans la classe entity ; et on a tout intérêt à éviter les dynamic_cast lorsqu'on le peut (note : le visiteur acyclique de R. C. Martin peut nécessiter des dynamic_cast<>, mais le visiteur étant nécessairement couplé avec la classe qu'il visite, cela n'a pas d'influence négative en termes de design).
Je vais être encore un peu hors sujet mais je ne suis pas tellement d'accord avec toi - il n'y a selon moi aucun lien sémantique entre la "gestion" d'une entité et le fait de savoir si il y a encore besoin de la "gérer".
Plus généralement, il n'y a strictement aucune raison pour que le contrôle de durée de vie d'une ressource soit uniquement lié au contrôle de la liste des ressources. Il n'y a même aucune raison pour que ce dernier soit utilisateur du premier. Dans certains cas, c'est même l'inverse : c'est le contrôle de la durée de vie qui va décider si telle ou telle ressource est encore valide (exemple : une liste de connexions TCP/IP + un vérificateur de la validité des connexions). Dans certains cas extrême, ce contrôle inversé par rapport à ta vision est même asynchrone. La même vision peut être utilisée pour tout ce qui concerne la création des ressources (donc le coté factory du manager). Si j'ai une thread de streaming qui génère les ressources, c'est à elle de connaître ma liste de ressources ; ce n'est pas à ma collection de ressources de connaître ma thread de streaming - ça n'aurait pas vraiment de sens...
Donc sans même parler de la violation évidente du SRP (malgré ce que tu dis ; le fait que trois fonctions distinctes cohabitent dans une classe est une violation du SRP, même si tu penses sincèrement que les trois ne peuvent évoluer qu'ensemble), il y a u problème lié à l'inversion des dépendances dans l'idée même d'un manager omnipotent.
Le fait que le nombre de classe se multiplie n'est pas un problème en soit, étant donné que (grosso-modo, aux prologues près), la somme de code reste identique. Comme tu le dit, c'est du déplacement de code, mais ce n'est pas un déplacement de code gratuit : il a du sens.
Considérant les informations incomplète que tu m'a fourni concernant ton design, je te propose cette idée, et je te pose une question : y voit-tu des inconvénients majeurs ?
A noter que ce design n'est certainement pas parfait (c'est une tentative de présentation de solution en quelques minutes). L'idée de base est d'avoir une classe message_dispatcher semblable à celle que j'ai présenté dans ma série d'article sur une interface C++ à l'API windows ou sur gamedev.net ici. Ainsi, les classes entity_factory et entity_remover sont aussi capable de recevoir des messages et d'en émettre. Ce design, relativement simple, respecte les principes principaux dont nous n'arrêtons pas de parler(*), et permet l'application de la loi de Demeter de manière naturelle. entity_factory/entity_remover s'enregistrent en tant que récepteurs de messages au niveau de la classe plus générique message_dispatcher. On peut ainsi avoir plusieurs factory et plusieurs remover pour gérer les cas plus complexes liés à la création/libération de ressources.
Si en plus ce n'est pas au manager de créer les entités qu'il gère, on se heurte à un problème dès lors qu'il s'agit pour lui de les libérer. Dans ton design, changer la policy de destruction des ressources nécessite de modifier la classe manager ; cette modification peut avoir un impact sur le code existant et entraîner des régressions dommageable à ton produit. Ton code a donc une viscosité élevée. Dans le design que je propose, il suffit d'ajouter un message et une classe remover, ce qui n'a aucun impact sur la contrôle des ressources lui même et sur le reste du code. Impossible d'introduire une régression puisqu'on ne modifie pas le code existant, et ce dernier à une viscosité moindre.
Ce que je note aussi, c'est que (tu le reconnais toi même, même si tu le minimise) le SRP n'est pas respecté et que du coup, tu n'arrives pas à respecter la loi de Demeter. C'est un point important - parce qu'après mure réflexion, le respect du SRP est nécessaire au respect de la loi de Demeter - sans lui l'application a loi de Demeter va mener à des choses trop étranges pour être décrites ici
Bref, tout ça pour dire que plus on réduit le couplage entre les classes, plus il est aisé de respecter SRP et les autres principes, plus le design s'ouvre, et plus il devient naturel d'utiliser la loi de Demeter. On peut me rétorquer que ça risque de faire plein de petites classes, mais ce n'est pas un mal en soi - au contraire. Ca simplifie d'autant plus la maintenance du projet.
Après, chacun voit midi à sa porte. Je suis conscient que l'architecture OO n'est pas une science - c'est plus un art qu'autre chose. Chaque personne a une vision qui lui est propre et une approche des problèmes qui lui est propre. Je me contente d'expliquer mon approche, qui peut ne pas convenir à tout le monde, mais qui (à mon sens) a le mérite de bien mettre en exergue les relations entre les classes et de promouvoir un design léger, fait de petites classes relativement indépendantes les unes des autres, dont certaines seront (peut être un jour) aisément réutilisables dans d'autres projets.
---------
(*) a propos du respect des principes dans cet exemple...
OCP : on peut créer autant de type d'entité que l'on veut ; il faudra bien évidemment créer les visiteurs correspondants. Si les classes entity_factory et entity_remover héritent de classes de base plus générales, alors on peut aussi étendre le design au niveau des factory et remover sans avoir à modifier l'existant. A noter que le pattern visiteur a un peu de mal avec le principe ouvert/fermé - c'est la raison pour laquelle j'ai préféré le visiteur acyclique, moins contraignant, mais nécessitant d'utiliser dynamic_cast<> (ce qui n'est pas catastrophique, car le visiteur est nécessairement fortement couplé avec l'objet qu'il visite ; cf mon article V comme Visiteur)
LSP : pas forcément facile à voir ; toutes les entités sont traitées de la même manière, dès lors qu'on les manipule via l'interface de entity.
SRP : les liens définis entre les classes ainsi que les fonctions de chacune des classes présentées permettent de s'assurer que ce principe est respecté.
ISP : aucune des classes présentée ne dépend de classes ou d'interface dont elle n'ont pas l'utilité. Les interfaces sont donc clairement séparées.
DIP : on le voit clairement - les liens se font principalement sur des abstractions. entity dépends de visitor et message_dispatcher ; entity_collection dépends de entity ; ... Les seuls liens entre classes plus concrètes sont ceux qui on du sens (entity_message_dispatcher dispatche des messages sur une collection d'entité ; pas sur une collection de grenouilles afghanes). En un sens, c'est logique : ce sont là les classes métier, tandis que les autres classes, plus abstraites, n'ont pas véritablement de notion métier qui leur serait attachée.
Partager