Kiedy używać ogólnych w projektowaniu interfejsu

11

Mam pewne interfejsy, które zamierzam wdrożyć w przyszłości przez strony trzecie, i sam zapewniam podstawowe wdrożenie. Będę używał tylko kilku, aby pokazać przykład.

Obecnie są one zdefiniowane jako

Pozycja:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

Teraz Itemi ItemStackFactoryja mogę absolutnie przewidzieć trochę innej firmy, która chciałaby przedłużyć go w przyszłości. ItemStackContainerMogę również zostać rozszerzony w przyszłości, ale nie w sposób, który mogę przewidzieć, poza moją domyślną implementacją.

Teraz staram się, aby ta biblioteka była jak najbardziej niezawodna; jest to wciąż na wczesnym etapie (pre-pre-alfa), więc może to być akt nadmiernej inżynierii (YAGNI). Czy to odpowiednie miejsce do używania leków generycznych?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

I

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

Obawiam się, że może to spowodować, że implementacje i użytkowanie będą trudniejsze do odczytania i zrozumienia; Myślę, że to zalecenie, aby unikać zagnieżdżania generyków tam, gdzie to możliwe.

Zymus
źródło
1
Trochę mi się wydaje, że i tak ujawniasz szczegóły implementacji. Dlaczego osoba dzwoniąca musi współpracować i znać / dbać o stosy, a nie o zwykłe ilości?
cHao
To coś, o czym nie myślałem. Zapewnia to dobry wgląd w ten konkretny problem. Dopilnuję wprowadzenia zmian w moim kodzie.
Zymus

Odpowiedzi:

9

W interfejsie używa się ogólnych, gdy implementacja może być również ogólna.

Na przykład każda struktura danych, która może akceptować dowolne obiekty, jest dobrym kandydatem na ogólny interfejs. Przykłady: List<T>i Dictionary<K,V>.

Każda sytuacja, w której chcesz poprawić bezpieczeństwo typu w uogólniony sposób, jest dobrym kandydatem na generyczne. A List<String>to lista, która działa tylko na ciągach.

Każda sytuacja, w której chcesz zastosować SRP w sposób uogólniony i uniknąć metod specyficznych dla typu, jest dobrym kandydatem na leki generyczne. Na przykład PersonDocument, PlaceDocument i ThingDocument można zastąpić przez Document<T>.

Wzorzec Fabryki abstrakcyjnej jest dobrym przypadkiem użycia ogólnego, podczas gdy zwykła Metoda Fabryczna po prostu tworzyłaby obiekty, które dziedziczą ze wspólnego, konkretnego interfejsu.

Robert Harvey
źródło
Naprawdę nie zgadzam się z ideą, że ogólne interfejsy powinny być sparowane z ogólnymi implementacjami. Deklarowanie parametru typu ogólnego w interfejsie, a następnie udoskonalanie go lub tworzenie jego instancji w różnych implementacjach to potężna technika. Jest to szczególnie powszechne w Scali. Interfejsy abstrakcyjnych rzeczy; klasy je specjalizują.
Benjamin Hodgson,
@BenjaminHodgson: oznacza to po prostu, że tworzysz implementacje w ogólnym interfejsie dla określonych typów. Ogólne podejście jest nadal ogólne, choć nieco mniej.
Robert Harvey
Zgadzam się. Być może źle zrozumiałem twoją odpowiedź - wydawało ci się, że odradzasz tego rodzaju projekty.
Benjamin Hodgson,
@BenjaminHodgson: Czy metoda fabryczna nie byłaby napisana w oparciu o konkretny interfejs, a nie ogólny? (choć Fabryka abstrakcyjna byłaby ogólna)
Robert Harvey
Myślę, że się zgadzamy :) Prawdopodobnie napisałbym przykład OP jako coś w rodzaju class StackFactory { Stack<T> Create<T>(T item, int count); }. Mogę napisać przykład tego, co miałem na myśli przez „doprecyzowanie parametru typu w podklasie” w odpowiedzi, jeśli chcesz uzyskać dodatkowe wyjaśnienia, chociaż odpowiedź byłaby tylko stycznie powiązana z pierwotnym pytaniem.
Benjamin Hodgson,
8

Twój plan wprowadzenia ogólności do interfejsu wydaje mi się poprawny. Jednak twoje pytanie, czy byłby to dobry pomysł, czy nie, wymaga nieco bardziej skomplikowanej odpowiedzi.

Przez lata przeprowadziłem wywiady z wieloma kandydatami na stanowiska inżynierii oprogramowania w imieniu firm, dla których pracowałem, i zdałem sobie sprawę, że większość osób poszukujących pracy czuje się swobodnie, korzystając z klas ogólnych (na przykład standardowe klasy kolekcji), ale nie z pisaniem klas ogólnych. Większość nigdy nie napisała żadnej klasy ogólnej w swojej karierze i wygląda na to, że byliby całkowicie zagubieni, gdyby zostali poproszeni o napisanie jednej. Tak więc, jeśli narzucisz użycie generycznych na kogokolwiek, kto chce wdrożyć twój interfejs lub rozszerzyć podstawowe implementacje, możesz zniechęcać ludzi.

