Marco la baraque
1003Messages postés
9 mai 2008Date d'inscription
25 déc. 2008 à 22:48
Bonsoir,
Pour compléter ce que dit Walkhess, l'utilisation d'interfaces et de classes abstraites, ça a deux avantages.
- la factorisation de code. Selon certains, la factorisation est l'un des avantages de la programmation orientée objet, selon d'autres (et j'adhère à ce mouvement de pensée), la factorisation ça ne sert à rien, sauf parfois à concevoir de mauvais programme par souci de "sur-factorisation".
La factorisation, ça consiste à écrire des méthodes dans les classes, de manière à pouvoir les réutiliser sans les retaper dans les classes filles. C'est ce qui est utilisé lorsque tu implémentes des méthodes dans ta classe abstraite : si toutes les classes filles ont le même comportement pour cette méthode, alors elle doit être implémentée une et une seule fois dans la classe abstraite
- l'abstraction (le polymorphisme). C'est la manière d'utiliser une référence vers une interface (qui définit une série de méthodes), et d'utiliser une implémentation concrète, mais sans la connaître.
Par exemple, quand tu vas utiliser une liste (interface java.util.List), tu ne vas pas connaître quelle implémentation tu vas utiliser. Tu vas par contre savoir que tu peux insérer un élément à un endroit, en supprimer un, connaître la taille...
Et donc dans ton code, tu vas utiliser une référence vers une List.
Ensuite, tu vas utiliser réellement une implémentation de cette interface, mais d'un point de vue utilisation, tu ne sauras pas ce que c'est. Que ce soit une LinkedList (suppression et insertion en O(n) par exemple), ou une ArrayList(en temps constant pour ces méthodes), ça ne t'intéresse que lorsque tu compares les performances lors de l'implémentation initiale de ton ensemble. Une fois que c'est fait, tu fais abstraction de cet aspect "bas-niveau" pour te concentrer uniquement sur l'utilisation de ta liste, et donc tu passes par ton interface.
En espérant avoir été un peu plus loin, sans avoir été trop confus...
Cordialement
Merci de ton avis.
J'avoue avoir été un peu extrême en affirmant que "la factorisation ne sert à rien". J'entendais simplement :
"il ne faut pas créer de classe abstraite uniquement par souci de factorisation".
En effet, dans ce cas, on augmente le risque de bugs en voulant rendre générique du code similaire (certes si tu as 50 fois la même méthode, c'est qu'il y a un problème dans ta conception), et on crée des classes abstraites qui n'ont absolument aucun intérêt ni technique ni fonctionnel, car elles servent juste à centraliser du code.
Pour moi la notion d'héritage est bien plus riche que ça : on doit avant tout hériter d'un type, puis ensuite on hérite (et éventuellement défini ou redéfini) de ses méthodes. C'est d'ailleurs ce que tu expliques dans ton exemple avec les formes : tes objets étendent ta classe abstraite car ils sont similaire d'un point de vue comportement (un carré ou un rectangle c'est une figure 2D, donc elles étendent ta classe Figure2D), et non pas uniquement pour factoriser le code.
Si tu veux uniquement factoriser du code, tu peux utiliser un design pattern Wrapper, ce sera d'autant plus propre que tu ne "perdra" pas ton héritage (dans de nombreux langages objets l'héritage multiple n'existe pas, et étendre une classe abstraite uniquement pour factoriser du code fera que ta classe ne pourra plus étendre de classe par la suite, ce qui peut être gênant si tu souhaites étendre un type et un comportement par la suite).
Cordialement,
je sais que ce post date un peu maintenant, mais pour tous ceux qui tomberaient dessus par hasard comme moi, il peut être utile d'apporter encore quelques précisions.
Une interface, c'est avant tout un contrat, à savoir un ensemble de propriétés et méthodes que la classe DOIT implémenter.
Une classe abstraite, c'est avant tout une classe permettant de factoriser un ensemble de propriétés et de méthodes communes, voire même un constructeur commun.
C'est bien utile dans le cadre de l'écriture de plugins par exemple. Cela permet de définir des méthodes de construction et d'appel communes, permettant ainsi à l'application parente de ne connaitre que les points d'entrée de la classe abstraite.
Pour parler UML :
- L'interface permet de définir les Boundary (frontières) de la classe finale.
- La classe abstraite permet de définir les CU (Cas d'utilisation) simplifiées.
- La classe finale permet de définir les CU optimisées, prenant en compte les CU abstraites et concrètes.
Je me permet de remonter ce poste... Car je suis, disons le, assez choqué par l'intervention de "Marco la baraque"!
Je reprend vos termes..
***
En effet, dans ce cas, on augmente le risque de bugs en voulant rendre générique du code similaire (certes si tu as 50 fois la même méthode, c'est qu'il y a un problème dans ta conception), et on crée des classes abstraites qui n'ont absolument aucun intérêt ni technique ni fonctionnel, car elles servent juste à centraliser du code.
***
Je donne un exemple simple d'implémentation, juste technique, de centralisation de code avec classes Abstraites...
J'ai besoin de définir des objets pour un projet personnel comme tel : Les données ci dessous sont à titre d'exemple...
Objet A : variables : String name, String phoneNumber
Objet B : variables : String name, String insuranceNumber
Objet C : variables : String name, ImageIcon image, String productId
Objet D variables : String name, ImageIcon image, String comment
Cas avec factorisation via classes Abstraites ca donne :
Toutes les classes ci dessous ont juste les getter et setter pour les variables qu'elles portent en public
Abstract class AbstractBeanWithName : String name
Abstract class AbstractBeanWithImage extends AbstractBeanWithName : ImageIcon image
BeanWithPhoneNumber extends AbstractBeanWithName
BeanWithInsuranceNumber extends AbstractBeanWithName
BeanWithProductIdNumber extends AbstractBeanWithImage
BeanWithCommentNumber extends AbstractBeanWithImage
- Les variables ont le même nom partout, uniformité...
- Si je veux savoir quels objets ont une image, je recherche les références à AbstractBeanWithImage (gain de temps énorme)
- Gain de place énorme.. si on connait son architecture, on sait de suite quelles variables sont héritées
- Maintenabilité parfaite..., besoin d'une vérification de saisie sur une variable... je peux la mettre au niveau du setter, exemple bidon mais probant, une seule méthode à retoucher
-- j'ai besoin un jour d'un objet avec juste un nom, je retire Abstract de ma classe AbstractBeanWithName qui devient BeanWithName... , même chose si un jour besoin d'un objet avec nom et image... AbstractBeanWithImage devient BeanWithImage
- Si un traitement est particulier à l'un des objets... par exemple le nom sur BeanWithPhoneNumber et qu'il existe déjà un traitement sur ce setter que tous les autres objets utilisent, l'override du setter est possible... Cas super rare si la spec est bien faite au départ... l'analyse, mais une seule méthode à créer...
Cas sans facto :
Object A : variables : String name, String phoneNumber
Object B : variables : String name, String insuranceNumber
Object C : variables : String name, ImageIcon image, String productId
Object D variables : String name, ImageIcon image, String comment
- Les variables peuvent finir avec des noms différents.. ex : nom, name, data_name (ce qui arrive souvent, si peu de factorisation du code... et ca devient totalement illisible)
- Je dois ouvrir toutes les classes pour savoir lesquelles portent une image... puisque chaque classe pouvant être codée par des dev différents, les noms des variables peuvent changer...
- Classe 4 fois plus grande que dans l'autre cas... donc scrolling... et il faut ouvrir tout pour savoir qui porte quel type de variable, méthodes, même si identique (name) (image)
- Maintenabilité faible, si une modification est à reporter partout, cas de la vérification de saisie sur un setter, je dois parcourir toutes les classes et faire un copier collé bête et méchant (perte de temps, risque d'oubli, sans compter le noms des méthodes et des variables qui peuvent changer) setNom, setName etc....
-- j'ai besoin un jour d'un objet avec juste un nom, je rajoute une classe... risque de changer de nom de variable... etc etc , même chose si un jour besoin d'un objet avec nom et image...
- Si un traitement est particulier à l'un des objets... je l'implémente dans son coin
Bref, d'expérience, sur les projets non factorisés... sans aller dans l'extrême que j'ai exposé... Qui reste cependant totalement valable et probant, puisque là, c'était juste une factorisation purement technique... On obtient :
- Des classes codées par des devs différents, qui font la même chose... car manque de connaissance de l'architecture du projet
- Des variables à l'utilité identique... String nom, qui changent name, nom, data_name, illisible, impossible à facilement retrouver dans le code, à part tout parcourir
- Problème de performance, car certaines méthodes ayant le même but, la même fonction, vont changer de nom, d'algorithme, impossible à retrouver... parfois on fait une correction dans un coin et oh zut, un abrutis à fait la même chose avec un autre nom, seulement, je dois me taper 40 lignes de code, pour bien vérifier que c'est la même
La factorisation est l'a-panache de la perfection en programmation...
Gain de temps, Maintenabilité, lisibilité --> GML lol
***
Pour moi la notion d'héritage est bien plus riche que ça : on doit avant tout hériter d'un type, puis ensuite on hérite (et éventuellement défini ou redéfini) de ses méthodes. C'est d'ailleurs ce que tu expliques dans ton exemple avec les formes : tes objets étendent ta classe abstraite car ils sont similaire d'un point de vue comportement (un carré ou un rectangle c'est une figure 2D, donc elles étendent ta classe Figure2D), et non pas uniquement pour factoriser le code.
***
Vrai et Faux...
Vrai, si vous instanciez Figure2D... uniquement! et donc que celle ci ait une raison de l'être!
Faux, dans le sens ou... une classe abstraite trouve son utilité quand vous avez des objets dont l'implémentation de certaines méthodes différent... dessiner() comme l'a si bien exposé "misterETS", mais que des méthodes et/ou variables sont communes static ou pas...
Si Figure2D, n'est qu'un agrégat de méthodes et variables communes aux objet à dessiner en 2d, mais que chacun d'eux se dessine différemment, Figure2D n'a aucune raison d'être instanciée en temps que tel, puisqu'elle ne porte que la description de la méthode dessiner... Figure2D ne dessinant rien en soit même, on la met en Abstract... Logique des plus basique!
CQFD : Une classe abstraite est donc par nature une classe permettant de factoriser du code et de permettre tout de même une divergence de comportement de ses enfants... C'est donc un outil très puissant, mais qu'il faut savoir manier... Ce qui est encore appuyé par le fait que les variables définies dans une classe abstraite peuvent être public, protected, private en opposition à une interface dont les variables ne peuvent être que public...
Ainsi que par le pattern Template...
Une classe abstraite est aussi et comme son nom l'indique, utile pour abstraire un code client des spécificités des classes filles de cette classe abstraite...
Si une méthode reçoit en argument une classe concrète (instanciable) dans un code "client", toute modification des particularités de cette classe seront à prendre en compte dans le code "client", ainsi que si il y a ajout d'une classe concrète de la même famille...
Si maintenant celle ci reçoit en argument la Classe abstraite mère d'une famille de classes concrètes... le code client s'abstrait de leur particularités, sera plus facilement maintenable et la modification, création de classes concrètes de cette même famille (héritant la même classe abstraite passé ici en argument) ne demandera retouche du code client, que si, il y a ajout de nouvelle fonctionnalités (méthodes) utiles... dans son cas ;) (Pattern Abstract Factory)
****
Si tu veux uniquement factoriser du code, tu peux utiliser un design pattern Wrapper, ce sera d'autant plus propre que tu ne "perdra" pas ton héritage (dans de nombreux langages objets l'héritage multiple n'existe pas, et étendre une classe abstraite uniquement pour factoriser du code fera que ta classe ne pourra plus étendre de classe par la suite, ce qui peut être gênant si tu souhaites étendre un type et un comportement par la suite).
****
Le design pattern Wrapper est très proche de l'adapter et au final est plus proche de la spécialisation que de l'héritage, donc à l'opposer fondamentalement de la factorisation de code...
De plus, je rappel que les classes abstraites peuvent hériter d'autre classes.... Votre explication ne fait que malheureusement souligner votre manque de connaissances en la matière et risque justement d'induire en erreur des développeurs et de provoquer une faible maintenabilité, ainsi qu'un nombre de classes exponentielles... par rapport au besoin!
En espérant éclaircir le sujet pour les futurs lecteurs, a bon entendeur ;)