2009-07-01 7 views
5

Załóżmy, że mam następujące rodzaje danych:Dziedziczenie i enkapsulacji klas kolekcji w Javie

class Customer { 
    String id; // unique 
    OtherCustData someOtherData; 
} 

class Service { 
    String url; // unique 
    OtherServiceData someOtherData; 
} 

class LastConnection { 
    Date date; 
    OtherConnData someOtherData; // like request or response 
} 

teraz muszę pamiętać, gdy każdy z odbiorców przyłączonych do każdej z usług.
chciałbym zrobić strukturę:

Map<Customer, Map<Service, LastConnection>> lastConnections; 

Lub, aby móc wyszukiwać według identyfikatorów i nie trzeba pisać wszystkie równe() i hashCode():

Map<String, Map<String, LastConnection>> lastConnections; 

Teraz mogę przejść dane LastConnection przez

LastConnection connection = lastConnections.get(custId).get(srvUrl); 

Wszystko to wydaje się brzydkie, zwłaszcza że muszę przekazać go jako parametry do kilkudziesięciu metod spodziewających mapę map LastConnections, s O myślę tworzenia własnych klas, które mogłoby wyglądać tak:

class CustomerConnections extends HashMap<String, LastConnection> { 
} 

class AllConnections extends HashMap<String, CustomerConnections> { 
    public LastConnection get(String custId, String srvUrl) { 
     return get(custId).get(srvUrl); 
    } 
} 

Ok, dowiedziałem się już, że dziedziczenie jest 3v1l, więc spróbujmy skład:

class CustomerConnections { 
    Map<String, LastConnection> customerConnections; 
    LastConnection get(String srvUrl) { 
     return customerConnections.get(srvUrl); 
    } 
    ... // all other needed operations; 
} 

class AllConnections { 
    Map<String, CustomerConnections> allConnections; 
    public LastConnection get(String custId, String srvUrl) { 
     return get(custId).get(srvUrl); 
    } 
    public CustomerConnection get(String custId) { 
     return allConnections.get(custId); 
    } 
    ... // all other needed operations; 
} 

Problem polega na tym, że Nie jestem pewien, jaki byłby najlepszy sposób przestrzegania zasad SOLID i wszystkich najlepszych praktyk. Tworzenie klas, które nic nie robią, z wyjątkiem rozszerzania już istniejących kolekcji, wydaje się być pomnożeniem podmiotów poza koniecznością, ale uczyniłoby mój kod bardziej przejrzystym (Zwłaszcza, gdy istnieją kolejne poziomy - takie jak Mapa AllConnections według miesiąca i tak dalej). Jakieś wskazówki?

Odpowiedz

5

Tworzenie klas, które nic nie robić wyjątkiem rozszerzenie już istniejących kolekcji wydaje się mnożąc podmioty poza koniecznością

chciałbym zmienić przedłużyć do hermetyzacji. Ukrywasz szczegóły dotyczące przechowywania tych informacji. Klienci Twojej klasy nie muszą wiedzieć, w jaki sposób udostępniasz im historię połączeń z klientami. Myślę, że jest to dobry pomysł, ponieważ możesz zmienić model bazowy bez konieczności zmieniania swojego kodu przez klientów api.

ale spowodowałoby mojego kodu bardziej jasne

To jest wielki i jest to dobry powód, aby to zrobić. YourClass.getCustomerConnection (cId) jest znacznie jaśniejszy niż twójCollection.get (id) .get (id) .getConnection().Musisz ułatwić życie ludziom używającym tego kodu, nawet jeśli jesteś tą osobą.

(Zwłaszcza, gdy są kolejne poziomy - jak mapa AllConnections przez miesiąc i tak dalej)

dobre, to jesteś planowaniu i podejmowaniu kodu rozszerzalny. Co jest dobrą praktyką OO. Moim zdaniem, to, co zrobiłbym, to zwycięstwo, do jakiego doszedłeś.

-1

Można utworzyć klasę, która realizujeMap<K,V> i wewnętrznie przekazywać mapie Pojemnik:

class CustomerConnections implements Map<String,LastConnection> { 
    private Map<String, LastConnection> customerConnections; 

    @Override 
    public LastConnection get(Object srvUrl) { 
     return customerConnections.get(srvUrl); 
    } 
    // all other needed operations; 
} 

Zaletą tego podejścia jest to, że można przejść wokół Map który ma standard, dobrze - zdefiniowana umowa, ale unikasz rozszerzania klas bibliotecznych, co często jest niezrozumiałe.

EDIT: Jak wskazano poniżej, to ma tę wadę, że wymaga do wdrożenia wiele metod, które wykonują niczym delegata do kolekcji bazowej

+2

Poza tym, że niewinny mały komentarz "wszystkie inne potrzebne operacje" ukrywa metryczny przekaz delegacji. –

+0

@Michael Fair point – butterchicken

+1

Chociaż musisz zrobić to tylko raz. A DelegatingMap implementuje Mapę może być użyta jako klasa podstawowa dla wszystkich takich rozszerzeń. – paulcm

0

Dlaczego nie używać custId+"#"+srvurl jako klucza w prostym Map<String, LastConnection>?

Lub użyj klasy Tuple lub Pair jako klucza, który zawiera dwa identyfikatory i narzędzia hashCode() i equals() - "najczystszego" rozwiązania OO.

+0

Użycie custId + "#" + srvUrl jako klucza nie pozwoli mi szybko uzyskać wszystkich połączeń danego klienta. Ale dzięki za pomysł biblioteki Tuple, zagłębię się w to. – Jakub

+0

OK, to nie było częścią pierwotnego opisu problemu; w takim przypadku Mapą Map jest prawdopodobnie najlepsza opcja. –

1