Możesz jednak spróbować dostarczyć zarówno ogólne, jak i nie-ogólne wersje swoich interfejsów i podstawowych implementacji, aby ludzie, którzy tego nie robią, mogli żyć szczęśliwie w swoim wąskim, obsadzonym czcionkami świecie, podczas gdy ludzie, którzy generics mogą czerpać korzyści z szerszego rozumienia języka.

Mike Nakis
źródło
3

Teraz Itemi ItemStackFactoryja mogę absolutnie przewidzieć trochę innej firmy, która chciałaby przedłużyć go w przyszłości.

Twoja decyzja została podjęta za ciebie. Jeśli nie podasz ogólnych, będą musieli ulepszyć ich implementację za Itemkażdym razem, gdy używają:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

Powinieneś przynajmniej stworzyć ItemStackogólny sposób, jaki sugerujesz. Najgłębszą korzyścią ze stosowania leków generycznych jest to, że prawie nigdy nie musisz niczego rzucać, a oferowanie kodu, który zmusza użytkowników do rzucania, jest przestępstwem kryminalnym.

OldCurmudgeon
źródło
2

Wow ... Mam zupełnie inne podejście do tego.

Mogę absolutnie przewidzieć, że potrzebna będzie strona trzecia

Jest to błąd. Nie powinieneś pisać kodu „na przyszłość”.

Interfejsy są również zapisywane z góry na dół. Nie patrzysz na klasę i nie mówisz: „Hej, ta klasa mogłaby całkowicie zaimplementować interfejs, a potem mogę użyć tego interfejsu także gdzie indziej”. To niewłaściwe podejście, ponieważ tylko klasa, która korzysta z interfejsu, naprawdę wie, jaki powinien być interfejs. Zamiast tego powinieneś pomyśleć: „W tym miejscu moja klasa potrzebuje zależności. Pozwól mi zdefiniować interfejs dla tej zależności. Kiedy skończę pisać tę klasę, napiszę implementację tej zależności”. To, czy ktoś kiedykolwiek ponownie użyje twojego interfejsu i zastąpi Twoją domyślną implementację własną, jest zupełnie nieistotne. Mogą nigdy nie zrobić. Nie oznacza to, że interfejs był bezużyteczny. Sprawia, że ​​Twój kod działa zgodnie z zasadą Inwersji Kontroli, dzięki czemu Twój kod jest bardzo rozszerzalny w wielu miejscach ”

anton1980
źródło
0

Myślę, że używanie leków generycznych w twojej sytuacji jest zbyt skomplikowane.

Jeśli wybierzesz ogólną wersję interfejsów, projekt to wskazuje

  1. ItemStackContainer <A> zawiera jeden typ stosu, A. (tzn. ItemStackContainer nie może zawierać wielu typów stosów).
  2. ItemStack jest dedykowany do określonego rodzaju przedmiotu. Więc nie ma typu abstrakcji wszelkiego rodzaju stosów.
  3. Musisz zdefiniować konkretny ItemStackFactory dla każdego rodzaju elementów. Jest to uważane za zbyt dobre jak wzór fabryczny.
  4. Napisz trudniejszy kod, taki jak ItemStack <? przedłuża przedmiot>, zmniejszając łatwość konserwacji.

Te domniemane założenia i ograniczenia są bardzo ważnymi rzeczami, na które należy uważnie zdecydować. Tak więc, zwłaszcza na wczesnym etapie, nie należy wprowadzać tych przedwczesnych projektów. Pożądany jest prosty, łatwy do zrozumienia, wspólny projekt.

Akio Takahashi
źródło
0

Powiedziałbym, że zależy to głównie od tego pytania: jeśli ktoś musi rozszerzyć funkcjonalność Itemi ItemStackFactory,

  • czy po prostu nadpisują metody, a więc instancje ich podklas będą używane tak jak klasy podstawowe, zachowują się inaczej?
  • czy dodadzą członków i będą inaczej wykorzystywać podklasy?

W pierwszym przypadku nie ma potrzeby stosowania leków generycznych, w drugim prawdopodobnie tak jest.

Michael Borgwardt
źródło
0

Być może możesz mieć hierarchię interfejsów, w której Foo to jeden interfejs, a Bar to inny. Ilekroć typ musi być jednym i drugim, jest to FooAndBar.

Aby uniknąć przekroczenia granicy, wpisz FooAndBar tylko wtedy, gdy jest to konieczne, i przechowuj listę interfejsów, które nigdy niczego nie rozszerzają.

Jest to o wiele wygodniejsze dla większości programistów (najbardziej lubią sprawdzanie typu lub nigdy nie radzą sobie z pisaniem statycznym jakiegokolwiek rodzaju). Bardzo niewiele osób napisało własną klasę leków ogólnych.

Również jako uwaga: interfejsy określają, jakie możliwe akcje można uruchomić. Powinny to być czasowniki, a nie rzeczowniki.

Akash Patel
źródło
Może to być alternatywa dla używania leków generycznych, ale pierwotne pytanie dotyczyło tego, kiedy należy używać leków ogólnych w porównaniu z tym, co sugerujesz. Czy możesz poprawić swoją odpowiedź, aby zasugerować, że leki generyczne nigdy nie są konieczne, ale korzystanie z wielu interfejsów jest odpowiedzią dla wszystkich?
Jay Elston,