191 votes

Que signifie "programmer en fonction des interfaces, pas des implémentations" ?

On tombe sur cette phrase en lisant sur les modèles de conception.

Mais je ne le comprends pas, quelqu'un pourrait-il m'expliquer ?

6 votes

190voto

this. __curious_geek Points 23728

Les interfaces ne sont que des contrats ou des signatures et elles ne connaissent pas rien sur les implémentations.

Le codage contre l'interface signifie que le code client contient toujours un objet d'interface fourni par une usine. Toute instance renvoyée par la fabrique sera du type Interface que toute classe candidate à la fabrique doit avoir implémenté. De cette façon, le programme client ne se soucie pas de l'implémentation et la signature de l'interface détermine toutes les opérations qui peuvent être effectuées. Ceci peut être utilisé pour changer le comportement d'un programme au moment de l'exécution. Cela vous aide également à écrire des programmes bien meilleurs du point de vue de la maintenance.

Voici un exemple de base pour vous.

public enum Language
{
    English, German, Spanish
}

public class SpeakerFactory
{
    public static ISpeaker CreateSpeaker(Language language)
    {
        switch (language)
        {
            case Language.English:
                return new EnglishSpeaker();
            case Language.German:
                return new GermanSpeaker();
            case Language.Spanish:
                return new SpanishSpeaker();
            default:
                throw new ApplicationException("No speaker can speak such language");
        }
    }
}

[STAThread]
static void Main()
{
    //This is your client code.
    ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
    speaker.Speak();
    Console.ReadLine();
}

public interface ISpeaker
{
    void Speak();
}

public class EnglishSpeaker : ISpeaker
{
    public EnglishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak English.");
    }

    #endregion
}

public class GermanSpeaker : ISpeaker
{
    public GermanSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak German.");
    }

    #endregion
}

public class SpanishSpeaker : ISpeaker
{
    public SpanishSpeaker() { }

    #region ISpeaker Members

    public void Speak()
    {
        Console.WriteLine("I speak Spanish.");
    }

    #endregion
}

alt text

Il ne s'agit que d'un exemple de base et l'explication réelle du principe est dépasse le cadre de cette réponse.

EDITAR

J'ai mis à jour l'exemple ci-dessus et ajouté un résumé Speaker classe de base. Dans cette mise à jour, j'ai ajouté une fonctionnalité à tous les haut-parleurs pour "SayHello". Tous les haut-parleurs disent "Hello World". C'est donc une fonctionnalité commune avec une fonction similaire. Référez-vous au diagramme de classe et vous trouverez que Speaker classe abstraite mettre en œuvre ISpeaker et marque l'interface Speak() comme abstrait, ce qui signifie que la mise en œuvre de chaque enceinte est responsable de l'implémentation de l'élément Speak() car elle varie de Speaker a Speaker . Mais tous les locuteurs disent "Bonjour" à l'unanimité. Donc dans la classe abstraite Speaker nous définissons une méthode qui dit "Hello World" et chaque Speaker dérivera l SayHello() méthode.

Considérons un cas où SpanishSpeaker ne peut pas dire "Bonjour", donc dans ce cas, vous pouvez remplacer l'option SayHello() pour le locuteur espagnol et soulève l'exception appropriée.

Veuillez noter que, nous avons n'avons pas apporté de modifications à l'interface ISpeaker. Et le code client et SpeakerFactory restent également inchangé. Et c'est ce que nous obtenons en De la programmation à l'interface .

Et nous pourrions obtenir ce comportement en ajoutant simplement une classe abstraite de base Speaker et quelques modifications mineures dans chaque implémentation, laissant ainsi le programme original inchangé. C'est une caractéristique souhaitée de toute application et cela rend votre application facilement maintenable.

public enum Language
{
    English, German, Spanish
}

public class SpeakerFactory
{
    public static ISpeaker CreateSpeaker(Language language)
    {
        switch (language)
        {
            case Language.English:
                return new EnglishSpeaker();
            case Language.German:
                return new GermanSpeaker();
            case Language.Spanish:
                return new SpanishSpeaker();
            default:
                throw new ApplicationException("No speaker can speak such language");
        }
    }
}

class Program
{
    [STAThread]
    static void Main()
    {
        //This is your client code.
        ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English);
        speaker.Speak();
        Console.ReadLine();
    }
}

