Les classes / méthodes abstraites sont-elles obsolètes?

37

J'avais l'habitude de créer beaucoup de classes / méthodes abstraites. Puis j'ai commencé à utiliser des interfaces.

Maintenant, je ne sais pas si les interfaces ne rendent pas les classes abstraites obsolètes.

Vous avez besoin d'un cours totalement abstrait? Créez plutôt une interface. Vous avez besoin d'une classe abstraite avec une implémentation? Créez une interface, créez une classe. Hériter de la classe, implémenter l'interface. Un avantage supplémentaire est que certaines classes peuvent ne pas avoir besoin de la classe parente, mais simplement implémenter l'interface.

Alors, les classes / méthodes abstraites sont-elles obsolètes?

Boris Yankov
la source
Et si votre langage de programmation choisi ne supporte pas les interfaces? Il me semble que c'est le cas pour C ++.
Bernard
7
@Bernard: en C ++, une classe abstraite est une interface dans tous les noms, sauf name. Le fait qu'ils puissent également faire plus que des interfaces «pures» n'est pas un inconvénient.
gbjbaanb
@ gbjbaanb: Je suppose. Je ne me souviens pas de les utiliser comme interfaces, mais plutôt de fournir des implémentations par défaut.
Bernard le
Les interfaces sont la "devise" des références d'objet. De manière générale, ils constituent le fondement du comportement polymorphe. Les classes abstraites ont un but différent que deadalnix a parfaitement expliqué.
Jiggy
4
N’est-ce pas dire que les modes de transport sont obsolètes maintenant que nous avons des voitures? Oui, la plupart du temps, vous utilisez une voiture. Mais que vous ayez besoin d'autre chose que d'une voiture ou non, il ne serait pas vraiment correct de dire "je n'ai pas besoin d'utiliser les modes de transport". Une interface est bien la même que d' une classe abstraite sans aucune mise en œuvre, et avec un nom spécial, non?
Jack V.

Réponses:

111

Non.

Les interfaces ne peuvent pas fournir une implémentation par défaut, les classes abstraites et la méthode peuvent. Ceci est particulièrement utile pour éviter la duplication de code dans de nombreux cas.

C'est également un très bon moyen de réduire le couplage séquentiel. Sans méthodes / classes abstraites, vous ne pouvez pas implémenter de modèle de méthode. Je vous suggère de consulter cet article de Wikipédia: http://fr.wikipedia.org/wiki/Template_method_pattern

Deadalnix
la source
3
@deadalnix La méthode de modèle peut être très dangereuse. Vous pouvez facilement vous retrouver avec un code hautement couplé et commencer à pirater le code pour étendre la classe abstraite basée sur un modèle afin de gérer "un seul cas de plus".
quant_dev
9
Cela ressemble plus à un anti-modèle. Vous pouvez obtenir exactement la même chose avec la composition par rapport à une interface. La même chose s'applique aux classes partielles avec certaines méthodes abstraites. Plutôt que de forcer le code client à sous-classer et à le remplacer, l'implémentation devrait être injectée . See: fr.wikipedia.org/wiki/Composition_over_inheritance#Avantages
back2dos
4
Cela n'explique pas du tout comment réduire le couplage séquentiel. Je conviens qu'il existe plusieurs moyens pour atteindre cet objectif, mais répondez au problème dont vous parlez au lieu d'invoquer aveuglément un principe. Vous finirez par faire de la programmation culte du fret si vous ne pouvez pas expliquer pourquoi cela est lié au problème actuel.
deadalnix
3
@deadalnix: Dire que le couplage séquentiel est le mieux réduit avec le modèle de modèle est la programmation culte du fret (utiliser le modèle d'état / stratégie / usine peut aussi bien fonctionner), tout comme l'hypothèse qu'il doit toujours être réduit. Le couplage séquentiel est souvent une simple conséquence d’exposer un contrôle très fin sur quelque chose, ne voulant rien dire qu’un compromis. Cela ne veut toujours pas dire que je ne peux pas écrire de wrapper sans couplage séquentiel avec composition. En fait, cela facilite beaucoup les choses.
back2dos
4
Java a maintenant des implémentations par défaut pour les interfaces. Est-ce que cela change la réponse?
raptortech97
15

Une méthode abstraite existe afin que vous puissiez l'appeler à partir de votre classe de base mais l'implémenter dans une classe dérivée. Donc, votre classe de base sait:

public void DoTask()
{
    doSetup();
    DoWork();
    doCleanup();
}

protected abstract void DoWork();

C'est un moyen relativement agréable d'implémenter un trou dans le motif du milieu sans que la classe dérivée connaisse les activités d'installation et de nettoyage. Sans méthodes abstraites, vous auriez à compter sur la classe dérivée pour implémenter DoTasket se rappeler d’appeler base.DoSetup()et base.DoCleanup()tout le temps.

modifier

Merci également à deadalnix d’avoir posté un lien vers le modèle de méthode , ce que j’ai décrit ci-dessus sans connaître le nom. :)

