Co oznacza „program do interfejsów, a nie do implementacji”?

Odpowiedzi:

152

Interfejsy to tylko umowy lub podpisy i nie wiedzą nic o implementacjach.

Kodowanie względem interfejsu oznacza, że ​​kod klienta zawsze zawiera obiekt interfejsu, który jest dostarczany przez fabrykę. Każda instancja zwrócona przez fabrykę byłaby typu Interface, którą musi zaimplementować każda klasa kandydująca do fabryki. W ten sposób program kliencki nie martwi się o implementację, a podpis interfejsu określa, jakie wszystkie operacje można wykonać. Można to wykorzystać do zmiany zachowania programu w czasie wykonywania. Pomaga również w pisaniu znacznie lepszych programów z punktu widzenia konserwacji.

Oto podstawowy przykład dla Ciebie.

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
}

tekst alternatywny

To tylko podstawowy przykład, a rzeczywiste wyjaśnienie zasady wykracza poza zakres tej odpowiedzi.

EDYTOWAĆ

Zaktualizowałem powyższy przykład i dodałem abstrakcyjną Speakerklasę bazową. W tej aktualizacji dodałem funkcję do wszystkich głośników do „SayHello”. Wszyscy mówcy mówią „Hello World”. Jest to więc wspólna cecha o podobnej funkcji. Zapoznaj się z diagramem klas, a zobaczysz, że interfejs Speakerimplementacji klasy abstrakcyjnej ISpeakeri oznacza Speak()jako abstrakcyjny, co oznacza, że ​​każda implementacja głośnika jest odpowiedzialna za implementację Speak()metody, ponieważ różni się od Speakerdo Speaker. Ale wszyscy mówcy jednogłośnie mówią „Cześć”. Zatem w abstrakcyjnej klasie Speaker definiujemy metodę, która mówi „Hello World”, a każda Speakerimplementacja wyprowadza tę SayHello()metodę.

Rozważ przypadek, w którym SpanishSpeakernie można się przywitać, więc w takim przypadku możesz zastąpić SayHello()metodę dla języka hiszpańskiego i zgłosić odpowiedni wyjątek.

Należy pamiętać, że nie wprowadziliśmy żadnych zmian w interfejsie ISpeaker. Kod klienta i SpeakerFactory również pozostają niezmienione. I to właśnie osiągamy dzięki programowaniu do interfejsu .

Mogliśmy osiągnąć to zachowanie, po prostu dodając podstawowy głośnik klasy abstrakcyjnej i drobne modyfikacje w każdej implementacji, pozostawiając w ten sposób oryginalny program bez zmian. Jest to pożądana funkcja każdej aplikacji, która ułatwia jej konserwację.

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
}

tekst alternatywny

to. __curious_geek
źródło
19
Programowanie interfejsu to nie tylko rodzaj zmiennej referencyjnej. Oznacza to również, że nie używasz żadnych niejawnych założeń dotyczących swojej implementacji. Na przykład, jeśli używasz a Listjako typu, nadal możesz założyć, że dostęp losowy jest szybki przez wielokrotne wywoływanie get(i).
Joachim Sauer
17
Fabryki są ortogonalne do programowania w stosunku do interfejsów, ale myślę, że to wyjaśnienie sprawia, że ​​wydaje się, że są jego częścią.
T.
@Toon: zgadzam się z tobą. Chciałem podać bardzo prosty i prosty przykład programowania do interfejsu. Nie chciałem zmylić pytającego, wdrażając interfejs IFlyable w kilku klasach ptaków i zwierząt.
to. __curious_geek
@to. jeśli zamiast tego użyję abstrakcyjnej klasy lub wzorca fasady, czy nadal będzie to nazywane „programem do interfejsu”? czy też muszę jawnie użyć interfejsu i zaimplementować go w klasie?
never_had_a_name
1
Jakiego narzędzia UML używasz do tworzenia obrazów?
Adam Arold,
31

Wyobraź sobie interfejs jako kontrakt między obiektem a jego klientami. Oznacza to, że interfejs określa rzeczy, które obiekt może robić, oraz sygnatury dostępu do tych rzeczy.

Implementacje to rzeczywiste zachowania. Załóżmy na przykład, że masz metodę sort (). Możesz zaimplementować QuickSort lub MergeSort. To nie powinno mieć znaczenia dla kodu klienta wywołującego sort, o ile interfejs się nie zmieni.

