Pourquoi la plupart des architectes système insistent-ils sur la première programmation sur une interface ?

StackOverflow https://stackoverflow.com/questions/48605

Question

Presque tous les livres Java que j'ai lus parlent de l'utilisation de l'interface comme moyen de partager l'état et le comportement entre des objets qui, une fois "construits", ne semblaient pas partager de relation.

Cependant, chaque fois que je vois des architectes concevoir une application, la première chose qu’ils font est de commencer à programmer une interface.Comment ça se fait?Comment connaître toutes les relations entre les objets qui se produiront au sein de cette interface ?Si vous connaissez déjà ces relations, pourquoi ne pas simplement étendre une classe abstraite ?

Était-ce utile?

La solution

Programmer sur une interface signifie respecter le « contrat » créé en utilisant cette interface.Et donc si votre IPoweredByMotor l'interface a un start() méthode, les futures classes qui implémentent l'interface, qu'elles soient MotorizedWheelChair, Automobile, ou SmoothieMaker, en implémentant les méthodes de cette interface, ajoutez de la flexibilité à votre système, car un morceau de code peut démarrer le moteur de nombreux types de choses différents, car tout ce qu'un morceau de code a besoin de savoir, c'est qu'il répond à start().Cela n'a pas d'importance comment ils commencent, juste qu'ils il faut commencer.

Autres conseils

Excellente question.je vous renvoie à Josh Bloch dans Java efficace, qui écrit (point 16) pourquoi préférer l'utilisation d'interfaces aux classes abstraites.D’ailleurs, si vous n’avez pas ce livre, je vous le recommande vivement !Voici un résumé de ce qu’il dit :

  1. Les classes existantes peuvent être facilement adaptées pour implémenter une nouvelle interface. Tout ce que vous avez à faire est d’implémenter l’interface et d’ajouter les méthodes requises.Les classes existantes ne peuvent pas être facilement adaptées pour étendre une nouvelle classe abstraite.
  2. Les interfaces sont idéales pour définir des mix-ins. Une interface mixte permet aux classes de déclarer un comportement supplémentaire et facultatif (par exemple, Comparable).Il permet de mélanger la fonctionnalité facultative avec la fonctionnalité principale.Les classes abstraites ne peuvent pas définir de mix-ins : une classe ne peut pas étendre plus d'un parent.
  3. Les interfaces permettent des cadres non hiérarchiques. Si vous disposez d’une classe qui possède les fonctionnalités de nombreuses interfaces, elle peut toutes les implémenter.Sans interfaces, vous devriez créer une hiérarchie de classes gonflée avec une classe pour chaque combinaison d'attributs, ce qui entraînerait une explosion combinatoire.
  4. Les interfaces permettent des améliorations sécurisées des fonctionnalités. Vous pouvez créer des classes wrapper à l’aide du modèle Decorator, une conception robuste et flexible.Une classe wrapper implémente et contient la même interface, transmettant certaines fonctionnalités aux méthodes existantes, tout en ajoutant un comportement spécialisé aux autres méthodes.Vous ne pouvez pas faire cela avec des méthodes abstraites - vous devez plutôt utiliser l'héritage, qui est plus fragile.

Qu’en est-il de l’avantage des classes abstraites fournissant une implémentation de base ?Vous pouvez fournir une classe d'implémentation squelettique abstraite avec chaque interface.Cela combine les vertus des interfaces et des classes abstraites.Les implémentations squelettiques fournissent une assistance à l'implémentation sans imposer les contraintes sévères qu'imposent les classes abstraites lorsqu'elles servent de définitions de types.Par exemple, le Cadre de collecte définit le type à l'aide d'interfaces et fournit une implémentation squelettique pour chacune d'entre elles.

