2014-12-31 32 views
8

Mam aplikację, która używa bazy danych (MongoDB) do przechowywania informacji. W przeszłości używałem klasy pełnej statycznych metod do zapisywania i pobierania danych, ale od tego czasu zdałem sobie sprawę, że nie jest to obiekt bardzo zorientowany obiektowo, czy też przyszły dowód.Wzorce projektowe dla warstwy dostępu do danych

Chociaż jest bardzo mało prawdopodobne, że zmienię bazę danych wolę coś, co nie przywiązuje mnie zbyt mocno do Mongo. Chciałbym również móc buforować wyniki z opcją odświeżenia buforowanego obiektu z bazy danych, ale nie jest to konieczne i można to zrobić w innym miejscu.

Przyjrzałem się obiektom dostępu do danych, ale nie wydają się one bardzo dobrze zdefiniowane i nie mogę znaleźć dobrych przykładów implementacji (w Javie lub podobnym języku). Mam również wiele nietypowych przypadków, takich jak znajdowanie nazw użytkowników w celu uzupełnienia tabu, które wydają się niezbyt odpowiednie i spowodowałyby, że DAO byłby duży i nadęty.

Czy istnieją wzorce projektowe, które ułatwiłyby uzyskiwanie i zapisywanie obiektów bez zbytniej bazy danych? Dobre przykłady implementacji byłyby pomocne (najlepiej w Javie).

Odpowiedz

33

Cóż, wspólne podejście do przechowywania danych w java jest, jak zauważyłeś, zupełnie nie obiektowe. To samo w sobie nie jest ani złe ani dobre: ​​"zorientowanie na obiekt" nie jest ani zaletą, ani wadą, jest tylko jednym z wielu paradygmatów, które czasami pomagają w dobrym projektowaniu architektury (a czasami nie).

Powodem, dla którego DAO w Javie nie są zwykle obiektowo zorientowane, jest dokładnie to, co chcesz osiągnąć - rozluźniając zależność od bazy danych. W lepiej zaprojektowanym języku, który pozwala na wielokrotne dziedziczenie, ten lub oczywiście może być wykonany bardzo elegancko w sposób zorientowany obiektowo, ale w przypadku java, wydaje się, że jest to więcej kłopotu, niż jest to warte.

W szerszym znaczeniu, podejście inne niż OO pomaga oddzielić dane na poziomie aplikacji od sposobu ich przechowywania. To więcej niż (nie) zależność od specyfiki konkretnej bazy danych, ale także schematy przechowywania, co jest szczególnie ważne w przypadku korzystania z relacyjnych baz danych (nie zaczynaj mnie od ORM): możesz mieć dobrze zaprojektowany schemat relacyjny płynnie przetłumaczone na model OO aplikacji przez Twojego DAO.

Tak więc, obecnie większość DAO w Javie jest zasadniczo tym, o czym wspomniałeś na początku - klasami pełnymi metod statycznych. Jedną z różnic jest to, że zamiast tworzyć wszystkie metody statyczne, lepiej jest mieć pojedynczą statyczną "metodę fabryczną" (prawdopodobnie w innej klasie), która zwraca (pojedynczą) instancję twojego DAO, która implementuje określony interfejs , używane przez kod aplikacji w celu uzyskania dostępu do bazy danych:

public interface GreatDAO { 
    User getUser(int id); 
    void saveUser(User u); 
} 
public class TheGreatestDAO implements GreatDAO { 
    protected TheGeatestDAO(){} 
    ... 
} 
public class GreatDAOFactory { 
    private static GreatDAO dao = null; 
    protected static synchronized GreatDao setDAO(GreatDAO d) { 
     GreatDAO old = dao; 
     dao = d; 
     return old; 
    } 
    public static synchronized GreatDAO getDAO() { 
     return dao == null ? dao = new TheGreatestDAO() : dao; 
    } 
} 

public class App { 
    void setUserName(int id, String name) { 
      GreatDAO dao = GreatDAOFactory.getDao(); 
      User u = dao.getUser(id); 
      u.setName(name); 
      dao.saveUser(u); 
    } 
} 

Dlaczego to działa w odróżnieniu od metod statycznych? A co, jeśli zdecydujesz się przełączyć na inną bazę danych? Naturalnie stworzysz nową klasę DAO, implementującą logikę dla nowego magazynu. Jeśli używasz metod statycznych, będziesz musiał przejść przez cały swój kod, uzyskując dostęp do DAO i zmienić go na nową klasę, prawda? To może być ogromny ból. A co jeśli zmienisz zdanie i chcesz wrócić do starej bazy danych?

Przy takim podejściu wystarczy zmienić GreatDAOFactory.getDAO() i utworzyć instancję innej klasy, a cały kod aplikacji będzie korzystał z nowej bazy danych bez żadnych zmian.

W rzeczywistości jest to często wykonywane bez żadnych zmian kodu: metoda fabryczna pobiera nazwę klasy implementacji za pomocą ustawienia właściwości i tworzy ją za pomocą refleksji, więc wszystko, co musisz zrobić, aby zmienić implementacje edytuje plik właściwości.W rzeczywistości istnieją struktury - takie jak spring lub guice - które zarządzają tym mechanizmem "zastrzyku zależności", ale nie będę wchodził w szczegóły, po pierwsze, ponieważ jest to naprawdę poza zakresem twojego pytania, a także, ponieważ nie jestem koniecznie przekonani, że korzyści wynikające z korzystania z tych frameworków warte są integracji z większością aplikacji.

