Dlaczego interfejsy są bardziej pomocne niż nadklasy w uzyskiwaniu luźnego sprzężenia?

15

( Dla celów tego pytania, kiedy mówię „interfejs”, mam na myśli konstrukcję językowąinterface , a nie „interfejs” w innym znaczeniu tego słowa, tj. Publiczne metody, które klasa oferuje światu zewnętrznemu w celu komunikowania się i manipuluj nim. )

Luźne sprzężenie można uzyskać poprzez uzależnienie obiektu od abstrakcji zamiast rodzaju betonu.

Pozwala to na luźne sprzężenie z dwóch głównych powodów: 1- abstrakcje rzadziej się zmieniają niż konkretne typy, co oznacza, że ​​kod zależny jest mniej podatny na uszkodzenie. W czasie wykonywania można stosować 2 różne typy betonu, ponieważ wszystkie one pasują do abstrakcji. Nowe typy betonu można również dodać później, bez potrzeby zmiany istniejącego kodu zależnego.

Rozważmy na przykład klasę Cari dwie podklasy Volvooraz Mazda.

Jeśli twój kod zależy od a Car, może używać albo a, Volvoalbo Mazdapodczas działania. Później można również dodać dodatkowe podklasy bez potrzeby zmiany kodu zależnego.

Ponadto Car- co jest abstrakcją - jest mniej prawdopodobne, że się zmieni niż Volvolub Mazda. Samochody były ogólnie takie same od dłuższego czasu, ale Volvos i Mazdas są znacznie bardziej skłonni do zmiany. Tj. Abstrakcje są bardziej stabilne niż typy konkretne.

Wszystko po to, aby pokazać, że rozumiem, czym jest luźne połączenie i jak można to osiągnąć, polegając na abstrakcjach, a nie konkrecjach. (Jeśli napisałem coś niedokładnego, proszę to powiedzieć).

Nie rozumiem tego:

Abstrakcje mogą być nadklasami lub interfejsami.

Jeśli tak, to dlaczego interfejsy są szczególnie chwalone za ich zdolność do swobodnego łączenia? Nie rozumiem, czym różni się od używania nadklasy.

Jedyne różnice, jakie widzę, to: 1- Interfejsy nie są ograniczone przez pojedyncze dziedziczenie, ale nie ma to wiele wspólnego z tematem luźnego łączenia. 2- Interfejsy są bardziej „abstrakcyjne”, ponieważ w ogóle nie mają logiki implementacji. Ale nadal nie rozumiem, dlaczego to robi tak wielką różnicę.

Proszę wyjaśnić mi, dlaczego mówi się, że interfejsy wspaniale pozwalają na luźne sprzężenie, podczas gdy proste superklasy nie są.

