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

Один наткнется на эту фразу при чтении о шаблонах проектирования.

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

Ответ 1

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

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

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

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 http://ruchitsurati.net/myfiles/interface.png

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

ИЗМЕНИТЬ

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

Рассмотрим случай, когда SpanishSpeaker не может сказать "Привет", поэтому в этом случае вы можете переопределить метод SayHello для испанского Speaker и создать правильное исключение.

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

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

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 http://demo.ruchitsurati.net/myfiles/interface1.png

Ответ 2

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

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

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

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

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

Ответ 3

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

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

Это подмножество 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 - это обеспечивает некоторую инфраструктуру, и, что важно, все реализации поставщика могут использоваться взаимозаменяемо, если вы кодируете против него.

Ответ 4

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

Ответ 5

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

Решение проблемы состоит в том, чтобы иметь интерфейс performMaintenance() в классе Car и скрывать детали внутри соответствующей реализации. Каждый тип автомобиля обеспечит его собственную реализацию для выполнения обслуживания(). Как владелец автомобиля, все, с чем вам приходится иметь дело, это performMaintenance() и не беспокоиться об адаптации, когда есть CHANGE.

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

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

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

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

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

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

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

Ответ 6

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

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

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

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

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

WorkerClass → IDAL → DALClass.

Итак, DALClass реализует интерфейс IDAL, и через него вызывает только рабочий класс.

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

WorkerClass → IDAL → IFakeDAL.

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

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

WorkerClass → IDAL → DALClass

to

WorkerClass → IDAL → MongoDBClass

Ответ 7

Интерфейсы

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