La programmation sur les interfaces offre plusieurs avantages :

  1. Obligatoire pour les modèles de type GoF, tels que le modèle de visiteur

  2. Permet des implémentations alternatives.Par exemple, plusieurs implémentations d'objets d'accès aux données peuvent exister pour une seule interface qui résume le moteur de base de données utilisé (AccountDaoMySQL et AccountDaoOracle peuvent tous deux implémenter AccountDao)

  3. Une classe peut implémenter plusieurs interfaces.Java ne permet pas l'héritage multiple de classes concrètes.

  4. Détails de mise en œuvre des résumés.Les interfaces peuvent inclure uniquement des méthodes API publiques, masquant les détails d'implémentation.Les avantages incluent une API publique clairement documentée et des contrats bien documentés.

  5. Largement utilisé par les frameworks d'injection de dépendances modernes, tels que http://www.springframework.org/.

  6. En Java, les interfaces peuvent être utilisées pour créer des proxys dynamiques - http://java.sun.com/j2se/1.5.0/docs/api/java/lang/reflect/Proxy.html.Cela peut être utilisé très efficacement avec des frameworks tels que Spring pour effectuer une programmation orientée aspect.Les aspects peuvent ajouter des fonctionnalités très utiles aux classes sans ajouter directement de code Java à ces classes.Des exemples de ces fonctionnalités incluent la journalisation, l'audit, la surveillance des performances, la démarcation des transactions, etc. http://static.springframework.org/spring/docs/2.5.x/reference/aop.html.

  7. Implémentations simulées, tests unitaires - Lorsque les classes dépendantes sont des implémentations d'interfaces, des classes fictives peuvent être écrites pour implémenter également ces interfaces.Les classes fictives peuvent être utilisées pour faciliter les tests unitaires.

Je pense que l'une des raisons pour lesquelles les classes abstraites ont été largement abandonnées par les développeurs pourrait être un malentendu.

Quand le Bande de quatre a écrit:

Programmez vers une interface et non une implémentation.

il n’existait pas d’interface Java ou C#.Ils parlaient du concept d’interface orientée objet, que possède chaque classe.Erich Gamma le mentionne dans cet entretien.

Je pense que suivre toutes les règles et principes mécaniquement sans réfléchir conduit à une base de code difficile à lire, à naviguer, à comprendre et à maintenir.Souviens-toi:La chose la plus simple qui puisse fonctionner.

Comment ça se fait?

Parce que c'est ce que disent tous les livres.Comme les modèles GoF, de nombreuses personnes le considèrent comme universellement bon et ne se demandent jamais si c'est vraiment le bon design ou non.

Comment connaître toutes les relations entre les objets qui se produiront au sein de cette interface ?

Ce n’est pas le cas, et c’est un problème.

Si vous connaissez déjà ces relations, alors pourquoi ne pas simplement prolonger une classe abstraite?

Raisons de ne pas étendre une classe abstraite :

  1. Vous avez des implémentations radicalement différentes et créer une classe de base décente est trop difficile.
  2. Vous devez graver votre seule et unique classe de base pour autre chose.

Si aucun des deux ne s’applique, continuez et utilisez une classe abstraite.Cela vous fera gagner beaucoup de temps.

Questions que vous n'avez pas posées :

Quels sont les inconvénients de l’utilisation d’une interface ?

Vous ne pouvez pas les changer.Contrairement à une classe abstraite, une interface est gravée dans le marbre.Une fois que vous en avez un utilisé, son extension cassera le code, point final.

Ai-je vraiment besoin de l’un ou l’autre ?

La plupart du temps, non.Réfléchissez très bien avant de créer une hiérarchie d'objets.Un gros problème dans des langages comme Java est qu’il rend beaucoup trop facile la création de hiérarchies d’objets massives et compliquées.

Prenons l'exemple classique que LameDuck hérite de Duck.Cela semble facile, n'est-ce pas ?

Eh bien, jusqu'à ce que vous deviez indiquer que le canard a été blessé et qu'il est maintenant boiteux.Ou indiquez que le canard boiteux a été guéri et peut à nouveau marcher.Java ne vous permet pas de modifier le type d'un objet, donc utiliser des sous-types pour indiquer une boiterie ne fonctionne pas réellement.

La programmation à une interface signifie respecter le "contrat" ​​créé en utilisant cette interface

C’est la chose la plus mal comprise à propos des interfaces.

Il n’existe aucun moyen de faire respecter un tel contrat avec les interfaces.Les interfaces, par définition, ne peuvent spécifier aucun comportement.Les cours sont le lieu où le comportement se produit.

Cette croyance erronée est si répandue qu’elle est considérée par de nombreuses personnes comme une idée reçue.C’est pourtant faux.