Biblioteki, takie jak Java API i .NET Framework, intensywnie wykorzystują interfejsy, ponieważ miliony programistów używają dostarczonych obiektów. Twórcy tych bibliotek muszą bardzo uważać, aby nie zmieniać interfejsu do klas w tych bibliotekach, ponieważ wpłynie to na wszystkich programistów korzystających z biblioteki. Z drugiej strony mogą zmieniać implementację tak bardzo, jak chcą.

Jeśli jako programista kodujesz w oparciu o implementację, to gdy tylko ona się zmieni, Twój kod przestanie działać. Pomyśl więc o zaletach interfejsu w ten sposób:

  1. ukrywa rzeczy, których nie musisz wiedzieć, ułatwiając korzystanie z obiektu.
  2. zawiera umowę dotyczącą zachowania obiektu, więc możesz na tym polegać
Vincent Ramdhanie
źródło
Oznacza to, że musisz być świadomy tego, do czego zlecasz wykonanie obiektu: w przykładzie pod warunkiem, że zamawiasz tylko rodzaj, niekoniecznie typ stabilny.
pingwin
Podobnie jak w dokumentacji biblioteki nie wspomina się o implementacji, są one tylko opisami dołączonych interfejsów klas.
Joe Iddon
18

Oznacza to, że powinieneś spróbować napisać swój kod tak, aby używał abstrakcji (abstrakcyjnej klasy lub interfejsu) zamiast bezpośrednio implementacji.

Zwykle implementacja jest wstrzykiwana do kodu za pośrednictwem konstruktora lub wywołania metody. Zatem Twój kod wie o interfejsie lub klasie abstrakcyjnej i może wywołać wszystko, co jest zdefiniowane w tym kontrakcie. Jako rzeczywisty obiekt (implementacja interfejsu / klasy abstrakcyjnej) jest używany, wywołania działają na obiekcie.

Jest to podzbiór Liskov Substitution Principle(LSP), L SOLIDzasad.

Przykładem w .NET może być kodowanie za pomocą IListzamiast Listlub Dictionary, więc możesz użyć dowolnej klasy, która implementuje IListzamiennie w kodzie:

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

Innym przykładem z biblioteki klas podstawowych (BCL) jest ProviderBaseklasa abstrakcyjna - zapewnia to pewną infrastrukturę i, co ważne, oznacza, że ​​wszystkie implementacje dostawców mogą być używane zamiennie, jeśli kodujesz przeciwko niej.

Oded
źródło
ale jak klient może współdziałać z interfejsem i używać jego pustych metod?
never_had_a_name
1
Klient nie wchodzi w interakcję z interfejsem, ale poprzez interfejs :) Obiekty oddziałują z innymi obiektami za pomocą metod (komunikatów), a interfejs jest rodzajem języka - gdy wiesz, że dany obiekt (osoba) implementuje (mówi) po angielsku (IList ), możesz go używać bez potrzeby dowiedzenia się czegoś więcej o tym obiekcie (że jest on również Włochem), ponieważ nie jest to potrzebne w tym kontekście (jeśli chcesz poprosić o pomoc, nie musisz wiedzieć, że mówi również po włosku jeśli rozumiesz angielski).
Gabriel Ščerbák
BTW. Zasada substytucji IMHO Liskova dotyczy semantyki dziedziczenia i nie ma nic wspólnego z interfejsami, które można znaleźć również w językach bez dziedziczenia (Go from Google).
Gabriel Ščerbák
6

Gdybyś miał napisać klasę samochodu w erze samochodów spalinowych, to jest duża szansa, że ​​zaimplementowałbyś oilChange () jako część tej klasy. Ale kiedy zostaną wprowadzone samochody elektryczne, będziesz miał kłopoty, ponieważ nie ma potrzeby wymiany oleju w tych samochodach ani wdrożenia.

Rozwiązaniem problemu jest posiadanie interfejsu performMaintenance () w klasie Car i ukrycie szczegółów wewnątrz odpowiedniej implementacji. Każdy typ samochodu zapewniałby własną implementację funkcji performMaintenance (). Jako właściciel samochodu musisz tylko zająć się utrzymaniem () i nie martwić się o adaptację w przypadku ZMIANY.

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

