Thématiques principales

mercredi 7 mars 2018

Design Pattern : DAO


Aujourd’hui nous revenons à java et plus particulièrement à sa version entreprise JEE. Le but de cet article n’est pas de présenter JEE dans son ensemble ( cela sera le propos d’un article plus générale sur JEE) mais de regarder un pattern important du développement des applications d’entreprises. Le pattern que j'évoque n’est pas le pattern MVC auquel nous allons dédié un article plus tard également mais le pattern DAO (Data Access Object) [1] qui nous permet de gérer la couche d'accès aux bases de données. Bien sur aujourd’hui JPA (TopLink [2], Hibernate [3]) a largement pris sa place mais à l'époque où ce dernier n’avait pas la maturité qu’on lui connaît aujourd’hui, ce pattern a rendu bien des services techniques mais aussi théorique en apportant un concept clef de la programmation, le découplage.

Je vous propose donc de traiter un peu ce sujet afin et de le poursuivre par JPA (ba oui forcément…)

Bien sur j’aurai du commencer par une présentation générale de JEE avant d’aborder des sujet plus précis. Ainsi afin que l’on puisse s’y retrouver, il faut juste imaginer une architecture applicative JEE comme une architecture en 3 couches, une couche présentation, une couche métier et une couche données


Dans cette architecture, on trouvera un certains nombres de patterns permettant de faciliter le développement mais aussi permettant de rendre standard toutes applications JEE. Ainsi un développeur sur un projet JEE trouvera les mêmes types de structures dans un autre projet quelques soit le métier. Ainsi par exemple, on utilise classiquement le modèle MVC pour implémenter la couche présentation et gérer une partie de la couche métier. De la même manière, une application ayant des besoins de stockage de données et utilisant un SGBD devra implémenter une couche d'accès aux données. C’est à ce niveau que le pattern DAO va intervenir.

Le découplage de la couche métier

Dans l’absolu, lorsque l’on conçoit une application logiciel, la première composante à laquelle on pense et la composante métier . Effectivement, celle-ci comporte la plus grande partie de la valeur du logiciel en fournissant les briques fonctionnelles de base qui répondront aux besoins du client.

Cependant si le besoin fonctionnel nécessite la consolidation des données dans l’objectif d'être exploité ultérieurement, une première approche est mapper les données métiers directement à un système de stockage.

Par exemple si nous considérons un logiciel de gestion de tâches, associé à des utilisateurs qui vont les réaliser. nous pourrions avoir une structure métier tel que (je vous passe les accesseurs par soucis de concision mais ils doivent être présent afin de respecter la convention bean [11]):


public class Task {
 private String title;
 private String description;
 public enum State {READY, STARTED, DONE} ;
 public State state=State.READY;
 private List<User> users=new ArrayList<>();
 public Task(String title,String description){
  this.title=title;
  this.description=description;
 }
}
public class User {
 private String name;
 private List<Task> tasks=new ArrayList<>();
 public User(String name){
  this.name=name;
 }
 public void work(Task t){
  t.state=State.STARTED;
 }
 public void done(Task t){
  t.state=State.DONE;
 }
}


Ainsi une première approche peut nous décider que sérialiser nos objets en leur ajoutant une fonction toXml() sera suffisant. On imagine rapidement les limitations de cette approche en terme de maintenance car cela implique de produire du code métier dépendant d’une solution technique de sauvegarde. Alors que se passera t il si l’on souhaite passer à l'échelle et utiliser une techno comme JDBC ? On va encore modifier le code métier afin d’y ajouter les spécificités techniques de cette techno et de la base de données qui sera choisie? Non bien sur et je ne vous parle pas des tests unitaires qu’il faudra réadapter à chaque fois…. et la complexité de les produire….

Il nous faut donc une approche rationnelle et propre permettant de développer séparément le code métier et la ou les solutions de stockage. Alors bien sur il existe des patterns génériques permettant le découplage de composantes logicielles comme les patterns visiteur[4] , proxy[5] , observateur[6] ou médiateur [7]. En fait le pattern DAO est plutôt une association de plusieurs patterns dont le Pont [8] et le stratégie [9].

Ainsi le principe est simple, le pattern DAO implique la définition d’un certain nombre d’interfaces fournissant les services pour la manipulation du modèle de données au travers de quatre principales procédures formant le CRUD [10] : Create, Read, Update, Delete.

Il nous faut donc une interface dédiée par élément métier:


public interface IUserDAO {
 boolean create(User user);
 User read(String name);
 boolean update(User user);
 boolean delete(String name);
}
public interface ITaskDAO {
 boolean create(Task task);
 Task read(int identifiant);
 boolean update(Task task);
 boolean delete(int identifiant);
}


L’utilisation des objets DAO au travers des interfaces va alors nous garantir l'indépendance au niveau implémentation. Il nous reste par contre à fournir un moyen d’obtenir ces objets. Pour cela, on va s’appuyer sur le pattern factory qui va au passage nous servir d’environnement pour toute la configuration des drivers de la base de données cible.

On aura donc une factory par famille d’objet implémentant les interfaces DAO. Par exemple:


public class FactoryDAO {
 private String url;
 private String userDB;
 private String pwdDB;
 private FactoryDAO( String url, String userDB, String pwdDB ) {
  this.url = url;
  this.userDB = userDB;
  this.pwdDB = pwdDB;
 }
 public static FactoryDAO getInstance() throws ClassNotFoundException {
  String url="UrlBD";
  String driver="Driver";
  String userDB="UserBD";
  String pwdDB="mdpBD";
  Class.forName( driver );
  return new FactoryDAO(url, userDB, pwdDB);
 }
 public Connection getConnection() throws SQLException {
  return DriverManager.getConnection(url, userDB, pwdDB);
 }
 public IUserDAO getUserDao() {
  return new UserDAOImpl(this);
 }
 public ITaskDAO getTaskDao() {
  return new TaskDAOImpl(this);
 }
}


