Czy zawsze ma sens „programować do interfejsu” w Javie?

9

Widziałem dyskusję przy tym pytaniu dotyczącą sposobu tworzenia instancji klasy implementującej z interfejsu. W moim przypadku piszę bardzo mały program w Javie, który korzysta z instancji TreeMapi, zgodnie z opinią wszystkich, powinien być utworzony w następujący sposób:

Map<X> map = new TreeMap<X>();

W moim programie wywołuję funkcję map.pollFirstEntry(), która nie jest zadeklarowana w Mapinterfejsie (i kilku innych, którzy również są obecni w Mapinterfejsie). Udało mi się to zrobić, przesyłając TreeMap<X>tę metodę wszędzie, gdzie nazywam tę metodę:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

Rozumiem zalety wytycznych inicjalizacji opisanych powyżej dla dużych programów, jednak w przypadku bardzo małego programu, w którym ten obiekt nie zostałby przekazany innym metodom, uważam, że nie jest to konieczne. Mimo to piszę ten przykładowy kod jako część aplikacji o pracę i nie chcę, aby mój kod wyglądał źle lub nie był zaśmiecony. Jakie byłoby najbardziej eleganckie rozwiązanie?

EDYCJA: Chciałbym podkreślić, że bardziej interesują mnie ogólne dobre praktyki kodowania zamiast zastosowania konkretnej funkcji TreeMap. Jak niektóre z odpowiedzi już wskazały (i zaznaczyłem, że odpowiedziałem jako pierwszy, aby to zrobić), należy zastosować wyższy możliwy poziom abstrakcji, bez utraty funkcjonalności.

jimijazz
źródło
1
Użyj TreeMap, gdy potrzebujesz funkcji. Prawdopodobnie był to wybór projektowy z konkretnego powodu, więc powinien być również częścią wdrożenia.
@JordanReiter powinienem po prostu dodać nowe pytanie o tej samej treści, czy może istnieje wewnętrzny mechanizm zamieszczania postów?
jimijazz
2
„Rozumiem zalety wytycznych inicjalizacyjnych opisanych powyżej w przypadku dużych programów” Konieczność rozsyłania w dowolnym miejscu nie jest korzystna bez względu na rozmiar programu
Ben Aaronson

Odpowiedzi:

23

„Programowanie interfejsu” nie oznacza „używaj najbardziej abstrakcyjnej wersji”. W takim przypadku wszyscy po prostu skorzystaliby Object.

Oznacza to, że powinieneś zdefiniować swój program w możliwie najniższym stopniu abstrakcji bez utraty funkcjonalności . Jeśli potrzebujesz TreeMap, musisz zdefiniować umowę za pomocą TreeMap.

Jeroen Vannevel
źródło
2
TreeMap nie jest interfejsem, jest klasą implementacyjną. Implementuje interfejsy Map, SortedMap i NavigableMap. Opisana metoda jest częścią interfejsu NavigableMap . Użycie TreeMap zapobiegłoby przełączeniu implementacji na ConcurrentSkipListMap (na przykład), który stanowi cały punkt kodowania interfejsu, a nie implementacja.
3
@MichaelT: Nie spojrzałem na dokładną abstrakcję, jakiej potrzebuje w tym konkretnym scenariuszu, więc posłużyłem TreeMapjako przykład. „Program do interfejsu” nie powinien być traktowany dosłownie jako interfejs lub klasa abstrakcyjna - implementację można również uznać za interfejs.
Jeroen Vannevel
1
Mimo że publiczny interfejs / metody klasy implementacyjnej jest technicznie „interfejsem”, łamie koncepcję LSP i zapobiega podstawianiu innej podklasy, dlatego chcesz zaprogramować na public interface„publiczne metody implementacji” .
@JeroenVannevel Zgadzam się, że programowanie interfejsu można wykonać, gdy interfejs jest faktycznie reprezentowany przez klasę. Nie wiem jednak, jakie korzyści TreeMapprzyniosłoby to ponad SortedMaplubNavigableMap
toniedzwiedz
16

Jeśli nadal chcesz użyć interfejsu, którego możesz użyć

NavigableMap <X, Y> map = new TreeMap<X, Y>();

interfejs nie zawsze musi być używany, ale często jest punkt, w którym chcesz przyjąć bardziej ogólny widok, który pozwala zastąpić implementację (być może w celu przetestowania), a jest to łatwe, jeśli wszystkie odniesienia do obiektu są abstrakcyjne jako Typ interfejsu.


źródło
3

Kluczem do kodowania interfejsu zamiast implementacji jest uniknięcie wycieku szczegółów implementacji, które w innym przypadku ograniczałyby twój program.

Rozważ sytuację, w której oryginalna wersja twojego kodu używała HashMapi ujawniła to.

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

