Que signifie «programme aux interfaces, pas aux implémentations»?

Réponses:

148

Les interfaces ne sont que des contrats ou des signatures et elles ne savent rien des implémentations.

Codage par rapport aux moyens d'interface, le code client contient toujours un objet Interface qui est fourni par une usine. Toute instance retournée par l'usine serait de type Interface que toute classe candidate d'usine doit avoir implémentée. De cette façon, le programme client ne se soucie pas de l'implémentation et la signature de l'interface détermine ce que toutes les opérations peuvent être effectuées. Cela peut être utilisé pour modifier le comportement d'un programme au moment de l'exécution. Il vous aide également à écrire de bien meilleurs programmes 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
}

texte alternatif

Ceci n'est qu'un exemple de base et l'explication réelle du principe dépasse le cadre de cette réponse.

ÉDITER

J'ai mis à jour l'exemple ci-dessus et ajouté une Speakerclasse de base abstraite . Dans cette mise à jour, j'ai ajouté une fonctionnalité à tous les haut-parleurs pour "SayHello". Tous les orateurs parlent "Hello World". C'est donc une caractéristique commune avec une fonction similaire. Reportez-vous au diagramme de classes et vous constaterez que Speakerla classe abstraite implémente l' ISpeakerinterface et marque le Speak()comme abstrait, ce qui signifie que chaque implémentation Speaker est responsable de l'implémentation de la Speak()méthode car elle varie de Speakerà Speaker. Mais tous les orateurs disent "Bonjour" à l'unanimité. Ainsi, dans la classe abstraite Speaker, nous définissons une méthode qui dit "Hello World" et chaque Speakerimplémentation dérivera la SayHello()méthode.

Prenons un cas où SpanishSpeakerje ne peux pas dire bonjour, alors dans ce cas, vous pouvez remplacer la SayHello()méthode pour Spanish Speaker et lever une exception appropriée.

Veuillez noter que nous n'avons apporté aucune modification à l'interface ISpeaker. Et le code client et SpeakerFactory restent inchangés. Et c'est ce que nous réalisons par 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 d'origine inchangé. Il s'agit d'une fonctionnalité souhaitée de toute application et elle 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
}

texte alternatif

ce. __curious_geek
la source
19
La programmation de l'interface ne concerne pas uniquement le type de variable de référence. Cela signifie également que vous n'utilisez aucune hypothèse implicite concernant votre implémentation. Par exemple, si vous utilisez a Listcomme type, vous pouvez toujours supposer que l'accès aléatoire est rapide en appelant à plusieurs reprises get(i).
Joachim Sauer
16
Les usines sont orthogonales à la programmation des interfaces, mais je pense que cette explication donne l'impression qu'elles en font partie.
T.
@Toon: d'accord avec vous. Je voulais fournir un exemple très basique et simple pour la programmation à l'interface. Je ne voulais pas embrouiller le questionneur en implémentant l'interface IFlyable sur quelques classes d'oiseaux et d'animaux.
ça. __curious_geek
@ce. si j'utilise à la place une classe abstraite ou un motif de façade, sera-t-il toujours appelé "programme vers une interface"? ou dois-je explicitement utiliser une interface et l'implémenter sur une classe?
never_had_a_name
1
Quel outil uml utilisiez-vous pour créer les images?
Adam Arold
29

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

Les implémentations 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 appelant sort tant que l'interface ne change pas.

Les bibliothèques comme 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 changer l'interface avec les classes de ces bibliothèques car cela affectera tous les programmeurs utilisant la bibliothèque. D'un autre côté, ils peuvent changer la mise en œuvre autant qu'ils le souhaitent.

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

  1. il cache les choses que vous n'avez pas besoin de savoir, ce qui simplifie l'utilisation de l'objet.
  2. il fournit le contrat sur le comportement de l'objet, vous pouvez donc en dépendre
Vincent Ramdhanie
la source
Cela signifie que vous devez être conscient de ce que vous contractez l'objet à faire: dans l'exemple fourni, vous ne contractez que pour un tri, pas nécessairement un tri stable.
penguat
De la même manière que la documentation de la bibliothèque ne mentionne pas l'implémentation, ce ne sont que des descriptions des interfaces de classe incluses.
Joe Iddon
17

Cela signifie que vous devez essayer d'écrire votre code pour qu'il utilise une abstraction (classe abstraite ou interface) au lieu de l'implémentation directement.

Normalement, l'implémentation est injectée dans votre code via le constructeur ou 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 sur ce contrat. Lorsqu'un objet réel (implémentation de l'interface / classe abstraite) est utilisé, les appels opèrent sur l'objet.

Il s'agit d'un sous-ensemble du Liskov Substitution Principle(LSP), le L des SOLIDprincipes.

Un exemple dans .NET serait de coder avec IListau lieu de Listou Dictionary, vous pouvez donc utiliser n'importe quelle classe qui implémente de IListmaniè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 de la bibliothèque de classes de base (BCL) est la ProviderBaseclasse abstraite - cela fournit une certaine infrastructure et, ce qui est important, signifie que toutes les implémentations de fournisseur peuvent être utilisées de manière interchangeable si vous codez contre elle.

Oded
la source
mais comment un client peut-il interagir avec une interface et utiliser ses méthodes vides?
never_had_a_name
1
Le client n'interagit pas avec l'interface, mais via l'interface :) Les objets interagissent avec d'autres objets via des méthodes (messages) et une interface est une sorte de langage - lorsque vous savez qu'un certain objet (personne) implémente (parle) anglais (IList ), vous pouvez l'utiliser sans avoir besoin d'en savoir plus sur cet objet (qu'il est également italien), car il 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).
Gabriel Ščerbák
BTW. Le principe de substitution IMHO Liskov concerne la sémantique de l'héritage et n'a rien à voir avec les interfaces, qui peuvent également être trouvées dans les langues sans héritage (Go from Google).
Gabriel Ščerbák
5

