Что означает «программа для интерфейсов, а не реализации»?


131

Эту фразу можно встретить, читая о шаблонах проектирования.

Но я этого не понимаю, может ли кто-нибудь мне это объяснить?


Ответы:


148

Интерфейсы - это просто контракты или подписи, и они ничего не знают о реализациях.

Кодирование по отношению к интерфейсу означает, что клиентский код всегда содержит объект интерфейса, который предоставляется фабрикой. Любой экземпляр, возвращаемый фабрикой, будет иметь тип Интерфейс, который должен быть реализован любым классом-кандидатом фабрики. Таким образом, клиентская программа не заботится о реализации, а подпись интерфейса определяет, какие все операции могут быть выполнены. Это можно использовать для изменения поведения программы во время выполнения. Это также помогает вам писать гораздо лучшие программы с точки зрения обслуживания.

Вот вам простой пример.

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
}

альтернативный текст

Это всего лишь базовый пример, и фактическое объяснение принципа выходит за рамки этого ответа.

РЕДАКТИРОВАТЬ

Я обновил приведенный выше пример и добавил абстрактный Speakerбазовый класс. В этом обновлении я добавил функцию «SayHello» для всех выступающих. Все выступающие говорят «Hello World». Так что это общая черта с аналогичной функцией. Обратитесь к диаграмме классов, и вы обнаружите, что интерфейс Speakerреализации абстрактного класса ISpeakerпомечается Speak()как абстрактный, что означает, что каждая реализация Speaker отвечает за реализацию Speak()метода, поскольку он варьируется от Speakerдо Speaker. Но все выступающие единодушно говорят «Привет». Итак, в абстрактном классе Speaker мы определяем метод, который говорит «Hello World», и каждая Speakerреализация будет наследовать этот SayHello()метод.

Рассмотрим случай, когда SpanishSpeakerнельзя сказать «Привет», поэтому в этом случае вы можете переопределить SayHello()метод для говорящего по-испански и вызвать соответствующее исключение.

Обратите внимание, что мы не вносили никаких изменений в интерфейс ISpeaker. И клиентский код, и SpeakerFactory также остаются неизменными. И этого мы достигаем с помощью программирования через интерфейс .

И мы могли бы добиться такого поведения, просто добавив базовый абстрактный класс Speaker и немного изменив его в реализации Each, оставив исходную программу без изменений. Это желательная функция любого приложения, которая делает ваше приложение легко обслуживаемым.

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
}

альтернативный текст


19
Программирование интерфейса касается не только типа ссылочной переменной. Это также означает, что вы не используете никаких неявных предположений о своей реализации. Например, если вы используете в Listкачестве типа a , вы все равно можете предполагать, что произвольный доступ выполняется быстро, многократно вызывая get(i).
Иоахим Зауэр

16
Фабрики ортогональны программированию и интерфейсам, но я думаю, что это объяснение создает впечатление, будто они являются его частью.
Т.

@Toon: согласен с тобой. Я хотел предоставить очень простой и простой пример программирования интерфейса. Я не хотел запутывать вопрошающего, реализуя интерфейс IFlyable для нескольких классов птиц и животных.
это __curious_geek

@этот. если я вместо этого использую абстрактный класс или паттерн фасада, будет ли он называться «программа для интерфейса»? или мне нужно явно использовать интерфейс и реализовать его в классе?
never_had_a_name

1
Какой инструмент uml вы использовали для создания изображений?
Адам Арольд

29

Думайте об интерфейсе как о контракте между объектом и его клиентами. То есть интерфейс определяет, что может делать объект, и сигнатуры для доступа к этим вещам.

Реализации - это фактическое поведение. Скажем, например, у вас есть метод sort (). Вы можете реализовать QuickSort или MergeSort. Это не должно иметь значения для клиентского кода, вызывающего sort, до тех пор, пока интерфейс не изменяется.

Такие библиотеки, как Java API и .NET Framework, интенсивно используют интерфейсы, потому что миллионы программистов используют предоставленные объекты. Создатели этих библиотек должны быть очень осторожны, чтобы не изменить интерфейс к классам в этих библиотеках, потому что это повлияет на всех программистов, использующих библиотеку. С другой стороны, они могут менять реализацию сколько угодно.

Если, как программист, вы кодируете вопреки реализации, то как только она меняет, ваш код перестает работать. Итак, подумайте о преимуществах интерфейса следующим образом:

  1. он скрывает то, что вам не нужно знать, делая объект более простым в использовании.
  2. он предоставляет контракт о том, как объект будет вести себя, поэтому вы можете положиться на это

Это означает, что вам нужно знать, что вы заключаете контракт с объектом: в приведенном примере вы заключаете контракт только на сортировку, не обязательно на стабильную сортировку.
Penguat

Подобно тому, как в документации библиотеки не упоминается реализация, это просто описания включенных интерфейсов классов.
Джо Иддон

17

Это означает, что вы должны попытаться написать свой код так, чтобы он напрямую использовал абстракцию (абстрактный класс или интерфейс), а не реализацию.

Обычно реализация внедряется в ваш код через конструктор или вызов метода. Итак, ваш код знает об интерфейсе или абстрактном классе и может вызывать все, что определено в этом контракте. Поскольку используется реальный объект (реализация интерфейса / абстрактного класса), вызовы работают с объектом.

Это подмножество Liskov Substitution Principle(LSP), L SOLIDпринципов.

Примером в .NET будет код с IList вместо Listили Dictionary, поэтому вы можете использовать любой класс, который IListвзаимозаменяемо реализуется в вашем коде:

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