Aviv Cohn
źródło
3
Większość języków (np. Java, C #), które mają „interfejsy”, obsługuje tylko pojedyncze dziedziczenie. Ponieważ każda klasa może mieć tylko jedną bezpośrednią nadklasę, (abstrakcyjne) nadklasy są zbyt ograniczone, aby jeden obiekt obsługiwał wiele abstrakcji. Sprawdź cechy (np . Role Scali lub Perla ), aby znaleźć nowoczesną alternatywę, która pozwala również uniknąć „ problemu z diamentem ” przy wielokrotnym dziedziczeniu.
amon
@amon Czyli mówisz, że przewaga interfejsów nad klasami abstrakcyjnymi, gdy próbujesz osiągnąć luźne sprzężenie, nie ogranicza ich pojedyncze dziedziczenie?
Aviv Cohn
Nie, miałem na myśli, że kosztowny pod względem kompilatora ma więcej do zrobienia, gdy obsługuje klasę abstrakcyjną , ale prawdopodobnie można to pominąć.
pasty
2
Wygląda na to, że @amon jest na dobrej drodze, znalazłem ten post, w którym mówi się, że: interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class(co prowadzi mnie do porównania z C ++, gdzie interfejsy to tylko klasy z czystymi funkcjami wirtualnymi).
pasty
Powiedz, kto twierdzi, że superklasy są złe.
Tulains Córdova

Odpowiedzi:

11

Terminologia: będę odnosił się do konstrukcji języka interfacejako interfejsu , a do interfejsu typu lub obiektu jako powierzchni (z powodu braku lepszego terminu).

Luźne sprzężenie można uzyskać poprzez uzależnienie obiektu od abstrakcji zamiast rodzaju betonu.

Poprawny.

Pozwala to na luźne sprzężenie z dwóch głównych powodów: 1 - abstrakcje rzadziej się zmieniają niż konkretne typy, co oznacza, że ​​kod zależny jest mniej podatny na uszkodzenie. 2 - w czasie wykonywania można używać różnych rodzajów betonu, ponieważ wszystkie one pasują do abstrakcji. Nowe typy betonu można również dodać później, bez potrzeby zmiany istniejącego kodu zależnego.

Niezupełnie poprawne. Obecne języki na ogół nie przewidują zmiany abstrakcji (choć istnieją pewne wzorce projektowe do obsługi tego). Oddzielanie specyfiki od rzeczy ogólnych jest abstrakcją. Zwykle odbywa się to za pomocą jakiejś warstwy abstrakcji . Warstwę tę można zmienić na inną specyfikę bez łamania kodu opartego na tej abstrakcji - uzyskuje się luźne sprzężenie. Przykład inny niż OOP: sortProcedura może zostać zmieniona z Quicksort w wersji 1 na Tim Sort w wersji 2. Kod, który zależy tylko od sortowanego wyniku (tj. Opiera się na sortabstrakcji), jest zatem oddzielony od rzeczywistej implementacji sortowania.

To, co nazwałem powierzchnią powyżej, jest ogólną częścią abstrakcji. Obecnie w OOP zdarza się, że jeden obiekt musi czasami obsługiwać wiele abstrakcji. Niezupełnie optymalny przykład: Java java.util.LinkedListobsługuje zarówno Listinterfejs, który dotyczy abstrakcji „uporządkowanej, indeksowalnej kolekcji”, i obsługuje Queueinterfejs, który (w przybliżeniu) dotyczy abstrakcji „FIFO”.

W jaki sposób obiekt może obsługiwać wiele abstrakcji?

C ++ nie ma interfejsów, ale ma wiele elementów dziedziczenia, metod wirtualnych i klas abstrakcyjnych. Abstrakcję można następnie zdefiniować jako klasę abstrakcyjną (tj. Klasę, której nie można natychmiast utworzyć instancji), która deklaruje, ale nie definiuje metod wirtualnych. Klasy, które implementują specyfikę abstrakcji, mogą następnie odziedziczyć po tej klasie abstrakcyjnej i zaimplementować wymagane metody wirtualne.

Problem polega na tym, że wielokrotne dziedziczenie może prowadzić do problemu diamentowego , w którym kolejność wyszukiwania klas w celu implementacji metody (MRO: kolejność rozwiązywania metod) może prowadzić do „sprzeczności”. Istnieją dwie odpowiedzi na to:

  1. Zdefiniuj rozsądny porządek i odrzuć te zamówienia, których nie można rozsądnie zlinearyzować. C3 MRO jest dość rozsądne i działa dobrze. Zostało opublikowane w 1996 roku.

  2. Wybierz łatwą trasę i odrzuć wielokrotne dziedzictwo.

Java wybrała tę drugą opcję i wybrała pojedyncze dziedzictwo behawioralne. Nadal jednak potrzebujemy zdolności obiektu do obsługi wielu abstrakcji. Dlatego należy stosować interfejsy, które nie obsługują definicji metod, a jedynie deklaracje.

W rezultacie MRO jest oczywiste (wystarczy spojrzeć na każdą nadklasę w kolejności) i że nasz obiekt może mieć wiele powierzchni dla dowolnej liczby abstrakcji.

To okazuje się raczej niezadowalające, ponieważ dość często zachowanie jest częścią powierzchni. Rozważ Comparableinterfejs:

interface Comparable<T> {
    public int cmp(T that);
    public boolean lt(T that);  // less than
    public boolean le(T that);  // less than or equal
    public boolean eq(T that);  // equal
    public boolean ne(T that);  // not equal
    public boolean ge(T that);  // greater than or equal
    public boolean gt(T that);  // greater than
}

Jest to bardzo przyjazne dla użytkownika (ładne API z wieloma wygodnymi metodami), ale żmudne do wdrożenia. Chcielibyśmy, aby interfejs zawierał cmpi implementował inne metody automatycznie pod względem tej jednej wymaganej metody. Mieszanki , ale przede wszystkim cechy [ 1 ], [ 2 ] rozwiązują ten problem bez wpadania w pułapki wielokrotnego dziedziczenia.

Odbywa się to poprzez zdefiniowanie składu cechy, aby cechy ostatecznie nie brały udziału w MRO - zamiast tego zdefiniowane metody są komponowane w klasę implementującą.

ComparableInterfejs może być wyrażona w Scala jako

trait Comparable[T] {
    def cmp(that: T): Int
    def lt(that: T): Boolean = this.cmp(that) <  0
    def le(that: T): Boolean = this.cmp(that) <= 0
    ...
}

Kiedy klasa używa tej cechy, inne metody zostają dodane do definicji klasy:

// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
    override def cmp(that: Inty) = this.x - that.x
    // lt etc. get added automatically
}

Tak Inty(4) cmp Inty(6)byłoby -2i Inty(4) lt Inty(6)będzie true.

Wiele języków ma pewne wsparcie dla cech, a każdy język, który ma „Metaobject Protocol (MOP)”, może mieć dodane cechy. Ostatnia aktualizacja Java 8 dodała domyślne metody, które są podobne do cech (metody w interfejsach mogą mieć implementacje awaryjne, dzięki czemu implementacja tych metod jest opcjonalna).

