Что означает «программировать на интерфейсы, а не на реализации»? - PullRequest
112 голосов
/ 23 апреля 2010

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

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

Ответы [ 7 ]

126 голосов
/ 23 апреля 2010

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

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

Вот базовый пример для вас.

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

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

EDIT

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

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

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

И мы могли бы добиться этого, просто добавив базовый абстрактный класс 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

26 голосов
/ 23 апреля 2010

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

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

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

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

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

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

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

Это подмножество 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 голосов
/ 11 марта 2014

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

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

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. Вы идентифицируете работу (может быть новым интерфейсом интерфейса), которая подходит для всех ваших машин.
  5. Вы выйти с механизмом предоставления услуги.В основном вы собираетесь предоставить реализацию.
  6. Вы вызываете работу и делаете ее самостоятельно.Здесь вы будете выполнять работу по техническому обслуживанию.

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

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

4 голосов
/ 23 апреля 2010

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

2 голосов
/ 30 октября 2013

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

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

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

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

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

WorkerClass -> IDAL -> DALClass.

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

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

WorkerClass -> IDAL -> IFakeDAL.

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

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

WorkerClass -> IDAL -> DALClass

до

WorkerClass -> IDAL -> MongoDBClass

1 голос
/ 24 сентября 2010

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

...