Dodatkowe wyjaśnienie: jesteś właścicielem samochodu, który posiada wiele samochodów. Wytwarzasz usługę, którą chcesz zlecić na zewnątrz. W naszym przypadku chcemy zlecić obsługę techniczną wszystkich samochodów.

  1. Określasz umowę (interfejs), która obowiązuje dla wszystkich Twoich samochodów i usługodawców.
  2. Dostawcy usług wychodzą z mechanizmem świadczenia usługi.
  3. Nie chcesz martwić się o skojarzenie typu samochodu z usługodawcą. Wystarczy określić, kiedy chcesz zaplanować konserwację i ją wywołać. Właściwa firma serwisowa powinna wskoczyć i wykonać prace konserwacyjne.

    Alternatywne podejście.

  4. Identyfikujesz pracę (może to być nowy interfejs interfejsu), która jest odpowiednia dla wszystkich twoich samochodów.
  5. Ci wyjdzie z mechanizmem do świadczenia usługi. Zasadniczo masz zamiar zapewnić implementację.
  6. Wzywasz pracę i robisz to sam. Tutaj będziesz wykonywać odpowiednie prace konserwacyjne.

    Jakie są wady drugiego podejścia? Możesz nie być ekspertem w znalezieniu najlepszego sposobu wykonania konserwacji. Twoim zadaniem jest prowadzić samochód i cieszyć się nim. Nie zajmować się utrzymaniem go.

    Jakie są wady pierwszego podejścia? Istnieje narzut związany ze znalezieniem firmy itp. Jeśli nie jesteś wypożyczalnią samochodów, może to nie być warte wysiłku.

Raghav Navada
źródło
5

To stwierdzenie dotyczy łączenia. Jednym z potencjalnych powodów używania programowania obiektowego jest ponowne wykorzystanie. Na przykład możesz podzielić swój algorytm na dwa współpracujące ze sobą obiekty A i B. Może to być przydatne do późniejszego utworzenia innego algorytmu, który mógłby ponownie wykorzystać jeden z dwóch obiektów. Jednak gdy te obiekty komunikują się (wysyłają wiadomości - wywołują metody), tworzą między sobą zależności. Ale jeśli chcesz użyć jednego bez drugiego, musisz określić, co powinien zrobić inny obiekt C dla obiektu A, jeśli zastąpimy B. Te opisy nazywane są interfejsami. Umożliwia to obiektowi A komunikację bez zmian z innym obiektem zależnym od interfejsu. Oświadczenie, o którym wspomniałeś, mówi, że jeśli planujesz ponownie użyć jakiejś części algorytmu (lub bardziej ogólnie programu), powinieneś stworzyć interfejsy i polegać na nich,

Gabriel Ščerbák
źródło
4

Jak powiedzieli inni, oznacza to, że kod wywołujący powinien wiedzieć tylko o abstrakcyjnym rodzicu, a NIE o rzeczywistej klasie implementującej, która wykona pracę.

Pomaga to zrozumieć, DLACZEGO zawsze należy programować do interfejsu. Jest wiele powodów, ale dwa z najłatwiejszych do wyjaśnienia to

1) Testowanie.

Powiedzmy, że mam cały kod bazy danych w jednej klasie. Jeśli mój program wie o konkretnej klasie, mogę przetestować mój kod tylko wtedy, gdy naprawdę uruchamiam go na tej klasie. Używam -> do oznaczenia „rozmowy z”.

WorkerClass -> DALClass Jednak dodajmy interfejs do miksu.

WorkerClass -> IDAL -> DALClass.

Więc DALClass implementuje interfejs IDAL, a klasa robocza wywołuje TYLKO przez to.

Teraz, jeśli chcemy napisać testy dla kodu, moglibyśmy zamiast tego stworzyć prostą klasę, która zachowuje się jak baza danych.

WorkerClass -> IDAL -> IFakeDAL.

2) Ponowne użycie

Zgodnie z powyższym przykładem, powiedzmy, że chcemy przejść z SQL Server (z którego korzysta nasza konkretna klasa DAL) do MonogoDB. Wymagałoby to dużej pracy, ale NIE, gdybyśmy zaprogramowali interfejs. W takim przypadku po prostu piszemy nową klasę DB i zmieniamy (przez fabrykę)

WorkerClass -> IDAL -> DALClass

do

WorkerClass -> IDAL -> MongoDBClass

Mathieson
źródło
2

interfejsy opisują możliwości. pisząc kod imperatywny, mów raczej o możliwościach, których używasz, a nie o konkretnych typach lub klasach.

rektide
źródło