Другой пример из библиотеки базовых классов (BCL) - это ProviderBase абстрактный класс - он предоставляет некоторую инфраструктуру и, что немаловажно, означает, что все реализации поставщика могут использоваться взаимозаменяемо, если вы кодируете против него.


но как клиент может взаимодействовать с интерфейсом и использовать его пустые методы?
never_had_a_name

1
Клиент взаимодействует не с интерфейсом, а через интерфейс :) Объекты взаимодействуют с другими объектами через методы (сообщения), а интерфейс - это своего рода язык - когда вы знаете, что определенный объект (человек) реализует (говорит) английский язык (IList ), вы можете использовать его без какой-либо необходимости знать больше об этом объекте (что он также итальянец), потому что он не нужен в этом контексте (если вы хотите попросить о помощи, вам не нужно знать, что он также говорит по-итальянски если вы понимаете английский).
Габриэль Щербак

КСТАТИ. ИМХО принцип подстановки Лисков связан с семантикой наследования и не имеет ничего общего с интерфейсами, которые можно найти и в языках без наследования (Go from Google).
Габриэль Щербак

5

Если бы вам пришлось писать класс автомобилей в эпоху Combustion-Car, то велика вероятность, что вы бы реализовали oilChange () как часть этого класса. Но когда появятся электромобили, у вас возникнут проблемы, так как в этих автомобилях не требуется замена масла и нет никаких дополнительных приспособлений.

Решение проблемы состоит в том, чтобы иметь интерфейс performMain maintenance () в классе Car и скрывать детали внутри соответствующей реализации. Каждый тип Car будет предоставлять свою собственную реализацию для performMain maintenance (). Все, с чем вам, как владельцу Автомобиля, приходится иметь дело, - это выполнитьMain maintenance () и не беспокоиться об адаптации при ИЗМЕНЕНИИ.

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;
}

Дополнительное объяснение: вы - автовладелец, у которого несколько машин. Вы создаете услугу, которую хотите передать на аутсорсинг. В нашем случае мы хотим передать обслуживание всех автомобилей на аутсорсинг.

  1. Вы определяете договор (интерфейс), который действует для всех ваших автомобилей и поставщиков услуг.
  2. Поставщики услуг предлагают механизм предоставления услуги.
  3. Вам не нужно беспокоиться о привязке типа автомобиля к поставщику услуг. Вы просто указываете, когда хотите запланировать обслуживание, и вызываете его. Соответствующая сервисная компания должна вмешаться и выполнить работы по техническому обслуживанию.

    Альтернативный подход.

  4. Вы определяете работу (может быть новый интерфейс Interface), которая подходит для всех ваших автомобилей.
  5. Вы предлагаете механизм предоставления услуги. В основном вы собираетесь предоставить реализацию.
  6. Вы вызываете работу и делаете ее сами. Здесь вы будете выполнять соответствующие работы по техническому обслуживанию.

    В чем обратная сторона второго подхода? Возможно, вы не являетесь экспертом в поиске лучшего способа обслуживания. Ваша задача - водить машину и получать от нее удовольствие. Не заниматься его поддержанием.

    В чем обратная сторона первого подхода? Есть накладные расходы на поиск компании и т. Д. Если вы не компания по аренде автомобилей, это может не стоить усилий.


4

Это утверждение о сцеплении. Одна из возможных причин использования объектно-ориентированного программирования - повторное использование. Так, например, вы можете разделить свой алгоритм между двумя взаимодействующими объектами A и B. Это может быть полезно для последующего создания другого алгоритма, который может повторно использовать тот или иной из двух объектов. Однако, когда эти объекты общаются (отправляют сообщения - вызывают методы), они создают зависимости друг от друга. Но если вы хотите использовать одно без другого, вам нужно указать, что должен делать другой объект C для объекта A, если мы заменим B. Эти описания называются интерфейсами. Это позволяет объекту A без изменений связываться с другим объектом, полагаясь на интерфейс. В упомянутом вами заявлении говорится, что если вы планируете повторно использовать какую-то часть алгоритма (или, в более общем плане, программу), вы должны создавать интерфейсы и полагаться на них,


2

Как говорили другие, это означает, что ваш вызывающий код должен знать только об абстрактном родителе, а НЕ о фактическом реализующем классе, который будет выполнять эту работу.

Что помогает понять это, так это ПОЧЕМУ вы всегда должны программировать интерфейс. Есть много причин, но две из самых простых для объяснения:

1) Тестирование.

Скажем, у меня есть весь код моей базы данных в одном классе. Если моя программа знает о конкретном классе, я могу проверить свой код, только действительно запустив его для этого класса. Я использую -> для обозначения «разговаривает с».

WorkerClass -> DALClass Однако давайте добавим интерфейс к этому миксу.

WorkerClass -> IDAL -> DALClass.

Таким образом, DALClass реализует интерфейс IDAL, и рабочий класс вызывает ТОЛЬКО через него.

Теперь, если мы хотим написать тесты для кода, мы могли бы вместо этого создать простой класс, который просто действует как база данных.

WorkerClass -> IDAL -> IFakeDAL.

2) Повторное использование

Следуя приведенному выше примеру, допустим, мы хотим перейти с SQL Server (который использует наш конкретный DALClass) на MonogoDB. Это потребует серьезной работы, но НЕ если мы запрограммированы на интерфейс. В этом случае мы просто пишем новый класс БД и меняем (через фабрику)

WorkerClass -> IDAL -> DALClass

в

WorkerClass -> IDAL -> MongoDBClass


1

интерфейсы описывают возможности. при написании императивного кода говорите о возможностях, которые вы используете, а не о конкретных типах или классах.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.