J'ai bien compris que tu ne doutais pas de la qualité du travail effectué par ces équipes, mais de celle du résultat obtenu.
koala01 (et Bousk aussi, d'après son dernier post), tu sembles bloquer de manière récurrente sur deux aspects :
- tu n'acceptes pas l'idée que les services offerts par les composantes de Agent doivent tous être répliqués dans Agent elle-même. La raison est que ces composantes sont des types du domaine métier et connus du client par ailleurs. Je l'ai bien précisé dans l'article. Et lorsqu'un type est connu du client, on doit considérer que chaque service offert par ce type (comprendre chaque service de son interface publique) peut être utilisé par ce client. C'est aussi simple que cela. Maintenant, le point que tu pourrais contester (tu l'as fait) est la légitimité du fait que les types de ces composantes soient utilisés par le client par ailleurs. Seulement, c'est une hypothèse ! Une hypothèse au sens de la logique traditionnelle. C'est mon exemple, donc j'en fixe les données, c'est normal. Si tu refuses l'hypothèse de travail, tu passes à autre chose. Mais si tu veux argumenter sur l'exemple principal de mon article, il faut en accepter les hypothèses.
- tu reproches à la conception alternative que je propose, le fait qu'elle expose sa structure interne au client. Mais elle ne l'expose pas, comme je l'ai expliqué dans le (*) de mon article.
Mais peut-être qu'en inversant le sens de ma démonstration, ces deux points te sembleront plus clairs :
Situation 1 :
---- code client ----
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 class Agent { public: string nom() const { return m_nom; } string affiche() const; void demarre() { m_comportement.demarre(); } void arrete() { m_comportement.arrete(); } string descriptionComportement() const { return m_comportement.description(); } bool connait(const Fait & fait) const { return m_savoir.connait(fait); } bool peutDeduire(const Fait & fait) const { return m_savoir.peutDeduire(fait); } private: Comportement m_comportement; Savoir m_savoir; string m_nom; };
Dans la situation 1, la classe Agent offre un accès direct à l'ensemble des services nécessaires pour gérer les Agents de ma simulation. Ca me parait pas mal, sauf que, à y regarder de plus près, on voit que l'ISP (http://www.tomdalling.com/blog/softw...ion-principle/) n'est pas respecté. On voit en effet que les services demarre, arrete et descriptionComportement semblent toutes relatives aux actions de l'agent, que connait et peutDeduire semblent relatives aux connaissances de l'Agent, et que ces deux groupes cohérents semblent indépendants les uns des autres. Le diagnostic est confirmé quand on réalise que les fonctions gereObstacles et afficheConnaissances n'utilisent chacune qu'un des groupes de services.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 Agent Luc; Luc.demarre(); Luc.affiche(); gereObstacles(Luc); // n'utilise que Agent::arrete afficheConnaissances(Luc); // n'utilise que Agent::connait et Agent::peutDeduire
La solution typique pour résoudre se problème est de créer autant d'interfaces spécifiques que de groupes identifiés, de faire hériter Agent de ces interfaces spécifiques, puis de modifier la signature des fonctions gereObstacles et afficheConnaissances de manière à ce qu'elles ne manipulent plus que ces interfaces spécifiques.
Dans le cas présent cependant, je remarque que l'interface publique de ces interfaces spécifiques à créer correspond exactement à celle des types Comportement et Savoir utilisés pour implémenter leurs services (modulo quelques raccourcis de noms). Comme ces types sont connus et utilisés par ailleurs par le client, il est inutile à ce stade de créer des interfaces mimant ces types publiques. J'ajoute donc simplement des accesseurs aux composantes concernées de classe Agent, j'ajoute les indirections qu'il faut dans le code client, je renomme certains appels, et je modifie la signature des fonctions gereObstacles et afficheConnaissances de manière à ce qu'elles ne manipulent plus que les abstractions spécifiques.
Ce qui nous donne la situation 2 :
---- code client ----
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 class Agent { public: string nom() const { return m_nom; } string affiche() const; Comportement & getComportement() { return m_comportement; } // de même pour const Savoir & getSavoir() { return m_savoir; } // de même pour const void demarre() { m_comportement.demarre(); } void arrete() { m_comportement.arrete(); } string descriptionComportement() const { return m_comportement.description(); } bool connait(const Fait & fait) const { return m_savoir.connait(fait); } bool peutDeduire(const Fait & fait) const { return m_savoir.peutDeduire(fait); } private: Comportement m_comportement; Savoir m_savoir; string m_nom; };
Dans cette situation 2,
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 Agent Luc; Luc.getComportement().demarre(); Luc.affiche(); gereObstacles(Luc.getComportement()); // n'utilise que Agent::arrete afficheConnaissances(Luc.getSavoir()); // n'utilise que Agent::connait et Agent::peutDeduire
- nous avons adressé le problème de l'ISP, et donc nous en tirons les avantages. Par rapport à la solution habituelle des interfaces spécifiques, nous avons évité la création de ces types supplémentaires, et nous avons introduit une indirection dans l'appel à certains services, permettant parfois un nommage plus clair.
- nous avons conservé l'indépendance du client vis-à-vis de la structure interne de la classe Agent. En effet, les indirections ne marquent qu'une structuration de l'interface publique de Agent. Du point de vue du client, les nouveaux accesseurs permettent d'accéder à des composantes de l'interface publique. Celles-ci correspondent pour l'instant aux composantes internes de Agent, mais c'est un détail technique. Si le besoin se fait sentir de modifier la structure interne de Agent (par exemple en fusionnant deux composantes internes) et qu'on ne souhaite pas modifier le code client, il suffira alors de créer les interfaces spécifiques évoquées plus haut, en les implémentant de manière adéquate dans Agent. Remarquons qu'il est peu probable qu'une restructuration interne se produise, puisque la structure interne actuelle reflète la structure de l'interface publique de Agent, laquelle découle de l'ISP, lequel ne s'applique que quand on isole des groupes stables de services.
Ma conclusion : la qualité logicielle a strictement augmenté en passant de la situation 1 à la situation 2.
Dans l'exemple de mon article, la situation 1 correspond à l'"après Déméter", et la 2 à l'"avant Déméter".
Il n'y a aucune différence, excepté le choix de quelques membres pour illustrer la démonstration.
Partager