Що означає "програма на інтерфейси, а не на реалізацію"?


Відповіді:


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
}

alt текст

Це лише основний приклад, і фактичне пояснення принципу виходить за рамки цієї відповіді.

EDIT

Я оновив приклад вище та додав абстрактний Speakerбазовий клас. У цьому оновленні я додав функцію для всіх спікерів до "SayHello". Усі спікери говорять "Hello World". Тож це загальна риса з подібною функцією. Зверніться до діаграми класів , і ви побачите , що Speakerабстрактний клас реалізацію ISpeakerінтерфейсу і позначає , Speak()як абстрактні , що означає , що кожна реалізація Гучномовець відповідає за реалізацію Speak()методи , оскільки вона змінюється від Speakerдо Speaker. Але всі спікери говорять "Привіт" одноголосно. Отже, в абстрактному класі Speaker ми визначаємо метод, який говорить "Hello World", і кожна Speakerреалізація отримає SayHello()метод.

Розглянемо випадок, коли SpanishSpeakerне можна сказати привіт, тому в такому випадку ви можете перекрити SayHello()метод для іспанського спікера та створити належне виключення.

Зауважте, що ми не внесли жодних змін у інтерфейс 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 текст


19
Програмування в інтерфейсі стосується не лише типу контрольної змінної. Це також означає, що ви не використовуєте жодних неявних припущень щодо своєї реалізації. Наприклад, якщо ви використовуєте Listтип як тип, ви все ще можете припускати, що випадковий доступ швидкий шляхом багаторазового виклику get(i).
Йоахім Зауер

16
Фабрики є ортогональними для програмування інтерфейсів, але я думаю, що це пояснення робить так, ніби вони є частиною цього.
Т.

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

@ це. якщо я замість цього використовую абстрактний клас або візерунок фасаду, він все ще буде називатися "програма на інтерфейс"? чи явно я повинен використовувати інтерфейс та реалізовувати його на класі?
never_had_a_name

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

29

Розгляньте інтерфейс як договір між об'єктом та його клієнтами. Тобто в інтерфейсі вказуються речі, які може зробити об'єкт, і підписи для доступу до цих речей.

Реалізація - це фактична поведінка. Скажімо, наприклад, у вас є метод sort (). Ви можете реалізувати QuickSort або MergeSort. Це не має значення для сортування виклику коду клієнта до тих пір, поки інтерфейс не зміниться.

Бібліотеки, такі як Java API та .NET Framework, дуже активно використовують інтерфейси, оскільки мільйони програмістів використовують надані об’єкти. Творці цих бібліотек повинні бути дуже обережними, щоб вони не змінювали інтерфейс класів у цих бібліотеках, оскільки це вплине на всіх програмістів, які використовують бібліотеку. З іншого боку, вони можуть змінювати реалізацію скільки завгодно.

Якщо ви, як програміст, кодуєте проти реалізації, то, як тільки він змінює ваш код, він перестає працювати. Тож подумайте про переваги інтерфейсу таким чином:

  1. вона приховує речі, які вам не потрібно знати, що робить об'єкт більш простим у використанні.
  2. він передбачає договір того, як буде вести себе об’єкт, щоб ви могли залежати від цього

Це означає, що вам потрібно знати про те, що ви погоджуєте об'єкт, який потрібно зробити: у прикладі за умови, що ви укладаєте договори лише для сортування, не обов'язково стабільного сортування.
пінгват

Так схоже на те, як бібліотечна документація не згадує про реалізацію, вони є лише описом інтерфейсів класу, що включені.
Джо Іддон

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) ), ви можете використовувати його з будь-якою потребою знати більше про цей об’єкт (що він також італійський), оскільки він не потрібен у цьому контексті (якщо ви хочете звернутися за допомогою, вам не потрібно знати, що він говорить також італійською мовою якщо ви розумієте англійську).
Габріель Шчербак

До речі. Принцип заміщення ІМХО Ліскова стосується семантичного успадкування і не має нічого спільного з інтерфейсами, які можна знайти також у мовах без успадкування (Ідіть від Google).
Габріель Шчербак

5

Якби ви писали автомобільний клас у епоху згоряння-автомобіля, є велика ймовірність, що ви застосували oilChange () у складі цього класу. Але, коли електромобілі будуть представлені, ви зіткнетесь з проблемою, оскільки для цих автомобілів немає жодної зміни масла, а також її впровадження.

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

    У чому полягає недолік 2-го підходу? Ви, можливо, не є експертом у пошуку найкращого способу обслуговування. Ваше завдання - керувати автомобілем і насолоджуватися ним. Не займатися справою його підтримання.

    У чому полягає мінус першого підходу? Якщо ви є компанією з прокату автомобілів, можливо, це не варто того, щоб докласти зусиль.


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.