Donc cette déclaration dans le PO

Presque tous les livres Java que je lis parlent d'utiliser l'interface comme un moyen de partager l'état et le comportement entre les objets

n'est tout simplement pas possible.Les interfaces n'ont ni état ni comportement.Ils peuvent définir les propriétés que les classes d'implémentation doivent fournir, mais c'est aussi proche que possible.Vous ne pouvez pas partager un comportement à l'aide d'interfaces.

Vous pouvez supposer que les gens implémenteront une interface pour fournir le type de comportement impliqué par le nom de ses méthodes, mais ce n'est pas la même chose.Et cela n'impose aucune restriction sur le moment où de telles méthodes sont appelées (par exemple, Start doit être appelé avant Stop).

Cette déclaration

Obligatoire pour les modèles de type GoF, tels que le modèle de visiteur

est également incorrect.Le livre du GoF n’utilise exactement aucune interface, car elles ne faisaient pas partie des fonctionnalités des langages utilisés à l’époque.Aucun des modèles ne nécessite d'interfaces, bien que certains puissent les utiliser.OMI, le modèle Observer est un modèle dans lequel les interfaces peuvent jouer un rôle plus élégant (bien que le modèle soit normalement implémenté à l'aide d'événements de nos jours).Dans le modèle Visiteur, il est presque toujours nécessaire qu'une classe Visiteur de base implémentant un comportement par défaut pour chaque type de nœud visité soit requise, IME.

