Terminologia: będę odnosił się do konstrukcji języka interface
jako 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: sort
Procedura 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 sort
abstrakcji), 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.LinkedList
obsługuje zarówno List
interfejs, który dotyczy abstrakcji „uporządkowanej, indeksowalnej kolekcji”, i obsługuje Queue
interfejs, 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:
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.
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ż Comparable
interfejs:
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ł cmp
i 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ą.
Comparable
Interfejs 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 -2
i 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.