public interface ISpeaker
{
    void Speak();
}

public abstract class Speaker : ISpeaker
{

    #region ISpeaker Members

    public abstract void Speak();

    public virtual void SayHello()
    {
        Console.WriteLine("Hello world.");
    }

    #endregion
}

public class EnglishSpeaker : Speaker
{
    public EnglishSpeaker() { }

    #region ISpeaker Members

    public override void Speak()
    {
        this.SayHello();
        Console.WriteLine("I speak English.");
    }

    #endregion
}

public class GermanSpeaker : Speaker
{
    public GermanSpeaker() { }

    #region ISpeaker Members

    public override void Speak()
    {
        Console.WriteLine("I speak German.");
        this.SayHello();
    }

    #endregion
}

public class SpanishSpeaker : Speaker
{
    public SpanishSpeaker() { }

    #region ISpeaker Members

    public override void Speak()
    {
        Console.WriteLine("I speak Spanish.");
    }

    public override void SayHello()
    {
        throw new ApplicationException("I cannot say Hello World.");
    }

    #endregion
}

alt text

23 votes

La programmation de l'interface n'est pas uniquement sur le type de la variable de référence. Cela signifie également que vous n'utilisez pas d'hypothèses implicites sur votre implémentation. Par exemple, si vous utilisez une List comme type, vous pourriez toujours supposer que l'accès aléatoire est rapide en appelant de manière répétée get(i) .

23 votes

Les usines sont orthogonales à la programmation aux interfaces, mais je pense que cette explication donne l'impression qu'elles en font partie.

0 votes

@Toon : d'accord avec vous. Je voulais fournir un exemple très basique et simple pour la programmation vers l'interface. Je ne voulais pas embrouiller le questionneur en implémentant l'interface IFlyable sur quelques classes d'oiseaux et d'animaux.

41voto

Vincent Ramdhanie Points 46265

Considérez une interface comme un contrat entre un objet et ses clients. C'est-à-dire que l'interface spécifie les choses qu'un objet peut faire, et les signatures pour accéder à ces choses.

Les mises en œuvre sont les comportements réels. Disons par exemple que vous avez une méthode sort(). Vous pouvez implémenter QuickSort ou MergeSort. Cela ne devrait pas avoir d'importance pour le code client qui appelle sort, tant que l'interface ne change pas.

Les bibliothèques telles que l'API Java et le .NET Framework font un usage intensif des interfaces car des millions de programmeurs utilisent les objets fournis. Les créateurs de ces bibliothèques doivent faire très attention à ne pas modifier l'interface des classes de ces bibliothèques, car cela affecterait tous les programmeurs utilisant la bibliothèque. En revanche, ils peuvent modifier l'implémentation autant qu'ils le souhaitent.

Si, en tant que programmeur, vous codez contre l'implémentation, dès qu'elle est modifiée, votre code cesse de fonctionner. Pensez donc aux avantages de l'interface de cette façon :

  1. il masque les éléments que vous n'avez pas besoin de connaître, ce qui rend l'objet plus simple à utiliser.
  2. il fournit le contrat sur la façon dont l'objet se comportera, de sorte que vous pouvez dépendre de lui

0 votes

Cela signifie que vous devez être conscient de ce à quoi vous contractez l'objet : dans l'exemple fourni, vous ne contractez qu'un tri, pas nécessairement un tri stable.

0 votes

De la même manière que la documentation des bibliothèques ne mentionne pas l'implémentation, il s'agit simplement de descriptions des interfaces des classes incluses.

28voto

Oded Points 271275

Cela signifie que vous devez essayer d'écrire votre code de manière à ce qu'il utilise une abstraction (classe ou interface abstraite) au lieu de l'implémentation directe.

