MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Entwurfsmuster für die Datenzugriffsschicht

Nun, der übliche Ansatz zur Datenspeicherung in Java ist, wie Sie bemerkt haben, überhaupt nicht sehr objektorientiert. Das ist an und für sich weder schlecht noch gut:„Objektorientierung“ ist weder Vor- noch Nachteil, es ist nur eines von vielen Paradigmen, das manchmal bei gutem Architekturdesign hilft (und manchmal nicht).

Der Grund, warum DAOs in Java normalerweise nicht objektorientiert sind, ist genau das, was Sie erreichen möchten - Ihre Abhängigkeit von der Datenbank zu lockern. In einer besser gestalteten Sprache, die Mehrfachvererbung zulässt, kann dies natürlich sehr elegant auf objektorientierte Weise erfolgen, aber mit Java scheint es einfach mehr Mühe zu geben, als es wert ist.

Im weiteren Sinne trägt der Non-OO-Ansatz dazu bei, Ihre Daten auf Anwendungsebene von der Art und Weise, wie sie gespeichert werden, zu entkoppeln. Dies ist mehr als (keine) Abhängigkeit von den Besonderheiten einer bestimmten Datenbank, sondern auch von Speicherschemata, was besonders wichtig ist, wenn relationale Datenbanken verwendet werden (lassen Sie mich nicht mit ORM beginnen):Sie können ein gut gestaltetes relationales Schema haben nahtlos in das Anwendungs-OO-Modell von Ihrem DAO übersetzt.

Was die meisten DAOs heutzutage in Java sind, sind im Wesentlichen das, was Sie am Anfang erwähnt haben - Klassen voller statischer Methoden. Ein Unterschied besteht darin, dass es besser ist, anstatt alle Methoden statisch zu machen, eine einzelne statische "Factory-Methode" (wahrscheinlich in einer anderen Klasse) zu haben, die eine (einzelne) Instanz Ihres DAO zurückgibt, die eine bestimmte Schnittstelle implementiert , wird vom Anwendungscode verwendet, um auf die Datenbank zuzugreifen:

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

Warum so im Gegensatz zu statischen Methoden? Nun, was ist, wenn Sie sich entscheiden, zu einer anderen Datenbank zu wechseln? Natürlich würden Sie eine neue DAO-Klasse erstellen und die Logik für Ihren neuen Speicher implementieren. Wenn Sie statische Methoden verwenden, müssten Sie jetzt Ihren gesamten Code durchgehen, auf das DAO zugreifen und es ändern, um Ihre neue Klasse zu verwenden, richtig? Dies könnte ein großer Schmerz sein. Und was ist, wenn Sie Ihre Meinung ändern und zurück zur alten db wechseln möchten?

Bei diesem Ansatz müssen Sie lediglich GreatDAOFactory.getDAO() ändern und erstellen Sie eine Instanz einer anderen Klasse, und Ihr gesamter Anwendungscode verwendet die neue Datenbank ohne Änderungen.

Im wirklichen Leben geschieht dies häufig ohne jegliche Änderungen am Code:Die Factory-Methode erhält den Namen der Implementierungsklasse über eine Eigenschaftseinstellung und instanziiert ihn mithilfe von Reflektion, sodass Sie zum Wechseln der Implementierungen nur eine Eigenschaft bearbeiten müssen Datei. Es gibt tatsächlich Frameworks - wie spring oder guice - die diesen "Abhängigkeitsinjektions" -Mechanismus für Sie verwalten, aber ich werde nicht ins Detail gehen, erstens, weil es den Rahmen Ihrer Frage wirklich sprengt, und auch, weil ich nicht unbedingt davon überzeugt bin, dass Sie davon profitieren Diese Frameworks sind für die meisten Anwendungen die Mühe wert, sich mit ihnen zu integrieren.

