Dobra czy zła praktyka maskowania zbiorów Java znaczącymi nazwami klas?

46

Ostatnio miałem zwyczaj „maskowania” kolekcji Java za pomocą przyjaznych dla ludzi nazw klas. Kilka prostych przykładów:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

Lub:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

Kolega zwrócił mi uwagę, że jest to zła praktyka i wprowadza opóźnienie / opóźnienie, a także jest anty-wzorem OO. Rozumiem, że wprowadza bardzo niewielki poziom wydajności, ale nie mogę sobie wyobrazić, że to w ogóle znaczące. Pytam więc: czy to dobrze czy źle i dlaczego?

herpylderp
źródło
11
To o wiele prostsze niż to. To zła praktyka, ponieważ wyobrażam sobie, że rozszerzasz implementacje podstawowej kolekcji JDK Java. W Javie możesz rozszerzyć tylko jedną klasę, więc musisz myśleć i projektować więcej, gdy masz rozszerzenie. W Javie używaj rozszerzaj oszczędnie.
InformedA
ChangeListkompilacja ulegnie awarii extends, ponieważ List jest interfejsem, wymaga implements. @randomA to, co sobie wyobrażasz, nie trafia w sedno z powodu tego błędu
zgryź
@gnat Nie brakuje tu sensu, zakładam, że przedłużał on implementationie HashMaplub, TreeMapa to, co miał, było literówką.
Poinformowano
4
To jest ZŁA praktyka. ZŁY ZŁY ZŁY. Nie rób tego Każdy wie, czym jest mapa <ciąg, widżet>. Ale WidgetCache? Teraz muszę otworzyć WidgetCache.java, muszę pamiętać, że WidgetCache to tylko mapa. Muszę sprawdzać za każdym razem, gdy pojawia się nowa wersja, czy nie dodałeś nic nowego do WidgetCache. Boże nie, nigdy tego nie rób.
Miles Rout
„Gdybyś widział ArrayList <ArrayList <? >> przekazywaną w kodzie, czy uciekłbyś krzycząc…?” Nie, czuję się dobrze z zagnieżdżonymi kolekcjami rodzajowymi. Ty też powinieneś być.
Kevin Krumwiede

Odpowiedzi:

75

Opóźnienie / opóźnienie? W tej sprawie nazywam BS. Z tej praktyki powinno być dokładnie zero. ( Edycja: W komentarzach zaznaczono, że może to w rzeczywistości hamować optymalizacje wykonywane przez maszynę wirtualną HotSpot. Nie wiem wystarczająco dużo o implementacji maszyny wirtualnej, aby to potwierdzić lub zaprzeczyć. Opierałem swój komentarz na C ++ implementacja funkcji wirtualnych).

Istnieje pewien narzut kodu. Musisz utworzyć wszystkie konstruktory z pożądanej klasy podstawowej, przekazując ich parametry.

Nie postrzegam tego też jako anty-wzoru. Uważam to jednak za straconą okazję. Zamiast tworzyć klasę, która wyprowadza klasę podstawową tylko ze względu na zmianę nazwy, może zamiast tego stworzysz klasę, która zawiera kolekcję i oferuje ulepszony interfejs dla poszczególnych przypadków? Czy pamięć podręczna widgetów naprawdę powinna oferować pełny interfejs mapy? A może powinien oferować wyspecjalizowany interfejs?

Ponadto w przypadku kolekcji wzorzec po prostu nie działa razem z ogólną zasadą używania interfejsów, a nie implementacji - to znaczy, że w zwykłym kodzie kolekcji należy utworzyć HashMap<String, Widget>a następnie przypisać ją do zmiennej typu Map<String, Widget>. Twoja WidgetCachepuszka nie przedłużać Map<String, Widget>, bo to interfejs. Nie może być interfejsem rozszerzającym interfejs podstawowy, ponieważ HashMap<String, Widget>nie implementuje tego interfejsu, podobnie jak żadna inna standardowa kolekcja. I chociaż możesz uczynić go klasą rozszerzającą się HashMap<String, Widget>, musisz następnie zadeklarować zmienne jako WidgetCachelub Map<String, Widget>, a pierwsza utraci elastyczność zastępowania innej kolekcji (być może kolekcji leniwego ładowania ORM), podczas gdy drugi rodzaj pokona punkt posiadania klasy.

Niektóre z tych kontrapunktów dotyczą także mojej zaproponowanej klasy specjalistycznej.

To są wszystkie kwestie do rozważenia. Może to być właściwy wybór. W obu przypadkach argumenty oferowane przez twojego kolegę są nieważne. Jeśli myśli, że to anty-wzór, powinien to nazwać.

