Czy istnieje zasada interfejsu „pytaj tylko o to, czego potrzebujesz”?

9

Zacząłem używać zasady projektowania i korzystania z interfejsów, która mówi w zasadzie: „proś tylko o to, czego potrzebujesz”.

Na przykład, jeśli mam kilka typów, które można usunąć, utworzę Deletableinterfejs:

interface Deletable {
   void delete();
}

Następnie mogę napisać ogólną klasę:

class Deleter<T extends Deletable> {
   void delete(T t) {
      t.delete();
   }
}

W innym miejscu kodu zawsze poproszę o jak najmniejszą odpowiedzialność za spełnienie potrzeb kodu klienta. Więc jeśli muszę tylko usunąć File, nadal będę prosić o, a Deletablenie o File.

Czy ta zasada jest powszechnie znana i ma już przyjętą nazwę? Czy to kontrowersyjne? Czy jest to omówione w podręcznikach?

glenviewjeff
źródło
1
Może luźne połączenie? Lub wąskie interfejsy?
tdammers

Odpowiedzi:

16

Uważam, że odnosi się to do tego, co Robert Martin nazywa zasadą segregacji interfejsów . Interfejsy są podzielone na małe i zwięzłe, aby konsumenci (klienci) musieli wiedzieć tylko o interesujących ich metodach. Możesz sprawdzić więcej na SOLID .

Vadim
źródło
4

Aby rozwinąć bardzo dobrą odpowiedź Vadima, odpowiem na pytanie „czy to kontrowersyjne” pytaniem „nie, nie bardzo”.

Ogólnie rzecz biorąc, segregacja interfejsów jest dobra, ponieważ zmniejsza ogólną liczbę „powodów zmiany” różnych zaangażowanych obiektów. Podstawową zasadą jest to, że gdy interfejs z wieloma metodami musi zostać zmieniony, powiedzmy, aby dodać parametr do jednej z metod interfejsu, wówczas wszyscy odbiorcy interfejsu muszą przynajmniej zostać ponownie skompilowani, nawet jeśli nie użyli zmienionej metody. „Ale to tylko rekompilacja!”, Słyszę, jak mówisz; może to być prawda, ale pamiętaj, że zwykle wszystko, co rekompilujesz, musi zostać wypchnięte jako część poprawki oprogramowania, bez względu na to, jak znacząca jest zmiana w pliku binarnym. Reguły te zostały pierwotnie opracowane na początku lat 90., gdy średnia komputerowa stacja robocza była mniej wydajna niż telefon w kieszeni, płonęło połączenie modemowe o szybkości 14,4 kb, a 3,5 „1,44 MB” dyskietek było głównym nośnikiem wymiennym. Nawet w obecnej erze 3G / 4G użytkownicy Internetu bezprzewodowego często mają plany transmisji danych z ograniczeniami, więc po wydaniu aktualizacji im mniej plików binarnych, które trzeba pobrać, tym lepiej.

Jednak, podobnie jak wszystkie dobre pomysły, segregacja interfejsu może się pogorszyć, jeśli zostanie niewłaściwie wdrożona. Po pierwsze, istnieje możliwość, że segregując interfejsy, utrzymując obiekt, który implementuje te interfejsy (spełniając zależności), względnie niezmieniony, możesz otrzymać „Hydrę”, krewnego anty-wzorca „Boski Obiekt”, w którym wszechwiedząca, wszechpotężna natura obiektu jest ukryta przed zależnymi od wąskich interfejsów. Skończysz z wielogłowym potworem, który jest co najmniej tak trudny do utrzymania, jak Boski Obiekt, plus koszty utrzymania wszystkich jego interfejsów. Nie ma dużej liczby interfejsów, których nie powinieneś przekraczać, ale każdy interfejs zaimplementowany w jednym obiekcie powinien być poprzedzony odpowiedzią na pytanie: „Czy ten interfejs przyczynia się do obiektu”

Po drugie, interfejs dla każdej metody może nie być konieczny, pomimo informacji SRP. Możesz skończyć z „kodem ravioli”; tak wiele kawałków wielkości kęsa, że ​​trudno jest prześledzić, aby dowiedzieć się, gdzie dokładnie się dzieje. Nie jest również konieczne dzielenie interfejsu na dwie metody, jeśli wszyscy obecni użytkownicy tego interfejsu potrzebują obu metod. Nawet jeśli jedna z klas zależnych potrzebuje tylko jednej z dwóch metod, ogólnie dopuszczalne jest, aby nie dzielić interfejsu, jeśli jego metody koncepcyjnie mają bardzo wysoką spójność (dobre przykłady to „metody antonimiczne”, które są ze sobą dokładnie przeciwieństwami).

