web-dev-qa-db-fra.com

Que signifie «programmer vers 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 cela?

115
never_had_a_name

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

En codant contre les 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 n'est pas préoccupé par l'implémentation et la signature d'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
}

alt text

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

MODIFIER

J'ai mis à jour l'exemple ci-dessus et ajouté une classe de base de haut-parleurs abstraite. Dans cette mise à jour, j'ai ajouté une fonctionnalité à tous les Spakers dans "SayHello". Tous les orateurs parlent "Hello World". C'est donc une caractéristique commune avec une fonction similaire. Reportez-vous au diagramme de classe et vous constaterez que la classe abstraite Speaker implémente l'interface ISpeaker et marque Speak () comme abstrait, ce qui signifie que chaque implémentation Speaker est responsable de l'implémentation de la méthode Speak car elle varie d'un locuteur à l'autre. 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 implémentation Speaker dérivera la méthode SayHello.

Considérons un cas où SpanishSpeaker ne peut pas dire bonjour, dans ce cas, vous pouvez remplacer la méthode SayHello pour le locuteur espagnol et lever l'exception appropriée.

Veuillez noter que nous n'avons apporté aucune modification à Interface ISpeaker. Et le code client et SpeakerFactory restent également inchangés. Et c'est ce que nous réalisons en Programmation-vers-interface .

Et nous pourrions obtenir ce comportement en ajoutant simplement un haut-parleur de classe abstraite de base et quelques modifications mineures dans chaque implémentation, laissant ainsi le programme d'origine inchangé. C'est 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
}

alt text

131

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 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 le tri tant que l'interface ne change pas.

Les bibliothèques comme 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 de ne pas changer l'interface aux classes de ces bibliothèques, car cela affectera 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'il 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 rend l'objet plus simple à utiliser.
  2. il fournit le contrat de la façon dont l'objet se comportera afin que vous puissiez en dépendre
26
Vincent Ramdhanie

Cela signifie que vous devez essayer d'écrire votre code afin 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 dans 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 de Liskov Substitution Principle (LSP), le L des principes SOLID .

Un exemple dans .NET serait de coder avec IList au lieu de List ou 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 de la bibliothèque de classes de base (BCL) est la classe abstraite ProviderBase - cela fournit une certaine infrastructure et, ce qui est aussi important, signifie que toutes les implémentations de fournisseurs peuvent être utilisées de manière interchangeable si vous codez contre.

15
Oded

Si vous deviez écrire une classe de voitures à l'époque des voitures de combustion, il y a de fortes chances que vous implémentiez oilChange () dans le cadre de cette classe. Mais, lorsque des voitures électriques sont introduites, vous auriez des problèmes car il n'y a pas de changement d'huile impliqué 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 masquer les détails dans l'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 performMaintenance () et ne vous inquiétez pas 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 propriétaire d'une voiture qui possède plusieurs voitures. Vous tailler 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) valable 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'invoquer. Une entreprise de services 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 convient à toutes vos voitures.
  5. Vous sortez avec un mécanisme pour fournir le service. Fondamentalement, vous allez fournir l'implémentation.
  6. Vous invoquez le travail et le faites vous-même. Ici, vous allez faire 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 pour tâche de l'entretenir.

    Quel est l'inconvénient de la première approche? Il y a les frais généraux de trouver une entreprise, etc. À moins que vous ne soyez une entreprise de location de voitures, cela ne vaut peut-être pas la peine.

5
Raghav Navada

Cette déclaration concerne le couplage. Une des raisons potentielles pour utiliser la programmation orientée objet est la réutilisation. Ainsi, par exemple, vous pouvez diviser votre algorithme en deux objets collaborateurs A et B. Cela pourrait ê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 (envoi de 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 différents objets 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 un programme), vous devez créer des interfaces et vous y fier, de sorte que vous pouvez changer l'implémentation concrète à tout moment sans changer d'autres objets si vous utilisez le interface déclarée.

4
Gabriel Ščerbák

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 cela est le POURQUOI vous devez toujours programmer sur une interface. Il y a plusieurs raisons, mais deux des plus faciles à expliquer sont

1) Test.

Disons que j'ai tout mon code de 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 sur cette classe. J'utilise -> pour signifier "parle à".

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

WorkerClass -> IDAL -> DALClass.

Ainsi, la 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 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 prendrait beaucoup de travail, 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

2
Mathieson

les interfaces décrivent les capacités. lorsque vous écrivez du code impératif, parlez des capacités que vous utilisez, plutôt que des types ou classes spécifiques.

1
rektide