Sebastian Redl
źródło
1
Zauważ jednak, że chociaż jest to całkiem możliwe, aby mieć wpływ na wydajność, nie będzie to takie straszne (pominięcie niektórych optymalizacji i uzyskanie dodatkowego połączenia w najgorszym przypadku - nie świetne w wewnętrznej pętli, ale w przeciwnym razie prawdopodobnie w porządku).
Voo
5
Ta naprawdę dobra odpowiedź mówi wszystkim: „nie oceniaj decyzji na podstawie wydajności” - a jedyną dyskusją poniżej jest „jak HotSpot sobie z tym radzi, czy niektóre (w 99,9% wszystkich przypadków) nie mają znaczenia wpływ na wydajność” - chłopaki, czy w ogóle próbowałeś przeczytać więcej niż pierwsze zdanie?
Doc Brown
16
+1 za „Zamiast tworzyć klasę, która wyprowadza klasę podstawową tylko ze względu na zmianę nazwy, może stworzysz klasę, która zawiera kolekcję i oferuje ulepszony interfejs dla poszczególnych przypadków?”
user11153
6
Praktyczny przykład ilustrujący główny punkt tej odpowiedzi: Dokumentacja dla public class Properties extends Hashtable<Object,Object>in java.utilmówi: „Ponieważ właściwości dziedziczą z Hashtable, metody put i putAll można zastosować do obiektu właściwości. Ich użycie jest zdecydowanie odradzane, ponieważ pozwalają wywołującemu wstawiać wpisy, których klucze lub wartości nie są ciągami. ”. Kompozycja byłaby znacznie czystsza.
Patricia Shanahan
3
@DocBrown Nie, ta odpowiedź twierdzi, że nie ma żadnego wpływu na wydajność. Jeśli wydasz mocne oświadczenie, lepiej będziesz mógł je wesprzeć, w przeciwnym razie ludzie prawdopodobnie cię w tym wydzwaniają. Cały komentarz (odpowiedzi) ma na celu wskazanie niedokładnych faktów lub założeń lub dodanie użytecznych notatek, ale z pewnością nie gratulowanie ludziom, po to jest system głosowania.
Voo
25

Według IBM jest to w rzeczywistości anty-wzorzec. Te klasy „typedef” nazywane są typami psuedo.

Artykuł wyjaśnia to znacznie lepiej niż ja, ale postaram się to podsumować na wypadek, gdyby link się złączył:

  • Każdy kod, który oczekuje, że WidgetCachenie może obsłużyćMap<String, Widget>
  • Te pseudotypy są „wirusowe”, gdy używają wielu pakietów, prowadzą do niezgodności, podczas gdy typ podstawowy (tylko głupia mapa <...>) działałby we wszystkich przypadkach we wszystkich pakietach.
  • Pseudo typy są często zbyt konkretne, nie implementują określonych interfejsów, ponieważ ich klasy podstawowe implementują tylko wersję ogólną.

W artykule proponują następującą sztuczkę, aby ułatwić życie bez używania pseudo typów:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

Który działa z powodu automatycznego wnioskowania typu.

(Doszedłem do tej odpowiedzi przez to pytanie dotyczące przepełnienia stosu)

Roy T.
źródło
13
Czy newHashMapoperator diamentów nie rozwiązuje teraz potrzeby?
svick
Absolutnie prawda, zapomniałem o tym. Zwykle nie pracuję w Javie.
Roy T.
Chciałbym, aby języki pozwalały na uniwersum typów zmiennych, które zawierałyby odniesienia do instancji innych typów, ale nadal mogły obsługiwać sprawdzanie czasu kompilacji, tak aby wartość typu WidgetCachemogła być przypisana do zmiennej typu WidgetCachelub Map<String,Widget>bez rzutowania, ale tam był środkiem, za pomocą którego metoda statyczna WidgetCachemogła odwoływać się Map<String,Widget>i zwracać go jako typ WidgetCachepo wykonaniu dowolnej pożądanej weryfikacji. Taka funkcja może być szczególnie przydatna w przypadku leków generycznych, których nie trzeba by było kasować (ponieważ ...
supercat,
... typy i tak istniałyby tylko w umyśle kompilatora). W przypadku wielu typów zmiennych istnieją dwa powszechne typy pól referencyjnych: jedno, które zawiera jedyne odwołanie do instancji, która może być zmutowana, lub jedno, które zawiera współdzielone odwołanie do instancji, której nikt nie może mutować. Przydałaby się możliwość nadania różnych nazw tym dwóm rodzajom pól, ponieważ wymagają one różnych wzorców użycia, ale oba powinny zawierać odniesienia do tych samych typów instancji obiektów.
supercat
5
To świetny argument na to, dlaczego Java potrzebuje aliasów typów.
GlenPeterson,
13

