MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Modèles de conception pour la couche d'accès aux données

Eh bien, l'approche courante du stockage de données en Java n'est, comme vous l'avez noté, pas du tout très orientée objet. Ce n'est en soi ni mauvais ni bon :"l'orientation objet" n'est ni un avantage ni un inconvénient, c'est juste l'un des nombreux paradigmes, qui aide parfois à une bonne conception d'architecture (et parfois non).

La raison pour laquelle les DAO en Java ne sont généralement pas orientés objet est exactement ce que vous voulez réaliser - assouplir votre dépendance vis-à-vis de la base de données spécifique. Dans un langage mieux conçu, qui permettait l'héritage multiple, cela, ou bien sûr, peut être fait très élégamment d'une manière orientée objet, mais avec Java, cela semble juste être plus difficile que cela n'en vaut la peine.

Dans un sens plus large, l'approche non OO aide à découpler vos données au niveau de l'application de la façon dont elles sont stockées. C'est plus qu'une (non) dépendance vis-à-vis des spécificités d'une base de données particulière, mais aussi des schémas de stockage, ce qui est particulièrement important lors de l'utilisation de bases de données relationnelles (ne me lancez pas sur ORM) :vous pouvez avoir un schéma relationnel bien conçu traduit de manière transparente dans le modèle OO de l'application par votre DAO.

Ainsi, ce que la plupart des DAO sont en Java de nos jours est essentiellement ce que vous avez mentionné au début - des classes, pleines de méthodes statiques. Une différence est que, au lieu de rendre toutes les méthodes statiques, il est préférable d'avoir une seule "méthode d'usine" statique (probablement dans une classe différente), qui renvoie une instance (singleton) de votre DAO, qui implémente une interface particulière , utilisé par le code de l'application pour accéder à la base de données :

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

Pourquoi le faire de cette façon par opposition aux méthodes statiques ? Eh bien, que se passe-t-il si vous décidez de passer à une autre base de données ? Naturellement, vous créeriez une nouvelle classe DAO, implémentant la logique de votre nouveau stockage. Si vous utilisiez des méthodes statiques, vous devriez maintenant parcourir tout votre code, accéder au DAO et le modifier pour utiliser votre nouvelle classe, n'est-ce pas ? Cela pourrait être une énorme douleur. Et si vous changiez d'avis et que vous vouliez revenir à l'ancienne base de données ?

Avec cette approche, tout ce que vous avez à faire est de changer le GreatDAOFactory.getDAO() et faites-lui créer une instance d'une classe différente, et tout votre code d'application utilisera la nouvelle base de données sans aucun changement.

Dans la vraie vie, cela se fait souvent sans aucune modification du code :la méthode de fabrique obtient le nom de la classe d'implémentation via un paramètre de propriété et l'instancie à l'aide de la réflexion. Ainsi, tout ce que vous avez à faire pour changer d'implémentation est de modifier une propriété dossier. Il existe en fait des frameworks - comme spring ou guice - qui gèrent pour vous ce mécanisme "d'injection de dépendances", mais je ne rentrerai pas dans les détails, d'abord parce que cela dépasse vraiment le cadre de votre question, et aussi, parce que je ne suis pas forcément convaincu que le bénéfice que vous tirez de l'utilisation ces frameworks valent la peine de s'y intégrer pour la plupart des applications.

Un autre avantage (probablement plus susceptible d'être exploité) de cette "approche d'usine" par opposition à l'approche statique est la testabilité. Imaginez que vous écrivez un test unitaire, qui devrait tester la logique de votre App classe indépendamment de tout DAO sous-jacent. Vous ne voulez pas qu'il utilise un véritable stockage sous-jacent pour plusieurs raisons (vitesse, devoir le configurer et le nettoyer par la suite, collisions possibles avec d'autres tests, possibilité de polluer les résultats des tests avec des problèmes dans DAO, sans rapport avec App , qui est actuellement testé, etc.).

Pour ce faire, vous voulez un framework de test, comme Mockito , qui vous permet de "simuler" la fonctionnalité de n'importe quel objet ou méthode, en le remplaçant par un objet "factice", avec un comportement prédéfini (je vais sauter les détails, car cela dépasse encore le cadre). Ainsi, vous pouvez créer cet objet factice en remplaçant votre DAO et créer la GreatDAOFactory retournez votre mannequin au lieu de la vraie chose en appelant GreatDAOFactory.setDAO(dao) avant le test (et le restaurer après). Si vous utilisiez des méthodes statiques au lieu de la classe d'instance, cela ne serait pas possible.

Un autre avantage, qui est un peu similaire au changement de base de données que j'ai décrit ci-dessus, est de "renforcer" votre dao avec des fonctionnalités supplémentaires. Supposons que votre application ralentisse à mesure que la quantité de données dans la base de données augmente et que vous décidiez d'avoir besoin d'une couche de cache. Implémentez une classe wrapper, qui utilise l'instance réelle de dao (qui lui est fournie en tant que paramètre de constructeur) pour accéder à la base de données, et met en cache les objets qu'elle lit en mémoire, afin qu'ils puissent être renvoyés plus rapidement. Vous pouvez ensuite créer votre GreatDAOFactory.getDAO instancier ce wrapper, pour que l'application en profite.

(C'est ce qu'on appelle le "modèle de délégation" ... et cela semble pénible, surtout lorsque vous avez de nombreuses méthodes définies dans votre DAO :vous devrez toutes les implémenter dans le wrapper, même pour modifier le comportement d'un seul . Alternativement, vous pouvez simplement sous-classer votre dao et y ajouter la mise en cache de cette façon. Ce serait beaucoup moins ennuyeux de coder au départ, mais cela pourrait devenir problématique lorsque vous décidez de changer la base de données ou, pire, d'avoir la possibilité de changer d'implémentation dans les deux sens.)

Une alternative tout aussi largement utilisée (mais, à mon avis, inférieure) à la méthode "d'usine" consiste à faire le dao une variable membre dans toutes les classes qui en ont besoin :

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

De cette façon, le code qui instancie ces classes doit instancier l'objet dao (pourrait toujours utiliser la fabrique) et le fournir en tant que paramètre du constructeur. Les frameworks d'injection de dépendances que j'ai mentionnés ci-dessus font généralement quelque chose de similaire à cela.

Cela offre tous les avantages de l'approche "méthode d'usine", que j'ai décrite plus tôt, mais, comme je l'ai dit, n'est pas aussi bonne à mon avis. Les inconvénients ici sont de devoir écrire un constructeur pour chacune de vos classes d'application, de faire exactement la même chose encore et encore, et de ne pas pouvoir instancier facilement les classes en cas de besoin, et une certaine perte de lisibilité :avec une base de code suffisamment grande , un lecteur de votre code, qui ne le connaît pas, aura du mal à comprendre quelle implémentation réelle du dao est utilisée, comment il est instancié, s'il s'agit d'un singleton, d'une implémentation thread-safe, s'il conserve l'état ou les caches quoi que ce soit, comment les décisions sur le choix d'une implémentation particulière sont prises, etc.