Oznacza to, że każda zmiana getFoo()jest przełomową zmianą API i sprawi, że ludzie będą z niej korzystać. Jeśli gwarantujesz tylko, że foojest to mapa, powinieneś ją zwrócić.

private Map foo = new HashMap();
public Map getFoo() { return foo; }

Zapewnia to elastyczność zmiany sposobu działania kodu. Zdajesz sobie sprawę, że footak naprawdę musi to być mapa, która zwraca rzeczy w określonej kolejności.

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

I to niczego nie psuje przez resztę kodu.

Możesz później wzmocnić kontrakt, który daje klasa, nie przerywając niczego.

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Dotyczy to zasady substytucji Liskowa

Podstawialność jest zasadą w programowaniu obiektowym. Stwierdza się, że w programie komputerowym, jeśli S jest podtypem T, wówczas obiekty typu T można zastąpić obiektami typu S (tj. Obiekty typu S mogą zastąpić obiekty typu T) bez zmiany któregokolwiek z pożądanych właściwości tego programu (poprawność, wykonane zadanie itp.).

Ponieważ NavigableMap jest podtypem mapy, podstawienia można dokonać bez zmiany programu.

Ujawnienie typów implementacji utrudnia zmianę wewnętrznego działania programu, gdy trzeba dokonać zmiany. Jest to bolesny proces i wiele razy powoduje brzydkie obejścia, które tylko później powodują więcej bólu w koderze (patrzę na was, poprzedni programista, który z jakiegoś powodu ciągle tasował dane między LinkedHashMap i TreeMap - zaufaj mi, ilekroć ja widzę twoje imię w obwodzie svn martwię się).

Nadal będziesz chciał uniknąć nieszczelnych typów implementacji. Na przykład możesz chcieć zaimplementować ConcurrentSkipListMap ze względu na pewne cechy wydajności lub po prostu lubisz to, java.util.concurrent.ConcurrentSkipListMapa nie java.util.TreeMapw instrukcjach importu lub cokolwiek innego.


źródło
1

Zgadzam się z innymi odpowiedziami, że powinieneś użyć najbardziej ogólnej klasy (lub interfejsu), której tak naprawdę potrzebujesz, w tym przypadku TreeMap (lub jak ktoś zasugerował NavigableMap). Ale chciałbym dodać, że jest to z pewnością lepsze niż rzucanie go wszędzie, co w każdym razie byłoby znacznie większym zapachem. Zobacz /programming/4167304/why-should-casting-be-avoided z kilku powodów.

Sebastiaan van den Broek
źródło
1

Chodzi o komunikuje swoją intencją od jak powinien być stosowany przedmiot. Na przykład, jeśli twoja metoda oczekuje Mapobiektu o przewidywalnej kolejności iteracji :

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

Następnie, jeśli absolutnie musisz powiedzieć wywołującym powyższej metody, że ona również zwraca Mapobiekt z przewidywalną kolejnością iteracji , ponieważ z jakiegoś powodu istnieje takie oczekiwanie:

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

Oczywiście, osoby wywołujące mogą nadal traktować obiekt zwracany Mapjako taki, ale jest to poza zakresem metody:

private Map<String, String> output = processOrderedMap(input);

Cofając się o krok

Ogólna rada dotycząca kodowania interfejsu ma (ogólnie) zastosowanie, ponieważ zazwyczaj to interfejs zapewnia gwarancję tego, co obiekt powinien być w stanie wykonać, czyli umowę . Wielu początkujących zaczyna od tego HashMap<K, V> map = new HashMap<>()i zaleca się deklarowanie tego jako a Map, ponieważ a HashMapnie oferuje nic więcej niż to, co Mappowinno zrobić. Dzięki temu będą w stanie zrozumieć (miejmy nadzieję), dlaczego ich metody powinny przyjąć Mapzamiast a HashMap, a to pozwoli im zrealizować funkcję dziedziczenia w OOP.

Cytując tylko jedną linijkę z pozycji Wikipedii na ulubioną zasadę wszystkich osób związaną z tym tematem:

Jest to relacja semantyczna, a nie tylko syntaktyczna, ponieważ ma ona na celu zagwarantowanie semantycznej interoperacyjności typów w hierarchii ...

Innymi słowy, użycie Mapdeklaracji nie jest spowodowane tym, że ma sens „składniowo”, ale raczej wywołania obiektu powinny mieć na uwadze, że jest to rodzaj Map.

Kod czyszczący

Uważam, że pozwala mi to czasami pisać czystszy kod, szczególnie jeśli chodzi o testy jednostkowe. Utworzenie HashMappojedynczego wpisu testowego tylko do odczytu zajmuje więcej niż wiersz (wyłączając użycie inicjalizacji podwójnego nawiasu klamrowego), kiedy mogę go łatwo zastąpić Collections.singletonMap().

hjk
źródło