it-swarm.dev

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

Natrafiamy na to zdanie, czytając o wzorach projektowych.

Ale nie rozumiem, czy ktoś mógłby mi to wyjaśnić?

115
never_had_a_name

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

Kodowanie w stosunku do interfejsu oznacza, że ​​kod klienta zawsze zawiera obiekt interfejsu dostarczany przez fabrykę. Każda instancja zwrócona przez fabrykę będzie typu Interfejs, który musi być zaimplementowana przez każdą kandydującą klasę fabryki. W ten sposób program kliencki nie martwi się implementacją, a sygnatura interfejsu określa, jakie operacje można wykonać. Można tego użyć do zmiany zachowania programu w czasie wykonywania. Pomaga także pisać o wiele lepsze programy 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
}

alt text

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

EDYTOWAĆ

Zaktualizowałem powyższy przykład i dodałem abstrakcyjną klasę podstawową głośników. W tej aktualizacji dodałem funkcję do wszystkich Spakersów do „SayHello”. Wszyscy mówcy mówią „Hello World”. To wspólna funkcja o podobnej funkcji. Zapoznaj się ze schematem klas, a przekonasz się, że klasa abstrakcyjna Speaker implementuje interfejs ISpeaker i oznacza Speak () jako abstrakcyjną, co oznacza, że ​​każda implementacja Speaker jest odpowiedzialna za implementację metody Speak, ponieważ różni się w zależności od głośnika. Ale wszyscy mówcy jednogłośnie mówią „Cześć”. Tak więc w abstrakcyjnej klasie Speaker definiujemy metodę, która mówi „Hello World”, a każda implementacja Speaker będzie czerpać metodę SayHello.

Rozważ przypadek, w którym SpanishSpeaker nie może się przywitać, w takim przypadku możesz zastąpić metodę SayHello dla hiszpańskojęzycznego i zgłosić odpowiedni wyjątek.

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

Możemy to osiągnąć, dodając po prostu podstawową klasę głośników klasy abstrakcyjnej i kilka drobnych modyfikacji w każdej implementacji, pozostawiając oryginalny program bez zmian. Jest to pożądana funkcja każdej aplikacji i sprawia, że ​​aplikacja jest łatwa w utrzymaniu.

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

131

Pomyśl o interfejsie jako umowie między obiektem a jego klientami. Oznacza to, że interfejs określa rzeczy, które może zrobić obiekt, oraz sygnatury dostępu do tych rzeczy.

Wdrożenia są faktycznymi zachowaniami. Powiedzmy na przykład, że masz metodę sort (). Możesz wdrożyć QuickSort lub MergeSort. Nie powinno to mieć znaczenia dla sortowania kodu klienta, dopóki interfejs się nie zmieni.

Biblioteki takie jak Java API i .NET Framework intensywnie korzystają z interfejsów, ponieważ miliony programistów używają udostępnionych 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, jak chcą.

Jeśli jako programista piszesz kod w stosunku do implementacji, to gdy tylko się zmieni, Twój kod przestanie działać. Pomyśl o zaletach interfejsu w ten sposób:

  1. ukrywa rzeczy, których nie musisz wiedzieć, upraszczając obsługę obiektu.
  2. zapewnia umowę dotyczącą zachowania obiektu, dzięki czemu można na nim polegać
26

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

Zwykle implementacja jest wstrzykiwana do twojego kodu przez konstruktor lub wywołanie metody. Twój kod wie o interfejsie lub klasie abstrakcyjnej i może wywoływać wszystko, co jest zdefiniowane w tej umowie. Ponieważ używany jest rzeczywisty obiekt (implementacja interfejsu/klasy abstrakcyjnej), wywołania działają na obiekcie.

To jest podzbiór Liskov Substitution Principle (LSP), L zasad SOLID .

Przykładem .NET może być kodowanie za pomocą IList zamiast List lub Dictionary, więc możesz użyć dowolnej klasy, która implementuje IList zamiennie w twoim 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 ProviderBase klasa abstrakcyjna - zapewnia to pewną infrastrukturę i, co ważne, oznacza, że ​​wszystkie implementacje dostawcy mogą być używane zamiennie, jeśli ją kodujesz.

15
Oded

Jeśli miałbyś napisać klasę samochodów w erze spalania samochodów, istnieje duża szansa, że ​​zaimplementujesz oilChange () jako część tej klasy. Ale kiedy wprowadzane są samochody elektryczne, mielibyście kłopoty, ponieważ w tych samochodach nie ma wymiany oleju i nie ma wdrożenia.