Si on voulait faire ça bien, on réalisera également une interface pour la factory de façon à la découpler totalement de son utilisateur final (qui sera probablement une servlet … mais c’est une autre histoire)

Bien notre factory va nous produire les Objets UserDAOImpl, TaskDAOImpl dont l'implémentation est spécifique à la base de données gérée par la factory. On trouvera donc dans ces deux classes les composantes CRUD telles que:


public class UserDAOImpl implements IUserDAO {
 private FactoryDAO factory;
 public UserDAOImpl(FactoryDAO factory ) {
  this.factory=factory;
 }
 @Override
 public boolean create(User user) {return false;}
 @Override
 public User read(String name) {return null;}
 @Override
 public boolean update(User user) { return false;}
 @Override
 public boolean delete(String name) { return false;}
}
public class TaskDAOImpl implements ITaskDAO {
 private FactoryDAO factory;
 public TaskDAOImpl(FactoryDAO factoryDAO) {
  this.factory=factoryDAO;
 }
 @Override
 public boolean create(Task task) { return false; }
 @Override
 public Task read(int identifiant) { return null; }
 @Override
 public boolean update(Task task) { return false;}
 @Override
 public boolean delete(int identifiant) { return false; }
}


Bien sur les composantes create, read, update et delete ne sont pas encore ici implémenté. On va s’en charger en utilisant la factory passé en paramètre de l’objet lors de sa construction. Par contre, et parce que c’est à la factory que nous avons donné le rôle de gérer les accès à la BD, nous allons ajouter des fonctions a cette première pour que les implémentations de DAO n’en soit que plus simple.

Pour implémenter ces méthodes, nous allons utiliser utiliser la méthode getConnection de la factory. Celle-ci va nous fournir un objet Connexion nous permettant de construire un Statement ou un PreparedStatement [12].

La différence entre les deux ces deux types d’objet est simple, les Statements permettant l'exécution d’une requête construite à la main alors que le PrepareStatement fournir une structure a trou a la place.

Dans tous les cas, leur exécution nous retournera un ResultSet contenant l’ensemble des données résultant de la requête.

Ainsi par exemple pour implémenter le read de la classe UserDAOImpl permettant de récupérer un utilisateur, on procédera tout d’abord en définissant la requête Sql a base select qui correspond (très simple dans cet exemple). Ensuite on demandera à la factory un PreparedStatement (obtenu à partir de la Connexion) que l’on exécutera. Son résultat sera alors traité afin d'être mapper avec un vrai objet métier User.


private static String SELECT="select name from User";
@Override
public User read(String name) throws SQLException {
 PreparedStatement statement=this.factory.preparedStatement(SELECT);
 ResultSet result=statement.executeQuery();
 return this.mapResultSetOnUser(result);
}
private User mapResultSetOnUser(ResultSet set) throws SQLException {
 ser user=new User(set.getString("name"));
 return user;
}


Bien sur ce cas présent est très simple et ne présente vraiment aucun difficulté particulière. Par contre, les méthodes traitant de la création, de la mise à jour et de la suppression vont être pour leur part un peu plus complexe à mener à cause de la nécessité de gérer la cohérence de la base de données.

En effet, la modification d’une association entre deux objets métiers va forcément impacter leur relation images dans le monde relationnel de la base de données. Et même avec tous les effort du monde, si l’on souhaite faire les choses de façon cohérente et ne pas risquer de tomber dans des états intermédiaire et passablement dégradé, il sera important d’employer un modèle transactionnel de plus haut niveau tel que JTA [13] mais c’est une autre histoire.

De plus rappelons nous, si a l’origine nous voulions découpler les couche métier et base de données, nous nous retrouvons malgré tout maintenant avec une bonne dose de code à maintenir (c’est toujours ça que de l’avoir dans le code métier). Dans un prochain article nous nous intéresserons a comment les frameworks de mapping objet relationnel ont résolu ce problème

Voilà, sans forcément être entré dans beaucoup de détails, nous avons fait un tour rapide du pattern DAO, de ses avantages et de ses faiblesses. Bien sur aujourd’hui d’autres approches sont à envisager pour concevoir la couche d'accès à la base de données mais il ne faut pas oublier qu’il existe encore des logiciels à maintenir utilisant encore cette technologie et qu’il reste bon de la connaître (du moins savoir de quoi nous sommes parti avant JPA, toplink, hibernate, etc....)

Références:

[1] https://fr.wikipedia.org/wiki/Objet_d%27acc%C3%A8s_aux_donn%C3%A9es
[2] http://www.oracle.com/technetwork/middleware/toplink/overview/index.html
[3] http://hibernate.org/
[4] http://un-est-tout-et-tout-est-un.blogspot.com/2017/12/design-pattern-visiteur.html
[5] http://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-proxy.html
[6] http://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-observateur.html
[7] http://un-est-tout-et-tout-est-un.blogspot.com/2017/11/ready-design-pattern-mediateur.html
[8] http://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-le-pont.html
[9] http://un-est-tout-et-tout-est-un.blogspot.com/2017/11/design-pattern-factory.html
[10] https://www.supinfo.com/articles/single/3637-operations-crud
[11] https://www.jmdoudoux.fr/java/dej/chap-javabean.htm
[12] https://java.developpez.com/faq/jdbc?page=Les-instructions-parametrees-moins-PreparedStatement#Qu-est-ce-qu-un-PreparedStatement
[13] http://blog.xebia.fr/2010/01/07/transactions-les-concepts-et-modeles/

Aucun commentaire:

Enregistrer un commentaire