Ein weiterer (wahrscheinlich eher zu nutzender) Vorteil dieses "Factory-Ansatzes" im Gegensatz zu Static ist die Testbarkeit. Stellen Sie sich vor, Sie schreiben einen Unit-Test, der die Logik Ihrer App testen soll Klasse unabhängig von einem zugrunde liegenden DAO. Sie möchten aus mehreren Gründen nicht, dass es einen echten zugrunde liegenden Speicher verwendet (Geschwindigkeit, Einrichtung und anschließende Bereinigung, mögliche Kollisionen mit anderen Tests, Möglichkeit der Verunreinigung von Testergebnissen mit Problemen in DAO, unabhängig von App , die gerade getestet wird usw.).

Dazu benötigen Sie ein Testframework wie Mockito , mit dem Sie die Funktionalität eines beliebigen Objekts oder einer beliebigen Methode „mocken“ können, indem Sie sie durch ein „Dummy“-Objekt mit vordefiniertem Verhalten ersetzen (ich werde die Details überspringen, da dies wiederum den Rahmen sprengen würde). Sie können also dieses Dummy-Objekt erstellen, das Ihr DAO ersetzt, und die GreatDAOFactory erstellen Geben Sie Ihren Dummy anstelle des Originals zurück, indem Sie GreatDAOFactory.setDAO(dao) aufrufen vor dem Test (und Wiederherstellung danach). Würden Sie anstelle der Instanzklasse statische Methoden verwenden, wäre dies nicht möglich.

Ein weiterer Vorteil, der dem oben beschriebenen Datenbankwechsel ähnlich ist, ist das "Aufpimpen" Ihres Dao mit zusätzlicher Funktionalität. Angenommen, Ihre Anwendung wird langsamer, wenn die Datenmenge in der Datenbank wächst, und Sie entscheiden, dass Sie eine Cache-Schicht benötigen. Implementieren Sie eine Wrapper-Klasse, die die echte dao-Instanz (die ihr als Konstruktorparameter bereitgestellt wird) verwendet, um auf die Datenbank zuzugreifen, und die Objekte, die sie liest, im Arbeitsspeicher zwischenspeichert, damit sie schneller zurückgegeben werden können. Sie können dann Ihre GreatDAOFactory.getDAO erstellen Instanziieren Sie diesen Wrapper, damit die Anwendung ihn nutzen kann.

(Dies wird "Delegationsmuster" genannt ... und scheint ein Schmerz im Hintern zu sein, besonders wenn Sie viele Methoden in Ihrem DAO definiert haben:Sie müssen alle im Wrapper implementieren, selbst um das Verhalten von nur einer zu ändern .Alternativ könnten Sie Ihr Dao einfach unterteilen und auf diese Weise Caching hinzufügen.Dies wäre im Voraus viel weniger langweiliges Programmieren, kann aber problematisch werden, wenn Sie sich entscheiden, die Datenbank zu ändern oder, schlimmer noch, die Option dazu zu haben Implementierungen hin und her wechseln.)

Eine ebenso weit verbreitete (aber meiner Meinung nach minderwertige) Alternative zur "Fabrik"-Methode ist das Erstellen des dao eine Mitgliedsvariable in allen Klassen, die sie benötigen:

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

Auf diese Weise muss der Code, der diese Klassen instanziiert, das dao-Objekt instanziieren (könnte immer noch die Factory verwenden) und es als Konstruktorparameter bereitstellen. Die oben erwähnten Dependency-Injection-Frameworks machen normalerweise etwas Ähnliches.

Dies bietet alle Vorteile der "Fabrikmethode", die ich zuvor beschrieben habe, aber, wie gesagt, meiner Meinung nach nicht so gut ist. Die Nachteile hier sind, dass Sie für jede Ihrer App-Klassen einen Konstruktor schreiben müssen, immer wieder genau dasselbe tun müssen, und auch nicht in der Lage sind, die Klassen bei Bedarf einfach zu instanziieren, und etwas an Lesbarkeit verloren haben:mit einer ausreichend großen Codebasis , ein Leser Ihres Codes, der damit nicht vertraut ist, wird es schwer haben zu verstehen, welche tatsächliche Implementierung des dao verwendet wird, wie es instanziiert wird, ob es sich um ein Singleton, eine Thread-sichere Implementierung handelt, ob es den Status behält oder zwischenspeichert irgendetwas, wie die Entscheidungen über die Auswahl einer bestimmten Implementierung getroffen werden usw.