MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Modelli di progettazione per il livello di accesso ai dati

Bene, l'approccio comune all'archiviazione dei dati in Java è, come hai notato, non molto orientato agli oggetti. Questo di per sé non è né cattivo né buono:"l'orientamento agli oggetti" non è né vantaggio né svantaggio, è solo uno dei tanti paradigmi, che a volte aiuta con una buona progettazione architettonica (a volte no).

Il motivo per cui i DAO in Java non sono solitamente orientati agli oggetti è esattamente ciò che si desidera ottenere:rilassare la dipendenza dal database specifico. In un linguaggio meglio progettato, che ha consentito l'ereditarietà multipla, questo, o ovviamente, può essere fatto in modo molto elegante in un modo orientato agli oggetti, ma con Java sembra essere più problematico di quanto valga la pena.

In un senso più ampio, l'approccio non OO aiuta a disaccoppiare i dati a livello di applicazione dal modo in cui sono archiviati. Questo è più della (non) dipendenza dalle specifiche di un particolare database, ma anche dagli schemi di archiviazione, che è particolarmente importante quando si utilizzano database relazionali (non farmi iniziare su ORM):puoi avere uno schema relazionale ben progettato perfettamente tradotto nel modello OO dell'applicazione dal tuo DAO.

Quindi, ciò che la maggior parte dei DAO sono in Java al giorno d'oggi sono essenzialmente ciò che hai menzionato all'inizio:classi, piene di metodi statici. Una differenza è che, invece di rendere statici tutti i metodi, è meglio avere un singolo "metodo di fabbrica" ​​statico (probabilmente, in una classe diversa), che restituisce un'istanza (singola) del tuo DAO, che implementa una particolare interfaccia , utilizzato dal codice dell'applicazione per accedere al database:

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);
     }
}

Perché farlo in questo modo rispetto ai metodi statici? E se decidessi di passare a un database diverso? Naturalmente, creeresti una nuova classe DAO, implementando la logica per il tuo nuovo storage. Se stavi usando metodi statici, ora dovresti esaminare tutto il tuo codice, accedere al DAO e cambiarlo per usare la tua nuova classe, giusto? Questo potrebbe essere un dolore enorme. E se poi cambiassi idea e volessi tornare al vecchio db?

Con questo approccio, tutto ciò che devi fare è modificare GreatDAOFactory.getDAO() e crea un'istanza di una classe diversa e tutto il codice dell'applicazione utilizzerà il nuovo database senza alcuna modifica.

Nella vita reale, questo viene spesso fatto senza alcuna modifica al codice:il metodo factory ottiene il nome della classe di implementazione tramite un'impostazione di proprietà e lo istanzia usando la riflessione, quindi tutto ciò che devi fare per cambiare implementazione è modificare una proprietà file. In realtà ci sono framework, come spring o guice - che gestiscono questo meccanismo di "iniezione di dipendenza" per te, ma non entrerò nei dettagli, prima, perché è davvero oltre lo scopo della tua domanda, e anche, perché non sono necessariamente convinto che il vantaggio che ottieni dall'uso vale la pena integrare questi framework per la maggior parte delle applicazioni.

Un altro vantaggio (probabilmente, più probabilmente sfruttato) di questo "approccio di fabbrica" ​​rispetto a quello statico è la testabilità. Immagina di scrivere uno unit test, che dovrebbe testare la logica della tua App classe indipendentemente da qualsiasi DAO sottostante. Non vuoi che utilizzi alcuna memoria reale sottostante per diversi motivi (velocità, doverlo configurare e ripulire le postfazioni, possibili collisioni con altri test, possibilità di inquinare i risultati dei test con problemi in DAO, non correlato a App , che è effettivamente in fase di test, ecc.).

Per fare ciò, vuoi un framework di test, come Mockito , che ti consente di "modificare" la funzionalità di qualsiasi oggetto o metodo, sostituendolo con un oggetto "fittizio", con un comportamento predefinito (salto i dettagli, perché anche questo va oltre lo scopo). Quindi, puoi creare questo oggetto fittizio sostituendo il tuo DAO e creare il GreatDAOFactory restituisci il tuo manichino invece di quello reale chiamando GreatDAOFactory.setDAO(dao) prima del test (e ripristinandolo dopo). Se utilizzassi metodi statici invece della classe di istanza, ciò non sarebbe possibile.

Un altro vantaggio, che è un po' simile al cambio di database che ho descritto sopra, è "potenziare" il tuo dao con funzionalità aggiuntive. Supponiamo che la tua applicazione diventi più lenta all'aumentare della quantità di dati nel database e tu decida di aver bisogno di un livello di cache. Implementare una classe wrapper, che utilizza l'istanza dao reale (fornita come parametro del costruttore) per accedere al database e memorizza nella cache gli oggetti che legge in memoria, in modo che possano essere restituiti più velocemente. Puoi quindi creare il tuo GreatDAOFactory.getDAO istanziare questo wrapper, affinché l'applicazione ne tragga vantaggio.

(Questo è chiamato "modello di delega" ... e sembra rompiscatole, specialmente quando hai molti metodi definiti nel tuo DAO:dovrai implementarli tutti nel wrapper, anche per alterare il comportamento di uno solo In alternativa, potresti semplicemente sottoclassare il tuo dao e aggiungere la memorizzazione nella cache in questo modo. Questo sarebbe molto meno noioso di codificare in anticipo, ma potrebbe diventare problematico quando decidi di cambiare il database o, peggio, di avere un'opzione di cambiando implementazioni avanti e indietro.)

Un'alternativa ugualmente ampiamente utilizzata (ma, a mio parere, inferiore) al metodo "fabbrica" ​​è la creazione di dao una variabile membro in tutte le classi che ne hanno bisogno:

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

In questo modo, il codice che crea un'istanza di queste classi deve creare un'istanza dell'oggetto dao (potrebbe ancora utilizzare il factory) e fornirlo come parametro del costruttore. I framework di iniezione delle dipendenze che ho menzionato sopra, di solito fanno qualcosa di simile a questo.

Questo fornisce tutti i vantaggi dell'approccio del "metodo di fabbrica", che ho descritto in precedenza, ma, come ho detto, non è così buono secondo me. Gli svantaggi qui sono dover scrivere un costruttore per ciascuna delle classi dell'app, fare la stessa identica cosa ancora e ancora, e anche non essere in grado di istanziare facilmente le classi quando necessario e una certa leggibilità persa:con una base di codice sufficientemente ampia , un lettore del tuo codice, che non ha familiarità con esso, avrà difficoltà a capire quale implementazione effettiva di dao viene utilizzata, come viene istanziata, se si tratta di un singleton, un'implementazione thread-safe, se mantiene lo stato o memorizza nella cache qualsiasi cosa, come vengono prese le decisioni sulla scelta di una particolare implementazione, ecc.