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 TreeMap
i, 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 Map
interfejsie (i kilku innych, którzy również są obecni w Map
interfejsie). 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.
źródło
Odpowiedzi:
„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
.źródło
TreeMap
jako przykład. „Program do interfejsu” nie powinien być traktowany dosłownie jako interfejs lub klasa abstrakcyjna - implementację można również uznać za interfejs.public interface
„publiczne metody implementacji” .TreeMap
przyniosłoby to ponadSortedMap
lubNavigableMap
Jeśli nadal chcesz użyć interfejsu, którego możesz uż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
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
HashMap
i ujawniła to.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, żefoo
jest to mapa, powinieneś ją zwrócić.Zapewnia to elastyczność zmiany sposobu działania kodu. Zdajesz sobie sprawę, że
foo
tak naprawdę musi to być mapa, która zwraca rzeczy w określonej kolejności.I to niczego nie psuje przez resztę kodu.
Możesz później wzmocnić kontrakt, który daje klasa, nie przerywając niczego.
Dotyczy to zasady substytucji Liskowa
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.ConcurrentSkipListMap
a niejava.util.TreeMap
w instrukcjach importu lub cokolwiek innego.źródło
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.
źródło
Chodzi o komunikuje swoją intencją od jak powinien być stosowany przedmiot. Na przykład, jeśli twoja metoda oczekuje
Map
obiektu o przewidywalnej kolejności iteracji :Następnie, jeśli absolutnie musisz powiedzieć wywołującym powyższej metody, że ona również zwraca
Map
obiekt z przewidywalną kolejnością iteracji , ponieważ z jakiegoś powodu istnieje takie oczekiwanie:Oczywiście, osoby wywołujące mogą nadal traktować obiekt zwracany
Map
jako taki, ale jest to poza zakresem metody: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 aMap
, ponieważ aHashMap
nie oferuje nic więcej niż to, coMap
powinno zrobić. Dzięki temu będą w stanie zrozumieć (miejmy nadzieję), dlaczego ich metody powinny przyjąćMap
zamiast aHashMap
, 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:
Innymi słowy, użycie
Map
deklaracji nie jest spowodowane tym, że ma sens „składniowo”, ale raczej wywołania obiektu powinny mieć na uwadze, że jest to rodzajMap
.Kod czyszczący
Uważam, że pozwala mi to czasami pisać czystszy kod, szczególnie jeśli chodzi o testy jednostkowe. Utworzenie
HashMap
pojedynczego 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()
.źródło