Personnellement, je pense que la réponse à cette question est triple :

  1. Les interfaces sont considérées par beaucoup comme une solution miracle (ces personnes travaillent généralement sous le sentiment d'une mauvaise compréhension du "contrat", ou pensent que les interfaces découplent comme par magie leur code)

  2. Les utilisateurs de Java sont très concentrés sur l'utilisation de frameworks, dont beaucoup nécessitent (à juste titre) des classes pour implémenter leurs interfaces.

  3. Les interfaces étaient le meilleur moyen de faire certaines choses avant l'introduction des génériques et des annotations (attributs en C#).

Les interfaces sont une fonctionnalité linguistique très utile, mais elles sont très mal utilisées.Les symptômes incluent :

  1. Une interface n'est implémentée que par une seule classe

  2. Une classe implémente plusieurs interfaces.Souvent présenté comme un avantage des interfaces, cela signifie généralement que la classe en question viole le principe de séparation des préoccupations.

  3. Il existe une hiérarchie d'héritage d'interfaces (souvent reflétée par une hiérarchie de classes).C'est la situation que vous essayez d'éviter en utilisant des interfaces en premier lieu.Trop d'héritage est une mauvaise chose, tant pour les classes que pour les interfaces.

Toutes ces choses sont des odeurs de code, OMI.

C'est une façon de promouvoir le lâche couplage.

Avec un faible couplage, un changement dans un module ne nécessitera pas un changement dans la mise en œuvre d'un autre module.

Une bonne utilisation de ce concept est Modèle d'usine abstraite.Dans l'exemple Wikipédia, l'interface GUIFactory produit une interface Button.La fabrique concrète peut être WinFactory (produisant WinButton) ou OSXFactory (produisant OSXButton).Imaginez si vous écrivez une application GUI et que vous devez parcourir toutes les instances de OldButton classe et les changer en WinButton.Puis l'année prochaine, vous devrez ajouter OSXButton version.

À mon avis, on voit cela si souvent parce que c’est une très bonne pratique qui est souvent appliquée dans de mauvaises situations.

Les interfaces présentent de nombreux avantages par rapport aux classes abstraites :

  • Vous pouvez changer d'implémentation sans reconstruire le code qui dépend de l'interface.Ceci est utile pour :classes proxy, injection de dépendances, AOP, etc.
  • Vous pouvez séparer l'API de l'implémentation dans votre code.Cela peut être bien car cela rend évident lorsque vous modifiez du code qui affectera d'autres modules.
  • Il permet aux développeurs qui écrivent du code dépendant de votre code de se moquer facilement de votre API à des fins de test.

Vous tirez le meilleur parti des interfaces lorsque vous traitez des modules de code.Cependant, il n’existe pas de règle simple pour déterminer où doivent se situer les limites des modules.Il est donc facile d’abuser de cette bonne pratique, en particulier lors de la première conception de certains logiciels.

Je suppose (avec @eed3s9n) que c'est pour promouvoir un couplage lâche.De plus, sans interfaces, les tests unitaires deviennent beaucoup plus difficiles, car vous ne pouvez pas modéliser vos objets.

Pourquoi l'extension est mauvaise.Cet article est à peu près une réponse directe à la question posée.Je ne vois presque aucun cas où vous pourriez réellement besoin une classe abstraite et de nombreuses situations où c'est une mauvaise idée.Cela ne signifie pas que les implémentations utilisant des classes abstraites sont mauvaises, mais vous devrez faire attention à ne pas rendre le contrat d'interface dépendant des artefacts d'une implémentation spécifique (exemple concret :la classe Stack en Java).

Encore une chose :il n'est pas nécessaire, ni recommandé, d'avoir des interfaces partout.En règle générale, vous devez identifier quand vous avez besoin d’une interface et quand vous n’en avez pas besoin.Dans un monde idéal, le deuxième cas devrait être implémenté comme cours final la plupart du temps.

Il y a d'excellentes réponses ici, mais si vous cherchez une raison concrète, ne cherchez pas plus loin que les tests unitaires.

Supposons que vous souhaitiez tester une méthode dans la logique métier qui récupère le taux d’imposition actuel pour la région dans laquelle une transaction a lieu.Pour ce faire, la classe de logique métier doit communiquer avec la base de données via un référentiel :

interface IRepository<T> { T Get(string key); }

class TaxRateRepository : IRepository<TaxRate> {
    protected internal TaxRateRepository() {}
    public TaxRate Get(string key) {
    // retrieve an TaxRate (obj) from database
    return obj; }
}

Tout au long du code, utilisez le type IRepository au lieu de TaxRateRepository.

Le référentiel dispose d'un constructeur non public pour encourager les utilisateurs (développeurs) à utiliser la fabrique pour instancier le référentiel :

public static class RepositoryFactory {

    public RepositoryFactory() {
        TaxRateRepository = new TaxRateRepository(); }

    public static IRepository TaxRateRepository { get; protected set; }
    public static void SetTaxRateRepository(IRepository rep) {
        TaxRateRepository = rep; }
}

La fabrique est le seul endroit où la classe TaxRateRepository est directement référencée.

Vous avez donc besoin de quelques classes de support pour cet exemple :

class TaxRate {
    public string Region { get; protected set; }
    decimal Rate { get; protected set; }
}

static class Business {
    static decimal GetRate(string region) { 
        var taxRate = RepositoryFactory.TaxRateRepository.Get(region);
        return taxRate.Rate; }
}

Et il existe également une autre implémentation d'IRepository - la maquette :

class MockTaxRateRepository : IRepository<TaxRate> {
    public TaxRate ReturnValue { get; set; }
    public bool GetWasCalled { get; protected set; }
    public string KeyParamValue { get; protected set; }
    public TaxRate Get(string key) {
        GetWasCalled = true;
        KeyParamValue = key;
        return ReturnValue; }
}

Étant donné que le code en direct (Business Class) utilise une Factory pour obtenir le référentiel, dans le test unitaire, vous branchez le MockRepository pour le TaxRateRepository.Une fois la substitution effectuée, vous pouvez coder en dur la valeur de retour et rendre la base de données inutile.

class MyUnitTestFixture { 
    var rep = new MockTaxRateRepository();

    [FixtureSetup]
    void ConfigureFixture() {
        RepositoryFactory.SetTaxRateRepository(rep); }

    [Test]
    void Test() {
        var region = "NY.NY.Manhattan";
        var rate = 8.5m;
        rep.ReturnValue = new TaxRate { Rate = rate };

        var r = Business.GetRate(region);
        Assert.IsNotNull(r);
        Assert.IsTrue(rep.GetWasCalled);
        Assert.AreEqual(region, rep.KeyParamValue);
        Assert.AreEqual(r.Rate, rate); }
}

N'oubliez pas que vous souhaitez tester uniquement la méthode de logique métier, pas le référentiel, la base de données, la chaîne de connexion, etc...Il existe différents tests pour chacun d’eux.En procédant de cette façon, vous pouvez isoler complètement le code que vous testez.

Un avantage secondaire est que vous pouvez également exécuter le test unitaire sans connexion à une base de données, ce qui le rend plus rapide et plus portable (pensez à une équipe multi-développeurs dans des sites distants).

Un autre avantage secondaire est que vous pouvez utiliser le processus de développement piloté par les tests (TDD) pour la phase de mise en œuvre du développement.Je n'utilise pas strictement TDD mais un mélange de TDD et de codage old-school.

Dans un sens, je pense que votre question se résume à simplement: "Pourquoi utiliser les interfaces et non les cours abstraits?" Techniquement, vous pouvez obtenir un couplage lâche avec les deux - l'implémentation sous-jacente n'est toujours pas exposée au code d'appel, et vous pouvez utiliser un modèle d'usine abstrait pour renvoyer une implémentation sous-jacente (implémentation d'interface vs.extension de classe abstraite) pour augmenter la flexibilité de votre conception.En fait, vous pourriez affirmer que les classes abstraites vous en donnent un peu plus, puisqu'elles vous permettent à la fois d'exiger des implémentations pour satisfaire votre code ("vous DEVEZ implémenter start()") et de fournir des implémentations par défaut ("J'ai un paint() standard que vous peut remplacer si vous le souhaitez") -- avec les interfaces, des implémentations doivent être fournies, ce qui, au fil du temps, peut conduire à des problèmes d'héritage fragiles via des modifications d'interface.

Fondamentalement, cependant, j'utilise des interfaces principalement en raison de la restriction d'héritage unique de Java.Si mon implémentation DOIT hériter d'une classe abstraite à utiliser en appelant du code, cela signifie que je perds la flexibilité d'hériter de quelque chose d'autre même si cela peut avoir plus de sens (par ex.pour la réutilisation de code ou la hiérarchie d'objets).

L’une des raisons est que les interfaces permettent la croissance et l’extensibilité.Supposons, par exemple, que vous ayez une méthode qui prend un objet comme paramètre,

Public Void Drink (Coffee Somedrink) {

}

Supposons maintenant que vous souhaitiez utiliser exactement la même méthode, mais en passant un objet hotTea.Eh bien, vous ne pouvez pas.Vous venez de coder en dur la méthode ci-dessus pour utiliser uniquement des objets café.Peut-être que c'est bien, peut-être que c'est mauvais.L'inconvénient de ce qui précède est qu'il vous enferme strictement avec un seul type d'objet lorsque vous souhaitez transmettre toutes sortes d'objets associés.

En utilisant une interface, disons IHotDrink,

interface IHotDrink { }

et réécrire votre méthode ci-dessus pour utiliser l'interface au lieu de l'objet,

Public Void Drink (ihotdrink Somedrink) {

}

Vous pouvez désormais transmettre tous les objets qui implémentent l'interface IHotDrink.Bien sûr, vous pouvez écrire exactement la même méthode qui fait exactement la même chose avec un paramètre d’objet différent, mais pourquoi ?Vous maintenez soudainement un code volumineux.

Il s’agit de concevoir avant de coder.

Si vous ne connaissez pas toutes les relations entre deux objets après avoir spécifié l'interface, alors vous avez mal défini l'interface, ce qui est relativement facile à corriger.

Si vous vous êtes plongé directement dans le codage et avez réalisé à mi-chemin qu'il vous manque quelque chose, c'est beaucoup plus difficile à corriger.

Vous pouvez voir cela d'un point de vue perl/python/ruby :

  • lorsque vous passez un objet en paramètre à une méthode, vous ne transmettez pas son type, vous savez simplement qu'il doit répondre à certaines méthodes

Je pense que considérer les interfaces Java comme une analogie expliquerait mieux cela.Vous ne transmettez pas vraiment un type, vous transmettez simplement quelque chose qui répond à une méthode (un trait, si vous voulez).

Je pense que la principale raison d'utiliser des interfaces en Java est la limitation à l'héritage unique.Dans de nombreux cas, cela entraîne des complications inutiles et une duplication de code.Jetez un œil aux traits de Scala : http://www.scala-lang.org/node/126 Les traits sont un type particulier de classes abstraites, mais une classe peut en étendre plusieurs.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top