Scott Whitlock
la source
2
+1, je préfère votre réponse à deadalnix '. Notez que vous pouvez implémenter une méthode de modèle de manière plus simple à l'aide de délégués (en C #):public void DoTask(Action doWork)
Joh
1
@Joh - vrai, mais ce n'est pas nécessairement aussi agréable, selon votre API. Si votre classe de base est Fruitet que votre classe dérivée est Apple, vous souhaitez appeler myApple.Eat(), pas myApple.Eat((a) => howToEatApple(a)). En outre, vous ne voulez Applepas avoir à appeler base.Eat(() => this.howToEatMe()). Je pense qu'il est plus simple de remplacer une méthode abstraite.
Scott Whitlock
1
@ Scott Whitlock: Votre exemple d'utilisation de pommes et de fruits est un peu trop éloigné de la réalité pour juger si le recours à l'héritage est préférable aux délégués en général. Évidemment, la réponse est "ça dépend", cela laisse donc beaucoup de place aux débats ... Quoi qu'il en soit, je trouve que les méthodes de template sont très utilisées lors du refactoring, par exemple lors de la suppression du code dupliqué. Dans de tels cas, je ne veux généralement pas me mêler de la hiérarchie des types et je préfère éviter l'héritage. Ce type d’opération est plus facile avec les lambdas.
Joh
Cette question illustre bien plus qu'une simple application du modèle Méthode de modèle. Les aspects public / non public sont connus dans la communauté C ++ en tant que modèle d'interface non virtuelle . En ayant une fonction publique non virtuelle qui enveloppe la fonction virtuelle - même si au départ, la première ne fait qu'appeler le second - vous laissez un point de personnalisation pour la configuration / nettoyage, la journalisation, le profilage, les contrôles de sécurité, etc. qui pourraient être nécessaires ultérieurement.
Tony
10

Non, ils ne sont pas obsolètes.

En fait, il existe une différence obscure mais fondamentale entre les classes / méthodes abstraites et les interfaces.

si l'ensemble des classes dans lesquelles l'une d'elles doit être utilisée a un comportement commun qu'elles partagent (classes apparentées, je veux dire), optez pour Classes / méthodes abstraites.

Exemple: commis, agent, directeur - toutes ces classes ont en commun CalculateSalary (), utilisent des classes de base abstraites.CalculerSalary () peut être implémenté différemment, mais il existe certaines autres choses comme GetAttendance (), par exemple, qui a une définition commune dans la classe de base.

Si vos classes n'ont rien de commun (classes sans rapport, dans le contexte choisi) entre elles mais a une action qui est très différente dans la mise en œuvre, optez pour Interface.

Exemple: vache, banc, voiture, classes apparentées au télésope, mais Isortable peut être là pour les trier dans un tableau.

Cette différence est généralement ignorée lorsqu’elle est abordée dans une perspective polymorphe. Mais personnellement, j’ai le sentiment qu’il ya des situations où l’un est plus apte que l’autre pour la raison exposée ci-dessus.

WinW
la source
6

Outre les autres bonnes réponses, il existe une différence fondamentale entre les interfaces et les classes abstraites que personne n'a mentionnée spécifiquement, à savoir que les interfaces sont beaucoup moins fiables et imposent donc une charge de test beaucoup plus lourde que les classes abstraites. Par exemple, considérons ce code C #:

public abstract class Frobber
{
    private Frobber() {}
    public abstract void Frob(Frotz frotz);
    private class GreenFrobber : Frobber
    { ... }
    private class RedFrobber : Frobber
    { ... }
    public static Frobber GetFrobber(bool b) { ... } // return a green or red frobber
}

public sealed class Frotz
{
    public void Frobbit(Frobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Je suis assuré qu'il n'y a que deux chemins de code que je dois tester. L’auteur de Frobbit peut s’appuyer sur le fait que le frobber est rouge ou vert.

Si à la place nous disons:

public interface IFrobber
{
    void Frob(Frotz frotz);
}
public class GreenFrobber : IFrobber
{ ... }
public class RedFrobber : Frobber
{ ... }

public sealed class Frotz
{
    public void Frobbit(IFrobber frobber)
    {
         ...
         frobber.Frob(this);
         ...
    }
}

Je ne sais absolument plus rien des effets de cet appel à Frob. Je dois m'assurer que tout le code de Frobbit est robuste contre toute implémentation possible d'IFrobber , même celle d'implémentations par des personnes incompétentes (mauvaises) ou activement hostiles envers moi ou mes utilisateurs (bien pire).

Les classes abstraites vous permettent d'éviter tous ces problèmes; Utilise les!

Eric Lippert
la source
1
Le problème dont vous parlez devrait être résolu en utilisant des types de données algébriques, plutôt que de plier les classes à un point où elles violeraient le principe ouvert / fermé.
back2dos
1
Premièrement, je n'ai jamais dit que c'était bon ou moral . Tu mets ça dans ma bouche. Deuxièmement (c’est ce que je veux dire): ce n’est qu’une piètre excuse pour une fonctionnalité linguistique manquante. Enfin, il existe une solution sans classes abstraites: pastebin.com/DxEh8Qfz . Contrairement à cela, votre approche utilise des classes imbriquées et abstraites, en lançant tout sauf le code exigeant la sécurité dans une grosse boule de boue. Il n’ya aucune bonne raison pour que RedFrobber ou GreenFrobber soit lié aux contraintes que vous souhaitez appliquer. Il augmente le couplage et verrouille de nombreuses décisions sans aucun avantage.
back2dos
1
Dire que les méthodes abstraites sont obsolètes est une erreur sans aucun doute, mais prétendre qu’elles devraient être préférées aux interfaces est un mauvais guide. Ils sont simplement un outil pour résoudre des problèmes différents des interfaces.
Groo
2
"il y a une différence fondamentale [...] les interfaces sont beaucoup moins fiables [...] que les classes abstraites". Je ne suis pas d'accord, la différence que vous avez illustrée dans votre code repose sur des restrictions d'accès, qui, selon moi, n'ont aucune raison de différer de manière significative entre les interfaces et les classes abstraites. C'est peut-être le cas en C #, mais la question est indépendante de la langue.
Joh
1
@ back2dos: J'aimerais simplement souligner que la solution d'Eric utilise en fait un type de données algébrique: une classe abstraite est un type somme. L'appel d'une méthode abstraite est identique à la recherche de modèle sur un ensemble de variantes.
Rodrick Chapman
4

Vous le dites vous-même:

Vous avez besoin d'une classe abstraite avec une implémentation? Créez une interface, créez une classe. Hériter de la classe, implémenter l'interface

cela semble beaucoup de travail comparé à «hériter de la classe abstraite». Vous pouvez travailler pour vous-même en abordant le code dans une perspective puriste, mais je trouve que j’en ai déjà assez à faire sans essayer d’ajouter à ma charge de travail sans aucun avantage pratique.

gbjbaanb
la source
Sans oublier que si la classe n'hérite pas de l'interface (ce qui n'a pas été mentionné, elle devrait l'être), vous devez écrire des fonctions de transfert dans certaines langues.
Sjoerd
Umm, le "travail supplémentaire" supplémentaire que vous avez mentionné est que vous avez créé une interface et que les classes l'ont implémentée - cela vous semble-t-il vraiment beaucoup de travail? En plus de cela, bien sûr, l'interface vous donne la possibilité de l'implémenter à partir d'une nouvelle hiérarchie qui ne fonctionnerait pas avec une classe de base abstraite.
Bill K
4

Comme je l'ai commenté sur @deadnix post: Les implémentations partielles sont un anti-modèle, malgré le fait que le modèle de modèle les formalise.

Une solution propre à cet exemple wikipedia du modèle de modèle :

interface Game {
    void initialize(int playersCount);
    void makePlay(int player);
    boolean done();
    void finished();
    void printWinner();
}
class GameRunner {
    public void playOneGame(int playersCount, Game game) {
        game.initialize(playersCount);
        int j = 0;
        for (int i = 0; !game.finished(); i++)
             game.makePlay(i % playersCount);
        game.printWinner();
    }
} 
class Monopoly implements Game {
     //... implementation
}

Cette solution est préférable car elle utilise la composition au lieu de l'héritage . Le modèle de modèle introduit une dépendance entre l'implémentation des règles de monopole et l'implémentation de la manière dont les jeux doivent être exécutés. Cependant, ce sont deux responsabilités totalement différentes et il n’ya aucune bonne raison de les associer.

back2dos
la source
+1 Comme note de côté: "Les implémentations partielles sont un anti-modèle, malgré le fait que le modèle de gabarit les formalise". La description sur wikipedia définit le modèle proprement, seul l'exemple de code est "faux" (dans le sens où il utilise l'héritage lorsque ce n'est pas nécessaire et qu'il existe une alternative plus simple, comme illustré ci-dessus). En d'autres termes, je ne pense pas que le motif lui-même soit à blâmer, mais seulement la façon dont les gens ont tendance à le mettre en œuvre.
Joh
2

Même votre alternative proposée inclut l’utilisation de classes abstraites. De plus, comme vous n'avez pas spécifié le langage, je vais aller de l'avant et dire que le code générique est de toute façon la meilleure option que l'héritage fragile. Les classes abstraites présentent des avantages importants par rapport aux interfaces.

DeadMG
la source
-1. Je ne comprends pas ce que "code générique vs héritage" a à voir avec la question. Vous devriez illustrer ou justifier pourquoi "les classes abstraites présentent des avantages significatifs par rapport aux interfaces".
Joh
1

Les classes abstraites ne sont pas des interfaces. Ce sont des classes qui ne peuvent pas être instanciées.

Vous avez besoin d'un cours totalement abstrait? Créez plutôt une interface. Vous avez besoin d'une classe abstraite avec une implémentation? Créez une interface, créez une classe. Hériter de la classe, implémenter l'interface. Un avantage supplémentaire est que certaines classes peuvent ne pas avoir besoin de la classe parente, mais simplement implémenter l'interface.

Mais alors vous auriez une classe inutile non abstraite. Des méthodes abstraites sont nécessaires pour combler les lacunes dans les fonctionnalités de la classe de base.

Par exemple, étant donné cette classe

public abstract class Frobber {
    public abstract void Frob();

    public abstract boolean IsFrobbingNeeded { get; }

    public void FrobUntilFinished() {
        while (IsFrobbingNeeded) {
            Frob();
        }
    }
}

Comment implémenteriez-vous cette fonctionnalité de base dans une classe qui n'a ni Frob()ni IsFrobbingNeeded?

configurateur
la source
1
Une interface ne peut pas non plus être instanciée.
Sjoerd
@Sjoerd: Mais vous avez besoin d'une classe de base avec l'implémentation partagée. ça ne peut pas être une interface.
configurateur
1

Je suis un créateur de framework de servlet où les classes abstraites jouent un rôle essentiel. Je dirais plus, j'ai besoin de méthodes semi-abstraites, quand une méthode doit être remplacée dans 50% des cas et j'aimerais que le compilateur avertisse que cette méthode n'a pas été remplacée. Je résous le problème en ajoutant des annotations. Pour revenir à votre question, il existe deux cas d'utilisation différents de classes abstraites et d'interfaces, et personne n'est jusqu'à présent obsolète.

Dmitriy R
la source
0

Je ne pense pas qu'elles soient obsolètes par les interfaces, mais elles pourraient l'être aussi par le modèle de stratégie.

L'utilisation principale d'une classe abstraite consiste à reporter une partie de l'implémentation; pour dire, "cette partie de l'implémentation de la classe peut être différente".

Malheureusement, une classe abstraite oblige le client à le faire via l' héritage . Alors que le modèle de stratégie vous permettra d’obtenir le même résultat sans héritage. Le client peut créer des instances de votre classe plutôt que de toujours définir les leurs, et son "implémentation" (comportement) peut varier de manière dynamique. Le modèle de stratégie présente l'avantage supplémentaire de pouvoir modifier le comportement au moment de l'exécution, pas seulement au moment de la conception, et offre également un couplage nettement plus faible entre les types impliqués.

Dave Cousineau
la source
0

J'ai généralement rencontré beaucoup, beaucoup plus de problèmes de maintenance associés à des interfaces pures que des ABC, même des ABC utilisés avec un héritage multiple. YMMV - dunno, peut-être que notre équipe les a juste mal utilisés.

Cela dit, si nous utilisons une analogie du monde réel, dans quelle mesure y a-t-il des interfaces pures complètement dépourvues de fonctionnalité et d’état? Si j'utilise l'USB comme exemple, c'est une interface raisonnablement stable (je pense que nous sommes maintenant à l'USB 3.2, mais elle a également maintenu une compatibilité ascendante).

Pourtant, ce n'est pas une interface sans état. Ce n'est pas dépourvu de fonctionnalité. Cela ressemble plus à une classe de base abstraite qu'à une interface pure. C'est en fait plus proche d'une classe concrète avec des exigences fonctionnelles et d'état très spécifiques, la seule abstraction étant ce qui se branche sur le port étant la seule partie substituable.

Sinon, il s'agirait simplement d'un "trou" dans votre ordinateur avec un facteur de forme normalisé et des exigences fonctionnelles beaucoup plus souples qui ne feraient rien par lui-même jusqu'à ce que chaque fabricant propose son propre matériel pour faire en sorte que ce trou fasse quelque chose, puis cela devient un standard beaucoup plus faible et rien de plus qu'un "trou" et une spécification de ce qu'il doit faire, mais pas de disposition centrale sur la façon de le faire. En attendant, nous pourrions nous retrouver avec 200 façons différentes de le faire après que tous les fabricants de matériel essaient de trouver leurs propres moyens pour associer fonctionnalité et état à ce "trou".

Et à ce stade, certains fabricants pourraient présenter des problèmes différents de ceux des autres. Si nous devons mettre à jour la spécification, nous pourrions avoir 200 implémentations concrètes de ports USB avec des manières totalement différentes d'aborder la spécification, de la mettre à jour et de la tester. Certains fabricants peuvent développer de facto des implémentations standard qu'ils partagent entre eux (votre classe de base analogique implémentant cette interface), mais pas tous. Certaines versions peuvent être plus lentes que d'autres. Certains pourraient avoir un meilleur débit mais une latence plus faible ou vice versa. Certains pourraient utiliser plus de batterie que d'autres. Certains risquent de s'effondrer et de ne pas fonctionner avec tout le matériel censé fonctionner avec des ports USB. Certains pourraient nécessiter la mise en place d’un réacteur nucléaire, ce qui a tendance à donner à ses utilisateurs un empoisonnement par rayonnement.

Et c'est ce que j'ai trouvé, personnellement, avec des interfaces pures. Dans certains cas, ils peuvent avoir un sens, par exemple simplement pour modéliser le facteur de forme d’une carte mère par rapport à un boîtier de processeur. Les analogies des facteurs de forme sont en effet à peu près sans état et dépourvues de fonctionnalité, comme dans le cas du "trou" analogique. Mais j’estime souvent que c’est une grave erreur pour les équipes de considérer cela comme étant supérieur dans tous les cas, même pas proche.

Au contraire, je pense que beaucoup plus de cas seraient résolus par ABC que par les interfaces si ce sont les deux choix possibles, à moins que votre équipe soit si gigantesque qu’il soit souhaitable de disposer de l’analogique supérieure à 200 implémentations USB concurrentes plutôt qu’une norme centrale. maintenir. Dans une ancienne équipe dans laquelle je faisais partie, je devais en réalité me battre pour assouplir la norme de codage afin de permettre les ABC et l'héritage multiple, principalement en réponse aux problèmes de maintenance décrits ci-dessus.


la source