Normalement, l'implémentation est injectée dans votre code par le biais du constructeur ou d'un appel de méthode. Ainsi, votre code connaît l'interface ou la classe abstraite et peut appeler tout ce qui est défini dans ce contrat. Lorsqu'un objet réel (mise en œuvre de l'interface/classe abstraite) est utilisé, les appels sont effectués sur l'objet.

Il s'agit d'un sous-ensemble de la Liskov Substitution Principle (LSP), le L de la SOLID principes.

Un exemple dans .NET serait de coder avec IList au lieu de List o Dictionary vous pouvez donc utiliser n'importe quelle classe qui implémente IList de manière interchangeable dans votre code :

// myList can be _any_ object that implements IList
public int GetListCount(IList myList)
{
    // Do anything that IList supports
    return myList.Count();
}

Un autre exemple tiré de la bibliothèque des classes de base (BCL) est la fonction ProviderBase classe abstraite - elle fournit une certaine infrastructure et, surtout, permet d'utiliser toutes les implémentations du fournisseur de manière interchangeable si vous codez en fonction de cette classe.

0 votes

Mais comment un client peut-il interagir avec une interface et utiliser ses méthodes vides ?

2 votes

Le client n'interagit pas avec l'interface, mais à travers l'interface :) Les objets interagissent avec d'autres objets par le biais de méthodes (messages) et une interface est une sorte de langage - lorsque vous savez qu'un certain objet (personne) implémente (parle) l'anglais (IList), vous pouvez l'utiliser sans avoir besoin d'en savoir plus sur cet objet (qu'il est aussi italien), car ce n'est pas nécessaire dans ce contexte (si vous voulez demander de l'aide, vous n'avez pas besoin de savoir qu'il parle aussi italien si vous comprenez l'anglais).

1 votes

BTW. Je pense que le principe de substitution de Liskov concerne la sémantique de l'héritage et n'a rien à voir avec les interfaces, que l'on trouve également dans les langages sans héritage (Go de Google).

6voto

Gabriel Ščerbák Points 7982

Cette déclaration concerne le couplage. Une raison potentielle d'utiliser la programmation orientée objet est la réutilisation. Ainsi, par exemple, vous pouvez répartir votre algorithme entre deux objets A et B qui collaborent. Cela peut être utile pour la création ultérieure d'un autre algorithme, qui pourrait réutiliser l'un ou l'autre des deux objets. Cependant, lorsque ces objets communiquent (envoient des messages - appellent des méthodes), ils créent des dépendances entre eux. Mais si l'on veut utiliser l'un sans l'autre, il faut spécifier ce que doit faire un autre objet C pour l'objet A si l'on remplace B. Ces descriptions sont appelées interfaces. Cela permet à l'objet A de communiquer sans changement avec un autre objet s'appuyant sur l'interface. La déclaration que vous avez mentionnée dit que si vous prévoyez de réutiliser une partie d'un algorithme (ou plus généralement d'un programme), vous devez créer des interfaces et vous appuyer sur elles, de sorte que vous pouvez changer l'implémentation concrète à tout moment sans changer les autres objets si vous utilisez l'interface déclarée.

6voto

Mathieson Points 516

Comme d'autres l'ont dit, cela signifie que votre code d'appel ne doit connaître qu'un parent abstrait, et NON la classe d'implémentation réelle qui effectuera le travail.

Ce qui aide à comprendre cela, c'est la raison pour laquelle vous devez toujours programmer en fonction d'une interface. Il existe de nombreuses raisons, mais deux des plus faciles à expliquer sont les suivantes

1) Test.

Disons que j'ai tout le code de ma base de données dans une seule classe. Si mon programme connaît la classe concrète, je ne peux tester mon code qu'en l'exécutant réellement contre cette classe. J'utilise -> pour signifier "parle à".

WorkerClass -> DALClass Cependant, ajoutons une interface au mélange.

WorkerClass -> IDAL -> DALClass.

Ainsi, la DALClass implémente l'interface IDAL, et la classe ouvrière n'appelle QUE par cette interface.

Maintenant, si nous voulons écrire des tests pour le code, nous pourrions plutôt créer une classe simple qui agit comme une base de données.

WorkerClass -> IDAL -> IFakeDAL.

2) Réutilisation

En suivant l'exemple ci-dessus, disons que nous voulons passer de SQL Server (que notre DALClass concrète utilise) à MonogoDB. Cela demanderait un travail important, mais PAS si nous avons programmé une interface. Dans ce cas, nous écrivons simplement la nouvelle classe DB, et changeons (via la fabrique)

WorkerClass -> IDAL -> DALClass

à

WorkerClass -> IDAL -> MongoDBClass

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X