Skuteczność działania byłaby co najwyżej ograniczona do wyszukiwania vtable, które najprawdopodobniej już ponosisz. To nie jest uzasadniony powód do sprzeciwu.

Sytuacja jest na tyle powszechna, że ​​większość wszystkich statycznie typowanych języków programowania ma specjalną składnię dla typów aliasingu, zwykle nazywaną a typedef. Java prawdopodobnie ich nie skopiowała, ponieważ pierwotnie nie miała sparametryzowanych typów. Rozszerzenie klasy nie jest idealne z powodów, dla których Sebastian tak dobrze opisał swoją odpowiedź, ale może to być rozsądne obejście ograniczonej składni Javy.

Typedef ma wiele zalet. Wyrażają intencje programisty jaśniej, lepszą nazwą, na bardziej odpowiednim poziomie abstrakcji. Łatwiej jest je wyszukiwać w celach debugowania lub refaktoryzacji. Zastanów się nad znalezieniem wszędzie WidgetCacheużywanego znaku a zamiast znalezieniem tych konkretnych zastosowań Map. Łatwiej je zmienić, na przykład jeśli później okaże się, że potrzebujesz LinkedHashMapzamiast tego, a nawet własnego niestandardowego kontenera.

Karl Bielefeldt
źródło
Konstruktory mają również niewielki narzut.
Stephen C
11

Sugeruję, jak już wspomniano, użycie kompozycji zamiast dziedziczenia, abyś mógł udostępnić tylko te metody, które są naprawdę potrzebne, z nazwami pasującymi do zamierzonego przypadku użycia. Czy użytkownicy Twojej klasy naprawdę muszą wiedzieć, że WidgetCacheto mapa? I być w stanie zrobić z nim wszystko, czego chcą? A może po prostu muszą wiedzieć, że jest to pamięć podręczna dla widżetów?

Przykładowa klasa z mojej bazy kodu z rozwiązaniem podobnego problemu, który opisałeś:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

Widać, że wewnętrznie jest to „mapa”, ale interfejs publiczny tego nie pokazuje. I ma metody „przyjazne dla programistów”, takie jak appendMessage(Locale locale, String code, String message)łatwiejszy i bardziej sensowny sposób wstawiania nowych wpisów. Użytkownicy klasy nie mogą na przykład tego zrobić translations.clear(), ponieważ Translationsnie rozszerzają się Map.

Opcjonalnie zawsze możesz przekazać niektóre potrzebne metody wewnętrznie używanej mapie.

użytkownik11153
źródło
6

Widzę to jako przykład znaczącej abstrakcji. Dobra abstrakcja ma kilka cech:

  1. Ukrywa szczegóły implementacji, które nie mają znaczenia dla używającego go kodu.

  2. Jest tak skomplikowany, jak musi być.

Rozszerzając, ujawniasz cały interfejs rodzica, ale w wielu przypadkach większość z nich może być lepiej ukryta, więc chcesz robić to, co sugeruje Sebastian Redl, preferować kompozycję zamiast dziedziczenia i dodać instancję rodzica jako prywatny członek twojej klasy niestandardowej. Dowolną metodę interfejsu, która ma sens dla twojej abstrakcji, można łatwo delegować do (w twoim przypadku) wewnętrznej kolekcji.

Jeśli chodzi o wpływ na wydajność, zawsze dobrym pomysłem jest najpierw zoptymalizować kod pod kątem czytelności, a jeśli podejrzewa się wpływ na wydajność, profiluj kod, aby porównać dwie implementacje.

Mike Partridge
źródło
4

+1 do innych odpowiedzi tutaj. Dodam też, że społeczność DDD (Domain Driven Design) uważa to za bardzo dobrą praktykę. Opowiadają się za tym, aby twoja domena i interakcje z nią miały znaczenie semantyczne, w przeciwieństwie do podstawowej struktury danych. Może Map<String, Widget>to być pamięć podręczna, ale może to być także coś innego. Prawidłowo zrobione w My Not So Humble Opinion (IMNSHO) jest modelowanie tego, co reprezentuje kolekcja, w tym przypadku pamięć podręczna.

Dodam ważną zmianę w tym, że opakowanie klasy domeny wokół podstawowej struktury danych powinno prawdopodobnie mieć również inne zmienne składowe lub funkcje, które naprawdę sprawiają, że jest to klasa domeny z interakcjami, a nie tylko struktura danych (gdyby tylko Java miała typy wartości , dostaniemy je w Javie 10 - obietnica!)

Ciekawie będzie zobaczyć, jaki wpływ na to wszystko będą miały strumienie Java 8, mogę sobie wyobrazić, że być może niektóre publiczne interfejsy wolą radzić sobie ze Strumieniem (wstaw zwykłą prymityw Java lub Łańcuchem) w przeciwieństwie do obiektu Java.

Martijn Verburg
źródło