Inną (prawdopodobnie bardziej prawdopodobną korzyścią) zaletą tego "podejścia fabrycznego" w przeciwieństwie do statycznego jest testowalność. Wyobraź sobie, że piszesz test jednostkowy, który powinien przetestować logikę twojej klasy App niezależnie od jakiegokolwiek bazowego DAO. Nie chcesz, aby korzystał z jakiejkolwiek rzeczywistej pamięci bazowej z kilku powodów (prędkość, konieczność konfiguracji i czyszczenia słów, możliwe kolizje z innymi testami, możliwość zanieczyszczania wyników testów z problemami w DAO, niezwiązane z App, które jest faktycznie testowany itp.).

Aby to zrobić, potrzebujesz szkieletu testowego, takiego jak Mockito, który pozwala "wyśmiewać" funkcjonalność dowolnego obiektu lub metody, zastępując go obiektem "sztucznym", z predefiniowanym zachowaniem (pomijam szczegóły, ponieważ to znowu jest poza zakresem). Tak więc, możesz stworzyć ten sztuczny obiekt zastępując DAO i sprawić, że GreatDAOFactory zwróci twój manekin zamiast prawdziwego przez wywołanie GreatDAOFactory.setDAO(dao) przed testem (i przywrócenie go po). Jeśli używałbyś statycznych metod zamiast klasy instancji, nie byłoby to możliwe.

Jeszcze jedna korzyść, podobna do zamiany baz danych opisanych powyżej, polega na "uzupełnianiu" twojego dao dodatkową funkcjonalnością. Załóżmy, że aplikacja staje się wolniejsza, ponieważ ilość danych w bazie danych rośnie, a Ty decydujesz, że potrzebujesz warstwy pamięci podręcznej. Zaimplementuj klasę opakowującą, która używa rzeczywistej instancji dao (udostępnionej jako parametr konstruktora), aby uzyskać dostęp do bazy danych i buforuje obiekty, które odczytuje w pamięci, aby mogły być szybciej zwrócone. Następnie można utworzyć instancję tworzenia tego opakowania, aby aplikacja mogła z niego skorzystać.

(To się nazywa "wzorzec delegacji" ... i wydaje się być bólem w tyłku, szczególnie gdy masz wiele metod zdefiniowanych w twoim DAO: będziesz musiał wdrożyć je wszystkie w opakowaniu, nawet zmienić zachowanie tylko jednego.Alternatywnie, możesz po prostu podklasować dao i dodać buforowanie do niego w ten sposób.To byłoby o wiele mniej nudne kodowanie z góry, ale może stać się problematyczne, gdy zdecydujesz się zmienić bazę danych, lub, co gorsza, mieć możliwość przełączania implementacji tam iz powrotem).

Jeden równie szeroko stosowane (ale, moim zdaniem, gorsze) alternatywą dla „fabrycznej” metody dokonywania dao zmiennej członek We wszystkich klasach, które potrzebują go:

public class App { 
    GreatDao dao; 
    public App(GreatDao d) { dao = d; } 
} 

ten sposób kod że tworzenie instancji tych klas wymaga instancji obiektu dao (może nadal używać fabryki) i dostarczenia go jako parametru konstruktora. Schematy wtrysku zależności, o których wspomniałem powyżej, zwykle robią coś podobnego do tego.

Daje to wszystkie korzyści z metody "metody fabrycznej", którą opisałem wcześniej, ale, jak powiedziałem, nie jest tak dobre w mojej opinii. Wadami są tutaj: napisanie konstruktora dla każdej z klas aplikacji, wykonanie dokładnie tego samego i ponad, a także niemożność łatwego tworzenia instancji klas w razie potrzeby oraz utraconej czytelności: z wystarczająco dużą bazą kodu , czytelnik twojego kodu, nieznajomy z nim, będzie miał trudności ze zrozumieniem, która aktualna implementacja dao jest używana, jak jest tworzona, czy jest singletonem, implementacją wątkową, czy utrzymuje stan, czy pamięci podręczne cokolwiek, jak podejmowane są decyzje dotyczące wyboru konkretnej implementacji itp.

+0

Rzeczywiście zwrócę '' 'boolean''' dla operacji składowania/usuwania, aby wskazać, że trwa na dysku/baza danych została wykonana pomyślnie. – ambodi

+1

Nie, chcesz rzucić wyjątek, jeśli i tak się nie powiedzie: pozwala ci przekazać wiele cennych informacji o niepowodzeniu z powrotem do rozmówcy, w porównaniu do zwykłego zwrócenia fałszywego, a także sprawia, że ​​obsługa błędów po stronie dzwoniącego jest mniej kłopotliwa : zamiast sprawdzać status każdego połączenia i rozprzestrzeniać go do stosu, będą musieli wychwycić wyjątek tylko raz, na poziomie, na którym należy go obsłużyć. – Dima

+1

To jest niedoceniona odpowiedź, dobrze zrobione! Jedno pytanie, dlaczego 'setDAO' zwraca starsze/poprzednie DAO zamiast po prostu nic nie zwracać? Jak by to wykorzystać? –