Segregacja interfejsu powinna opierać się na klasach zależnych od interfejsu:

  • Jeśli od interfejsu zależy tylko jedna klasa, nie segreguj. Jeśli klasa nie używa jednej lub więcej metod interfejsu i jest to jedyny konsument interfejsu, istnieje duże prawdopodobieństwo, że nie powinieneś narażać tych metod w pierwszej kolejności.

  • Jeśli istnieje więcej niż jedna klasa zależna od interfejsu, a wszystkie osoby zależne używają wszystkich metod interfejsu, nie segreguj; jeśli musisz zmienić interfejs (w celu dodania metody lub zmiany podpisu), wszyscy obecni konsumenci zostaną dotknięci zmianą niezależnie od tego, czy segregujesz, czy nie (chociaż jeśli dodajesz metodę, której co najmniej jedna osoba zależna nie będzie potrzebować, rozważ ostrożnie, jeśli zmiana powinna zostać zaimplementowana jako nowy interfejs, być może dziedziczący po istniejącym).

  • Jeśli istnieje więcej niż jedna klasa zależna od interfejsu i nie używają one tych samych metod, jest to kandydat do segregacji. Spójrz na „spójność” interfejsu; czy wszystkie metody wspierają jeden, bardzo konkretny cel programowania? Jeśli możesz zidentyfikować więcej niż jeden główny cel interfejsu (i jego implementatorów), rozważ podzielenie interfejsów wzdłuż tych linii, aby utworzyć mniejsze interfejsy z mniejszą liczbą „powodów do zmiany”.

KeithS
źródło
Warto również zauważyć, że segregacja interfejsu może być dobra i dandysowa, jeśli używa się języka / systemu OOP, który może pozwolić kodowi na precyzyjne określenie kombinacji interfejsów, ale przynajmniej w .NET mogą powodować poważne bóle głowy, ponieważ nie ma przyzwoitych problemów sposób określenia zbioru „rzeczy, które implementują IFoo i IBar, ale w przeciwnym razie mogą nie mieć nic wspólnego”.
supercat
Parametry typu ogólnego można zdefiniować za pomocą kryteriów, w tym implementacji wielu interfejsów, ale masz rację, że wyrażenia wymagające typu statycznego zwykle nie obsługują określania więcej niż jednego. Jeśli istnieje potrzeba, aby typ statyczny zaimplementował zarówno IFoo, jak i IBar, a ty kontrolujesz oba te interfejsy, dobrym pomysłem może być implementacja IBaz : IFoo, IBari wymaganie tego.
KeithS,
Jeśli kod klienta może potrzebować czegoś, co może być użyte jako IFooi IBar, zdefiniowanie kompozytu IFooBarmoże być dobrym pomysłem, ale jeśli interfejsy są dokładnie podzielone, łatwo jest uzyskać dziesiątki różnych typów interfejsów. Weź pod uwagę następujące funkcje kolekcji: Wylicz, zlicz liczbę, odczytaj n-ty element, napisz n-ty element, wstaw przed n-tym elementem, usuń n-ty element, nowy element (powiększ kolekcję i zwróć indeks nowej przestrzeni) i dodaj. Dziewięć metod: ECRWIDNA. Prawdopodobnie mógłbym opisać dziesiątki typów, które naturalnie wspierałyby wiele różnych kombinacji.
supercat
Na przykład tablice wspierałyby ECRW. Arraylist wspierałby ECRWIDNA. Lista bezpiecznych wątków może obsługiwać ECRWNA [chociaż A byłby ogólnie przydatny tylko do wstępnego zapełniania listy]. Opakowanie tablicy tylko do odczytu może obsługiwać ECR. Interfejs listy kowariantnej może obsługiwać ECRD. Niespecyficzny interfejs może zapewniać obsługę C lub CD w sposób bezpieczny dla typu. Gdyby opcja Swap była możliwa, niektóre typy mogłyby obsługiwać CS, ale nie D (np. Tablice), podczas gdy inne obsługiwałyby CDS. Próba zdefiniowania odrębnych typów interfejsów dla każdej niezbędnej kombinacji umiejętności byłaby koszmarem.
supercat
Teraz wyobraź sobie, że chcesz mieć możliwość zawijania kolekcji obiektem, który może zrobić wszystko, co kolekcja, ale rejestruje każdą transakcję. Ilu opakowań potrzebowałoby? Gdyby wszystkie kolekcje odziedziczyły po wspólnym interfejsie, który zawierał właściwości identyfikujące ich umiejętności, wystarczy jedno opakowanie. Gdyby jednak wszystkie interfejsy były odrębne, trzeba by ich dziesiątek.
supercat