Rozwiązaniem tego problemu jest posiadanie interfejsu performMaintenance () w klasie Car i ukrywanie szczegółów w odpowiedniej implementacji. Każdy typ samochodu zapewnia własną implementację dla performMaintenance (). Jako właściciel samochodu wszystko, z czym musisz sobie poradzić, to performMaintenance () i nie martw się o dostosowanie 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 jest właścicielem wielu samochodów. Wykonujesz usługę, którą chcesz zlecić na zewnątrz. W naszym przypadku chcemy zlecić prace konserwacyjne wszystkich samochodów.

  1. Identyfikujesz umowę (interfejs), która obowiązuje dla wszystkich twoich samochodów i dostawców usług.
  2. Dostawcy usług dysponują mechanizmem świadczenia usługi.
  3. Nie chcesz się martwić o powiązanie typu samochodu z usługodawcą. Po prostu określasz, kiedy chcesz zaplanować konserwację i ją wywołujesz. Właściwa firma serwisowa powinna wskoczyć i wykonać prace konserwacyjne.

    Alternatywne podejście.

  4. Identyfikujesz pracę (może być nowym interfejsem interfejsu), która jest dobra dla wszystkich twoich samochodów.
  5. Ty wychodzi z mechanizmem świadczenia usługi. Zasadniczo zamierzasz zapewnić wdrożenie.
  6. Przywołujesz pracę i zrób to sam. Tutaj wykonasz odpowiednie prace konserwacyjne.

    Jakie są wady drugiego podejścia? Być może nie jesteś ekspertem w znalezieniu najlepszego sposobu przeprowadzenia konserwacji. Twoim zadaniem jest prowadzenie samochodu i czerpanie z niego przyjemności. Nie zajmować się utrzymywaniem go.

    Co to jest wadą pierwszego podejścia? Znalezienie firmy itd. Wiąże się z dodatkowymi kosztami. Jeśli nie jesteś firmą wynajmującą samochody, wysiłek może nie być wart.

5
Raghav Navada

To stwierdzenie dotyczy sprzężenia. Jednym z potencjalnych powodów zastosowania programowania obiektowego jest ponowne użycie. Na przykład możesz podzielić swój algorytm na dwa współpracujące obiekty A i B. Może to być przydatne do późniejszego utworzenia innego algorytmu, który może ponownie wykorzystać jeden lub dwa z tych obiektów. Jednak gdy obiekty te komunikują się (wysyłają wiadomości - metody wywoływania), tworzą między sobą zależności. Ale jeśli chcesz używać 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. Pozwala to obiektowi A komunikować się bez zmian z innym obiektem zależnym od interfejsu. Wspomniane oświadczenie mówi, że jeśli planujesz ponownie wykorzystać jakąś część algorytmu (lub bardziej ogólnie program), powinieneś utworzyć interfejsy i polegać na nich, abyś mógł zmienić konkretną implementację w dowolnym momencie bez zmiany innych obiektów, jeśli użyjesz zadeklarowany interfejs.

4

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

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

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, naprawdę uruchamiając go względem tej klasy. Używam ->, aby oznaczać „rozmowy”.

WorkerClass -> DALClass Dodajmy jednak interfejs do miksu.

WorkerClass -> IDAL -> DALClass.

Tak więc DALClass implementuje interfejs IDAL, a klasa robotnicza wywołuje TYLKO przez to.

Teraz, jeśli chcemy napisać testy dla kodu, moglibyśmy zamiast tego stworzyć prostą klasę, która po prostu działa jak baza danych.

WorkerClass -> IDAL -> IFakeDAL.

2) Użyj ponownie

Postępując zgodnie z powyższym przykładem, powiedzmy, że chcemy przejść z SQL Server (którego używa nasz konkretny DALClass) do MonogoDB. Wymagałoby to poważnej pracy, ale NIE, jeśli zaprogramowaliśmy interfejs. W takim przypadku po prostu piszemy nową klasę DB i zmieniamy (fabrycznie)

WorkerClass -> IDAL -> DALClass

do

WorkerClass -> IDAL -> MongoDBClass

2
Mathieson

interfejsy opisują możliwości. pisząc kod imperatywny, mów o używanych możliwościach, a nie o określonych typach lub klasach.

1
rektide