Niestety cechy są dość nowym wynalazkiem (2002), a zatem są dość rzadkie w większych językach głównego nurtu.

amon
źródło
Dobra odpowiedź, ale dodam, że języki pojedynczego dziedziczenia mogą fałszować wielokrotne dziedziczenie za pomocą interfejsów z kompozycją.
4

Nie rozumiem tego:

Abstrakcje mogą być nadklasami lub interfejsami.

Jeśli tak, to dlaczego interfejsy są szczególnie chwalone za ich zdolność do swobodnego łączenia? Nie rozumiem, czym różni się od używania nadklasy.

Po pierwsze, podtyp i abstrakcja to dwie różne rzeczy. Subtyping oznacza po prostu, że mogę zastąpić wartości jednego typu wartościami innego typu - żaden typ nie musi być abstrakcyjny.

Co ważniejsze, podklasy są bezpośrednio zależne od szczegółów implementacji ich nadklasy. To najsilniejszy rodzaj sprzężenia, jaki istnieje. W rzeczywistości, jeśli klasa podstawowa nie jest zaprojektowana z myślą o dziedziczeniu, zmiany w klasie podstawowej, które nie zmieniają jej zachowania, mogą nadal łamać podklasy i nie ma możliwości, aby wiedzieć a priori, czy nastąpi uszkodzenie. Jest to znane jako problem delikatnej klasy bazowej .

Implementacja interfejsu nie wiąże się z niczym innym, niż sam interfejs, który nie zawiera żadnego zachowania.

Doval
źródło
Dzięki za odpowiedź. Aby zobaczyć, czy rozumiem: jeśli chcesz, aby obiekt o nazwie A zależał od abstrakcji o nazwie B zamiast konkretnej implementacji tej abstrakcji o nazwie C, często lepiej jest, aby B był interfejsem implementowanym przez C, zamiast nadklasy rozszerzonej przez C. Jest tak, ponieważ: C podklasa B ściśle łączy C z B. Jeśli B się zmienia - C się zmienia. Jednak C implementujące B (B jest interfejsem) nie łączy B z C: B to tylko lista metod, które C musi zaimplementować, a zatem nie ma ścisłego połączenia. Jednak w przypadku obiektu A (zależnego) nie ma znaczenia, czy B jest klasą czy interfejsem.
Aviv Cohn
Poprawny? ..... .....
Aviv Cohn
Dlaczego uważasz, że interfejs jest do czegoś podłączony?
Michael Shaw,
Myślę, że ta odpowiedź przybija mu głowę. Używam C ++ całkiem sporo, i jak stwierdzono w jednej z pozostałych odpowiedzi, C ++ nie ma całkiem interfejsów, ale fałszujesz go, używając superklas ze wszystkimi metodami pozostawionymi jako „czysto wirtualne” (tj. Zaimplementowane przez dzieci). Chodzi o to, że łatwo jest tworzyć klasy podstawowe, które ROBIĄ coś wraz z delegowaną funkcjonalnością. W wielu, wielu, wielu przypadkach, ja i moi współpracownicy stwierdzamy, że robiąc to, pojawia się nowy przypadek użycia i unieważnia ten wspólny element funkcjonalności. Jeśli potrzebna jest wspólna funkcjonalność, łatwo jest stworzyć klasę pomocnika.
J Trana
@Prog Twój sposób myślenia jest w większości poprawny, ale znowu abstrakcja i podtyp są dwiema odrębnymi rzeczami. Kiedy mówisz, you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named Cże zakładasz, że klasy nie są jakoś abstrakcyjne. Abstrakcja to wszystko, co ukrywa szczegóły implementacji, więc klasa z polami prywatnymi jest tak samo abstrakcyjna jak interfejs z tymi samymi metodami publicznymi.
Doval
1

Między klasami nadrzędnymi i podrzędnymi występuje sprzężenie, ponieważ dziecko zależy od rodzica.

Powiedzmy, że mamy klasę A, a klasa B dziedziczy po niej. Jeśli wejdziemy do klasy A i zmienimy rzeczy, klasa B również ulegnie zmianie.

Powiedzmy, że mamy interfejs I, a klasa B go implementuje. Jeśli zmienimy interfejs I, to chociaż klasa B może go już nie implementować, klasa B pozostaje niezmieniona.

Michael Shaw
źródło
Jestem ciekawy, czy downvoters mieli powód, czy po prostu mieli zły dzień.
Michael Shaw,
1
Nie głosowałem za tym, ale myślę, że może to mieć związek z pierwszym zdaniem. Klasy podrzędne są sprzężone z klasami nadrzędnymi, a nie na odwrót. Rodzic nie musi nic wiedzieć o dziecku, ale dziecko potrzebuje dogłębnej wiedzy o nim.
@JohnGaughan: Dzięki za opinie. Edytowane dla jasności.
Michael Shaw,