Si vous deviez écrire une classe de voiture à l'époque des voitures à combustion, il y a de fortes chances que vous implémentiez oilChange () dans le cadre de cette classe. Mais, lorsque les voitures électriques sont introduites, vous seriez en difficulté car il n'y a pas de changement d'huile pour ces voitures et aucune mise en œuvre.

La solution au problème est d'avoir une interface performMaintenance () dans la classe Car et de cacher les détails dans une implémentation appropriée. Chaque type de voiture fournirait sa propre implémentation pour performMaintenance (). En tant que propriétaire d'une voiture, tout ce que vous avez à faire est de performMaintenance () et de ne pas vous soucier de vous adapter en cas de CHANGEMENT.

class MaintenanceSpecialist {
    public:
        virtual int performMaintenance() = 0;
};

class CombustionEnginedMaintenance : public MaintenanceSpecialist {
    int performMaintenance() { 
        printf("combustionEnginedMaintenance: We specialize in maintenance of Combustion engines \n");
        return 0;
    }
};

class ElectricMaintenance : public MaintenanceSpecialist {
    int performMaintenance() {
        printf("electricMaintenance: We specialize in maintenance of Electric Cars \n");
        return 0;
    }
};

class Car {
    public:
        MaintenanceSpecialist *mSpecialist;
        virtual int maintenance() {
            printf("Just wash the car \n");
            return 0;
        };
};

class GasolineCar : public Car {
    public: 
        GasolineCar() {
        mSpecialist = new CombustionEnginedMaintenance();
        }
        int maintenance() {
        mSpecialist->performMaintenance();
        return 0;
        }
};

class ElectricCar : public Car {
    public: 
        ElectricCar() {
             mSpecialist = new ElectricMaintenance();
        }

        int maintenance(){
            mSpecialist->performMaintenance();
            return 0;
        }
};

int _tmain(int argc, _TCHAR* argv[]) {

    Car *myCar; 

    myCar = new GasolineCar();
    myCar->maintenance(); /* I dont know what is involved in maintenance. But, I do know the maintenance has to be performed */


    myCar = new ElectricCar(); 
    myCar->maintenance(); 

    return 0;
}

Explication supplémentaire: vous êtes un propriétaire de voiture qui possède plusieurs voitures. Vous découpez le service que vous souhaitez externaliser. Dans notre cas, nous voulons externaliser les travaux de maintenance de toutes les voitures.

  1. Vous identifiez le contrat (Interface) qui vaut pour toutes vos voitures et prestataires de services.
  2. Les fournisseurs de services proposent un mécanisme pour fournir le service.
  3. Vous ne voulez pas vous soucier d'associer le type de voiture au fournisseur de services. Vous spécifiez simplement quand vous souhaitez planifier la maintenance et l'appeler. Une société de service appropriée doit intervenir et effectuer les travaux de maintenance.

    Approche alternative.

  4. Vous identifiez le travail (peut être une nouvelle interface d'interface) qui tient bon pour toutes vos voitures.
  5. Vous venez avec un mécanisme pour fournir le service. En gros, vous allez fournir la mise en œuvre.
  6. Vous invoquez le travail et le faites vous-même. Ici, vous allez effectuer le travail de maintenance approprié.

    Quel est l'inconvénient de la 2ème approche? Vous n'êtes peut-être pas l'expert pour trouver la meilleure façon d'effectuer la maintenance. Votre travail consiste à conduire la voiture et à en profiter. Ne pas avoir à le maintenir.

    Quel est l'inconvénient de la première approche? Il y a des frais généraux liés à la recherche d'une entreprise, etc.

Raghav Navada
la source
4

Cette déclaration concerne le couplage. Une des raisons potentielles de l'utilisation de la programmation orientée objet est la réutilisation. Ainsi, par exemple, vous pouvez diviser votre algorithme entre deux objets collaboratifs A et B. 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 - méthodes d'appel), ils créent des dépendances entre eux. Mais si vous voulez utiliser l'un sans l'autre, vous devez spécifier ce que doit faire un autre objet C pour l'objet A si nous remplaçons B. Ces descriptions sont appelées interfaces. Cela permet à l'objet A de communiquer sans changement avec un objet différent en fonction de 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 en remettre,

Gabriel Ščerbák
la source
2

Comme d'autres l'ont dit, cela signifie que votre code appelant ne devrait connaître qu'un parent abstrait, PAS la classe d'implémentation réelle qui fera le travail.

Ce qui aide à comprendre ceci est le POURQUOI vous devez toujours programmer une interface. Il y a de nombreuses raisons, mais deux des plus faciles à expliquer sont

1) Test.

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

WorkerClass -> DALClass Cependant, ajoutons une interface au mix.

WorkerClass -> IDAL -> DALClass.

Ainsi, DALClass implémente l'interface IDAL, et la classe de travail appelle UNIQUEMENT à travers cela.

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

WorkerClass -> IDAL -> IFakeDAL.

2) Réutiliser

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 majeur, mais PAS si nous avons programmé une interface. Dans ce cas, nous écrivons simplement la nouvelle classe DB et changeons (via l'usine)

WorkerClass -> IDAL -> DALClass

à

WorkerClass -> IDAL -> MongoDBClass

Mathieson
la source
1

les interfaces décrivent les capacités. lors de l'écriture de code impératif, parlez des capacités que vous utilisez, plutôt que des types ou classes spécifiques.

rektide
la source