Chciałbym utworzyć dedykowany obiekt do przechowywania tych informacji. To, co tworzysz, to obiekt, a nie prosta kolekcja.

Nie będzie to pochodzić z mapy lub innej dobrze znanej klasy kolekcji, ponieważ semantyka sposobu przechowywania tych informacji może się zmienić w przyszłości.

Zamiast zaimplementować klasę, która powiąże klienta i jego połączenie, a wewnątrz tej klasy należy zastosować odpowiednią klasę Collection (że jesteś na wolności, aby zmienić później bez wpływu na interfejs i resztę swojego kodu)

Twoja klasa klienta/menedżera połączeń to coś więcej niż zwykły kontener. Może przechowywać metadane (np. Kiedy ustalono ten związek). Może wykonywać wyszukiwania w połączeniach z danymi klienta (jeśli potrzebujesz). Może obsługiwać duplikaty w zależności od potrzeb, a nie w jaki sposób obsługuje je podstawowa klasa kolekcji itd. Itd. Możesz w łatwy sposób włączyć debugowanie/rejestrowanie/monitorowanie wydajności, aby łatwo zrozumieć, co się dzieje.

0

Dlaczego utrzymujesz relacje między obiektami poza tymi obiektami.

sugeruję coś jak następuje:

public class Customer 
{ 
    public List<LastConnection> getConnectionHistory() 
    { 
    ... 
    } 

    public List<LastConnection> getConnectionHistory(Service service) 
    { 
    ... 
    } 

    public List<LastConnection> getConnectionHistory(Date since) 
    { 
    ... 
    } 
} 

public class LastConnection 
{ 
    public Date getConnectionTime() 
    { 
    ... 
    } 

    public Service getService() 
    { 
    ... 
    } 
} 

Następnie przechodzą List<Customer> do metod, które wykorzystują tę informację.

+0

Ale wtedy moja klasa Klienta musi wiedzieć o klasie LastConnection. Mogę skończyć z mnóstwem cykli pomiędzy różnymi pakietami. Pożegnanie, możliwość ponownego wykorzystania ... Załóżmy, że w moim kodzie znajdują się cztery pakiety: Core, LastConnections, AddressBook i TransactionHistory (tylko przykłady). Jeśli zrobiłem mypackage.core. Klient wiedział o (i zarządzał) LastConnections, AddressBook i TransactionHistory to stworzy bałagan. Wolę, aby LastConnection wiedział o Kliencie, a Książka Adresowa wie o Kliencie i tak dalej, niż w inny sposób. W ten sposób implementacja następnej funkcjonalności nie zmieni istniejącego kodu. – Jakub

+0

Według paczek zakładam, że masz na myśli słoiki? W tym przypadku zdefiniowałbym klasy domen (jak opisano powyżej) w aplikacji, która używa tych relacji. Klasy domen mogą specjalizować lub otaczać spakowane klasy, moje podejście do hermetyzacji jest ukryte, ponieważ możesz ukryć atrybuty/funkcje pakowanych klas, które nie są istotne dla twojej aplikacji. Zwykle używałem tego, gdy "obiekty wartości" są zaangażowane, ale aplikacja nie powinna widzieć setterów, których eksponują "obiekty wartości". Kolejną korzyścią jest to, że aplikacja jest izolowana, jeśli zmienia się klasa pakowana. –

1

myślę, że należy również rozważyć, w jaki sposób zbiory będą wykorzystywane i jak dane są uzyskane:

  • Jeśli są one proste wynik ustawia mają być wyświetlane na stronie (na przykład), a następnie używanie standardowych zbiorów wydaje się rozsądne - i możesz użyć wielu standardowych bibliotek do manipulowania nimi.
  • Z drugiej strony, jeśli są zmienne, a wszelkie zmiany muszą zostać utrwalone (na przykład), wówczas może być preferowane umieszczanie ich w obrębie własnych klas (które mogą następnie zapisywać zmiany w bazach danych itp.).

Jak uzyskać i utrwalić swoje dane? Jeśli jest on przechowywany w bazie danych, to może możesz użyć SQL do wybrania LastConnections według klienta i/lub usługi i/lub miesiąca, zamiast konieczności samodzielnego utrzymywania danych (i po prostu zwrócić proste listy lub mapy połączeń lub liczby połączeń). A może nie chcesz uruchamiać zapytania dla każdego żądania, więc zachowaj całą bazę danych w pamięci.

Kapsułkowanie jest ogólnie rzeczą dobrą, szczególnie, ponieważ może pomóc ci przylegać do Law of Demeter - może będziesz mógł popchnąć niektóre operacje, które będziesz wykonywać na kolekcjach z powrotem do klasy AllConnections (która jest faktycznie DAO). Może to często pomóc w testowaniu jednostkowym.

Co więcej, dlaczego rozszerzenie HashMap jest uważane za złe, biorąc pod uwagę, że chcesz tylko dodać trywialną metodę pomocnika? W kodzie AllConnections AllConnections zawsze zachowa się w taki sam sposób, jak HashMap - jest polimorficznie substytucyjny. Oczywiście, potencjalnie blokujesz sobie używanie HashMap (zamiast TreeMap, itp.), Ale to prawdopodobnie nie ma znaczenia, ponieważ ma takie same publiczne metody, jak Map. Jednak to, czy rzeczywiście chcesz to zrobić, naprawdę zależy od tego, w jaki sposób zamierzasz korzystać z kolekcji - po prostu nie sądzę, że powinieneś automatycznie zdyskontować dziedziczenie dziedziczenia jako zawsze złe (to po prostu zwykle jest!)

class AllConnections extends HashMap<String, CustomerConnections> { 
    public LastConnection get(String custId, String srvUrl) { 
     return get(custId).get(srvUrl); 
    } 
}