Développez générique avec ADO.NET  par Sami Jaber (jaber@ifrance.com)

Introduction

Nous ne comptons plus le nombre d'articles sur ADO.NET faisant l'éloge de l'accès aux données à travers un mode entièrement spécifique. Plus précisément, toute documentation des API ADO.NET adopte une approche "Provider Centric" avec un code source totalement lié à la base de données sous-jacente. Pour preuve, le dernier ".NET Professionnel" réserve 1 page sur mille à la conception générique dans la section "Coder en ignorant le fournisseur". Quant à l'ouvrage de référence sur ASP.NET, "ASP.NET Professionnel", il n'y fait tout simplement aucune allusion sur un total de 1500 pages. Pourquoi diable Microsoft, et à travers lui, toute la communauté .NET en général, choisissent-ils de promouvoir ADO.NET à travers une seule et même conception faisant la part belle au développement spécifique ?

Si nous sommes convaincu qu'une majorité d'applications se destineront essentiellement à une cible bien précise et que le changement de base de données en cours de projet est une opération exceptionnelle, qu'en est-il des applications destinées à un déploiement générique indépendant d'un SGBD particulier ? Qu'en est-il aussi des applications multi-bases préférant faire appel en phase de développement à des solutions légères de type MySQL tout en privilégiant lors de la phase de production des produits tels que SQL Server, Oracle ou DB2 ?

Bref, il serait dommage de se passer d'une conception générique et multi-bases surtout lorsque le surcoût occasionné est quasiment nul par rapport à une solution analogue mais spécifique.

Ces préoccupations seront au coeur de cet article avec à la clé la présentation du premier Design Pattern créé par DotNetGuru : le GDAO (Generic Data Access Objects). Nous verrons ainsi que la conception d'une application .NET multi-base n'est pas d'une grande complexité car tous les ingrédients sont déjà réunis au sein du Framework pour mettre en place une telle architecture.

Enfin, nous nous intéresserons de près à JDBC (Java DataBase Connectivity), la couche d'accès aux données standard et générique de Java. JDBC propose une API totalement découplée des drivers du marché permettant de disposer d'un code équivalent quelque soit la base utilisée. Le but de cet article est donc de vous montrer les bénéfices de cette approche associée à la puissance du Framework ADO.NET.

La généricité à l'honneur

ADO.NET est sûrement, avec Enterprises Services, l'une des API de .NET prêtant le plus à discussions. Non pas que l'architecture technique ne soit pas à la hauteur des ambitions du projet .NET, mais la généricité est totalement occultée alors qu'elle occupe une place non négligeable dans les classes et interfaces du Framework.

L'approche "Provider centric" 

Aujourd'hui, la gestion des providers avec ADO.NET est un imbroglio total pour le quidam moyen. Entre les providers managés et les providers non managés, les providers managés s'appuyant sur des API non managées et les providers OleDB génériques,  il y en a partout !

Pourtant, à bien y regarder, la compréhension aurait été sûrement meilleure si seulement le Framework avait adopté par défaut une démarche générique consistant à masquer toute forme d'implémentation propriétaire.

Avant d'aller plus loin, faisons un point sur les différents types de providers existants et l'approche généralement mise en avant.

Il existe aujourd'hui deux types de providers :

- Managés : ces providers sont entièrement écrits en code managé, ils s'intègrent naturellement avec la plateforme .NET. Il en existe plusieurs et leur nombre ne cesse de croître en fonction des nouvelles disponibilités. Parmi eux, nous trouvons les providers Oracle, DB2, ODBC, etc...

- Non managés : ce sont les anciennes DLL d'accès aux données (ADO) se basant sur l'interopérabilité COM afin de garder une compatibilité ascendante avec du code existant. Ce cas étant très particulier, il ne sera pas traité dans cet article car il nécessite la prise en compte d'API non conformes à ADO.NET.

La majorité du code présenté dans les ouvrages .NET ou dans les diverses documentations officielles de MSDN et sur les différents sites spécialisés se font l'écho d'une programmation spécifique se basant sur l'implémentation d'un Provider donné telle qu'illustrée dans les programmes sources suivants. L'objectif de ce programme, par ailleurs très simple, consiste à exécuter une requête SQL (select) et à récupérer son résultat. Alors que toutes les bases de données existantes sur le marché gèrent cette fonctionnalité suivant le même principe, voici le type d'implémentation proposée partout :  

 

            // Code utilisant un fournisseur OleDB

            using System.Data.OleDb;

            public void ExecuteSQLWithOleDB(string myConnString)

            {

                  string mySelectQuery = "SELECT * FROM Orders";
                  OleDbConnection myConnection = new OleDbConnection(myConnString);
                  OleDbCommand myCommand = new OleDbCommand(mySelectQuery,myConnection);
                  myConnection.Open();
                  OleDbDataReader myReader = myCommand.ExecuteReader();
                  try
                  {
                        while (myReader.Read())
                        {
                             Console.WriteLine(myReader.GetInt32(0) + ", " + myReader.GetString(1));
                        }
                  }
                  finally
                  {
                        myReader.Close
(); // always call Close when done reading.
                       
myConnection.Close();// always call Close when done reading.

                  }
            }

            // La même méthode utilisant SQL Server

            using System.Data.SqlClient;

            public void ExecuteSQLWithSQLServer(string myConnString)

            {

                  string mySelectQuery = "SELECT * FROM Orders";
                 
SqlConnection myConnection = new SqlConnection(myConnString);
                 
SqlCommand myCommand = new SqlCommand(mySelectQuery,myConnection);
                  myConnection.Open();
                 
SqlDataReader myReader = myCommand.ExecuteReader();
                  try
                  {
                        while (myReader.Read())
                        {
                             Console.WriteLine(myReader.GetInt32(0) + ", " + myReader.GetString(1));
                        }
                  }
                  finally
                  {
                        myReader.Close
(); // always call Close when done reading.
                       
myConnection.Close(); // always call Close when done reading.
                  }
            }

            // La même méthode utilisant Oracle            

           
using System.Data.OracleClient ;

            public void ExecuteSQLWithOracle(string myConnString)

            {

                  string mySelectQuery = "SELECT * FROM Orders";
                 
OracleConnection myConnection = new OracleConnection(myConnString);
                 
OracleCommand myCommand = new OracleCommand(mySelectQuery,myConnection);
                  myConnection.Open();
                 
OracleDataReader myReader = myCommand.ExecuteReader();
                  try
                  {
                        while (myReader.Read())
                        {
                             Console.WriteLine(myReader.GetInt32(0) + ", " + myReader.GetString(1));
                        }
                  }
                  finally
                  {
                        myReader.Close
(); // always call Close when done reading.
                       
myConnection.Close(); // always call Close when done reading.
                  }
            }

 

Cette approche ne vous étonne-t-elle pas ? Nous aurions pu aller très loin avec ce type de raisonnement mais nous avons préféré nous arrêter là. Le provider Oracle est situé dans le namespace System.Data.OracleClient et il faut utiliser OracleConnection() en lieu et place de SqlConnection(), même principe pour Odbc avec OdbcConnection(), qui lui, est fournit dans un Namespace Microsoft.odbc. En passant, il est légitime de s'interroger sur la cohérence de ces Namespaces. Pourquoi avoir mis Oracle et SQL Server directement dans la hiérarchie du Framework System alors qu'Odbc, lui, est externalisé dans un autre Namespace ? Il règne un certain désordre dans la gestion des différents providers et de leurs namespaces respectifs. Nous y reviendrons plus tard.

Le source précédent vous montre donc une des aberrations induites par l'utilisation de code orienté provider. Excepté le fait que la pérennité des API n'est pas assurée, l'utilisateur souhaitant passer d'une base à l'autre devra entièrement re-écrire son code.

L'approche Générique

Voici la méthode de programmation que nous vous proposons. Il existe dans les API ADO.NET un ensemble d'interfaces méconnues et très peu mises en avant dont le rôle est de fournir cette abstraction tant attendue. Vous trouverez les classes IDbConnection IDbCommand, IDbDataAdapter, IDataReader à partir desquelles dérivent SqlConnection, OdbcCommand ou encore OracleDataReader. Il suffit donc d'écrire un code s'appuyant sur ces interfaces suivant le principe ci-dessous.

            public void ExecuteGenericSQL(string myConnString)
            {
                  string mySelectQuery = "SELECT * FROM Orders";
                  IDataReader myGenericDataReader ;
                  IDbConnection myGenericConnection ;
                 
                  // La classe MyProviderConnectionClass n'existe pas, c'est un exemple

                  IDbConnection myGenericConnection = new MyProviderConnectionClass(...);
                  IdbCommand myGenericCommand = new myGenericConnection.CreateCommand();
                  myGenericCommand.Open();
                  myGenericCommand.CommandText = mySelectQuery ;
                  myGenericDataReader myReader = myGenericCommand.ExecuteReader();
                  try
                  {
                        while (myReader.Read())
                        {
                             Console.WriteLine(myReader.GetInt32(0) + ", " + myReader.GetString(1));
                       
}
                  }

                  finally
                 
{
                        // always call Close when done reading.
                       
myReader.Close();
                       
// always call Close when done reading.
                       
myConnection.Close();
                  }


Ce programme est totalement indépendant d'un provider donné et fait appel au polymorphisme de l'API ADO.NET. En y regardant de près, il se trouve que Microsoft utilise le Design Pattern Factory ou Usine de Classe (Voir article sur LDAP) à travers l'interface IDbConnection. Son rôle consiste à masquer les opérations de création d'objets spécifiques liés à un provider donné. Le client manipule donc les types ADO.NET à travers des API "standards" et génériques.

Bien entendu, ces interfaces ne pourront en aucun cas faire appel aux spécificités propres de telle ou telle base de données car elles représentent le plus petit dénominateur commun existant entre toutes les bases. Ce point, s'il s'avère délicat, n'exclut pas l'utilisation d'extensions propriétaires comme nous le verrons plus loin. Il suffit simplement d'adapter la démarche de façon à prendre en compte ce cas d'utilisation.

Malgré les améliorations apportées précédemment, il subsiste un inconvénient majeur. Le premier objet de type IDbConnection devant être instancié avec son type réel, il nous faut absolument paramétrer cette opération de manière à éviter d'écrire le code suivant qui serait préjudiciable à notre approche:

            if (sgbd.equals("SqlServer")) new SqlConnection("...");
            if (
sgbd.equals("Oracle")) new OracleConnection("...");
            if (
sgbd.equals("Odbc")) new OdbcConnection("...");
           
(...)


Sans compter que l'utilisation de l'ordre new Class() nécessite d'introduire l'ensemble des types dans les programmes sources à l'aide du mot-clé using System.Data.SqlClient, System.Data.OracleClient, Microsoft.Data.Odbc, .... Il va de soi qu'une telle approche serait dramatique pour notre Design Pattern car cela nécessiterait d'adapter notre code source en fonction de la base cible. C'est exactement ce que nous cherchons à éviter.

La solution "magique" passe donc par le chargement dynamique de classe. L'instanciation de la classe de Connexion (IDbConnection) sera réalisée de manière paramétrée en passant simplement le nom de la classe en question. Voici la démarche à suivre afin de récupérer ce fameux objet Connection:

// Remarquez l'absence total d'ordre using lié à un Provider particulier
using System;
using
System.Data ;
using
System.Reflection ;
using
System.Configuration ;

static IDbConnection GetConnection()
            {
                  // Exemples d'Assembly pouvant être chargée:
                  // "System.Data"
                  // "Microsoft.Data.Odbc"                 

                  Assembly objAssembly=Assembly.LoadWithPartialName(ConfigurationSettings.AppSettings["DBAssembly"]);

                  // Exemples de Types :
                  // "Microsoft.Data.Odbc.OdbcConnection"
                 
// "System.Data.OleDb.OleDbConnection"
                  // "System.Data.SqlClient.SqlConnection"
 

                  Type t = objAssembly.GetType(ConfigurationSettings.AppSettings["DBConnectionClass"]);

                  // On instancie ici le premier objet typé de notre hierarchie
                  IDbConnection ConnectionObj  = (IDbConnection) Activator.CreateInstance(t);

                  // Ne pas oublier d'initialiser la chaine de connexion
                 
ConnectionObj.ConnectionString = ConfigurationSettings.AppSettings["DBConnectionString"] ;

                  return ConnectionObj ;

            }

      public static void ExecuteSQL(IDbConnection myConnection)
            {
                  string mySelectQuery = "SELECT * FROM Orders";
                  IDbCommand myCommand = myConnection.CreateCommand() ;
                  myCommand.CommandText = mySelectQuery ;
                  myConnection.Open();
                  myCommand.ExecuteNonQuery();
                  IDataReader myReader = myCommand.ExecuteReader();
                  try
                  {
                        while (myReader.Read())
                        {
                             Console.WriteLine(myReader.GetInt32(0) + ", " + myReader.GetString(1));
                        }
                  }
                  finally
                  {
                        // always call Close when done reading.
                        myReader.Close();
                        // always call Close when done reading.
                       
myConnection.Close();
                  }
            }

            [STAThread]
            static void Main(string[] args)
           
{
                  // Cette opération doit être réalisée une seule fois
                  IDbConnection conn = GetConnection();
                  // Cette méthode est totalement générique
                  ExecuteSQL(conn) ;
            }

Non seulement ce code est générique et marchera avec n'importe quelle base, mais si d'aventure vous souhaitez introduire un nouveau type de Provider, il vous suffit simplement de créer les entrées adéquates dans le fichier de configuration GenericDAO.exe.config.

Nous avons utilisé l'ordre Assembly.LoadWithPartialName("AssemblyName") afin de charger la DLL requise par le Driver. A noter qu'il n'est nullement nécessaire de suffixer ce nom avec l'extension .dll. Ce chargement dynamique étant faiblement nommé (Partial Name), le Runtime se chargera de compléter la chaîne et de retrouver le chemin physique en fonction des références existantes lors de la compilation. Par ailleurs, l'ensemble des paramètres étant placées dans le fichier de configuration d'application, l'utilisateur, s'il le souhaite, pourra changer à tout moment la base de données utilisée sans avoir à recompiler ses sources. C'est une approche souvent utilisée par les outils de mapping Objet/Relationnel.


<?
xml version="1.0" encoding="utf-8" ?>
<
configuration>
      <appSettings>
            <add key="DBAssembly" value="System.Data" />
            <add key="DBConnectionClass" value="System.Data.OleDb.OleDbConnection" />
            <add key="DBConnectionString" value="Provider=SQLOLEDB;Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind" />

            <!--
            <add key="DBAssembly" value="System.Data" />
            <add key="DBConnectionClass" value="System.Data.SqlClient.SqlConnection" />
            <add key="DBConnectionString" value="Data Source=localhost;Initial Catalog=Northwind;uid=sa;Password=;" />
            --
>
            <!--

            <add key="DBAssembly" value="Microsoft.Data.Odbc" />
            <add key="DBConnectionClass" value="Microsoft.Data.Odbc.OdbcConnection" />
            <add key="DBConnectionString" value="DSN=LocalServer;uid=sa;Pwd=;" />
            --
>
      </appSettings>

</
configuration>

 

Les chaînes de connexion : un vrai méli-mélo

Qui n'a jamais eu à chercher pendant des heures la chaîne de connexion adéquate nécessaire pour la connexion à une base de données un tantinet "exotique". Tout d'abord, intéressons nous aux noms de variables. Alors que certains providers imposent un nommage très restrictif (AS400, DB2, ...), d'autres supportent des raccourcis du type "Uid=sa;Pwd=toto". Sans compter qu'un utilisateur souhaitant rechercher le catalogue des variables utilisées dans la chaîne de connexion pour une base spécifique sera dans l'impossibilité de le faire. C'est pour cette raison qu'ont été créés certains sites tels que www.connectionstrings.com.

Par ailleurs, certaines propriétés ne sont valables que dans un contexte bien déterminé alors que d'autres sont employées de manière spécifiques. Bref, le classique dictionnaire nom=valeur s'avère très vite limité et contraignant lorsqu'il s'agit de cibler une palette de bases aussi différentes l'une de l'autre sans aucune caractéristiques communes (shémas, fichiers AS/400, tables, ...). L'exemple suivant illustre ce phénomène avec deux chaînes de connexion, l'une destinée à une base AS400 utilisant un driver natif,  l'autre utilisant un DSN ODBC.

1) ConnectionString="PROVIDER=Client Access ODBC Driver (32-bit);BLOCKSIZE=64;RECBLOCK=2;XDYNAMIC=0;LAZYCLOSE=1;
COMPRESSION=1;SYSTEM=DWHXXXXX;DBQ=NXXXSYFP;UID=XXremovedXX;PWD=XXXX;"

2) ConnectionString="DSN=MaBaseAccess"

Pour résumer, le reproche pouvant être fait à l'encontre des chaînes de connexion est leur incapacité notoire à décrire le type de provider utilisé : Managé ou non managé.  

Utiliser un nommage sous la forme d'URI !

L'avantage qui consiste à faire appel au URI pour référencer un provider donné est de pouvoir typer la connexion utilisée. En Java, JDBC utilise cette technique avec la notion de Types de Drivers. Les types permettent de déclarer un accès faisant appel à un driver natif, semi-natif ou un pont odbc-jdbc. Il existe 4 types de Drivers :

Le nommage respecte les règles suivantes : jdbc:<sousprotocole>:<partiespécifique>. Par exemple, pour utiliser odbc :

jdbc:odbc:MyDSN (type 1)

ADO.NET pourrait suivre cet exemple avec un nommage adapté à la situation :

adonet:managed:odbc:MyDSN ou adonet:unmanaged:oledb://localhost:1433/base=Nothwind;uid=sa;pass=;

Il existe un autre avantage à représenter une connexion via une URI. Imaginez que l'ensemble des paramètres de configuration d'une application soient persistants dans un annuaire du type Active Directory. Il serait très simple de mettre en place une hiérarchie bien précise des objets en fonction de leur type respectif. Par exemple, les objets distants .NET Remoting seraient préfixés :

dotnetremoting:sao:tcp://localhost:1234/myObject .  // sao pour Server Activated Object

L'environnement .NET en général souffre d'un manque de cohérence dans le nommage des objets de types différents même s'ils sont d'ores et déjà utilisées à travers les monikers, hérités de l'API COM.

Ex : GetObject("WinNT://MyDomain/MyAccount,User")
Set MyObject2 = GetObject("LDAP://MyLdapSvr/CN=JeffSmith,DC=Fabrikam")
GetObject("queue:/new:VBQueuedComp.QCObject")

Utiliser les extensions spécifiques d'une base de données

Le Design Pattern que nous venons de vous présenter est très utile lorsque vous accéder de manière standard à des bases de données sans utiliser leurs fonctionnalités propriétaires. Malheureusement, cette technique vous prive d'un certain nombre d'extensions très utiles dans certaines situations et représente souvent la valeur ajoutée d'une base par rapport à une autre.

Prenons un exemple concret, SQL Server propose une fonctionnalité intéressante permettant de récupérer via une simple méthode SQLConnection.ExecuteXMLReader() un flux XML hiérarchique contenant la résolution de tous les enregistrements situés dans des tables liées. Cette méthode située dans la classe SQLConnection n'est pas présente dans l'interface IDbConnection, c'est pourquoi nous proposons de déporter cet appel dans une classe prévue à cette effet. Ainsi, tous les appels vers l'implémentation d'un provider donné, en l'occurrence, vers le provider SQL Server seront regroupés au sein de cette classe. L'intérêt d'une telle démarche consiste à concentrer dans des zones (classes) bien maîtrisées les parties propriétaires d'une application afin de réduire au maximum les risques en cas de migration. Moins de code vous aurez à re-écrire, mieux votre application se portera.

Voici le code source du même exemple précédent utilisant la fonction ExecuteXMLReader() :

using System;
using
System.Data ;
/// L'objet du délit
using System.Data.SqlClient ;
using
System.Xml ;

      /// <summary>
      /// SpecificDAO : code susceptible d'être adapté ou migré
      /// </summary>

      public class SpecificDAO
      {
            public DataSet ExecuteSpecificSQL()
            {
                  String sConnection = "server=localhost;Trusted_Connection=yes;database=northwind";
                  SqlConnection mySqlConnection = new SqlConnection(sConnection);
                 
SqlCommand mySqlCommand = new SqlCommand("select * from customers FOR XML AUTO, XMLDATA",
                    mySqlConnection);
                  mySqlCommand.CommandTimeout = 15;
                  mySqlConnection.Open(); 

                  // Now create the DataSet and fill it with xml data.
                  DataSet myDataSet1 = new DataSet();
                  myDataSet1.ReadXml((XmlTextReader)mySqlCommand.ExecuteXmlReader(), XmlReadMode.Fragment);
                  return myDataSet1 ;
            }
      }

Cette classe contient une référence vers le provider SQL Server ainsi que l'utilisation des classes d'implémentation du même provider. C'est donc un exemple type d'entrave aux règles d'encapsulation. C'est pourquoi il convient de découpler le client qui utilisera cette classe de l'API System.Data.SqlClient. Pour ce faire, il suffit simplement de toujours renvoyer au client des objets pérennes tels que des DataSet, Hashtable ou ArrayList. Plus concrètement, des collections d'objets n'ayant aucun lien direct avec les classes de l'API du Provider que vous êtes en train de manipuler.

Lorsqu'il sera nécessaire de modifier votre application ou de migrer vers une autre base, l'opération consistera simplement à ré-écrire l'implémentation de vos classes spécifiques. De manière similaire, nous aurions pu mettre en oeuvre une Factory chargée de renvoyer des objets par valeur et dont l'implémentation sera Provider-Centric. Cela vous montre bien les multiples choix s'offrant à vous en terme de conception afin de découpler les différentes couches d'architecture d'une application donnée.

Il demeure malgré tout un inconvénient à ce type d'approche. Le client ne pouvant manipuler directement d'objets spécifiques, toute utilisation externe d'un DataReader en mode connecté par exemple sera interdit. Dans l'exemple précédent, nous n'avions pas ce type de problème car les objets spécifiques étaient confinés à l'intérieur de la procédure ExecuteSpecificSQL.

Réserves à l'encontre de la hiérarchie du Namespace ADO.NET

Nous l'avions précédemment évoqué, certains choix concernant la hiérarchie des Namespaces d'ADO.NET sont quelque peu douteux. Premièrement, la forte présence du Provider SQL Server dans .NET ne justifie en aucun cas son intégration directement au sein du Framework, même si l'implication de Microsoft envers sa base de données maison est considérable. L'API System.Data.SqlClient, en tant qu'implémentation des interfaces présentes dans System.Data doit être située dans un Namespace externe. La raison en est simple, lorsque le Framework .NET est installé, il contient un ensemble de  classes et d'interfaces auxquelles doivent s'accommoder les utilisateurs ou clients. Ce Framework est vu comme une entité complète et indivisible. Cela signifie que toute modification d'une classe du Framework implique obligatoirement une nouvelle version. Or, en couplant l'implémentation du provider SQL Server avec le Framework, Microsoft associe les évolutions du Provider SQL Server avec celles du Framework et par la même occasion couple le Roadmap SQLServer à celui du Framework .NET. Sans commentaire.

Toujours dans le même registre, prenons l'exemple du Provider Oracle livré sous la forme de DLL externes. Une fois le produit installé, il s'insère dans la hiérarchie System.Data.OracleClient élargissant ainsi la structure et le scope même des classes de la BCL. Cela veut donc dire qu'à version équivalente du Framework, plusieurs personnes disposeront de classes et DLL différentes possédant chacune une hiérarchie de classes différentes. Il va de soi que cela peut-être potentiellement dangereux même si à priori le "patch" ou "add-In" en question est bien maîtrisé et n'influe aucunement sur le comportement des autres classes system du Framework. En d'autres termes, l'implémentation d'un produit donné n'a pas à se situer dans la hiérarchie originale de la BCL qui doit être réservée aux classes essentielles et indivisibles de .NET.

Malgré tout, certains signes montrent que Microsoft semble avoir identifié ce problème et s'oriente vers une voie plus saine. Ainsi, dans l'actuel Provider ODBC managé, vous aurez remarqué que le Namespace utilisé est Microsoft.Data.Odbc, brisant ainsi les règles fixées au départ par Microsoft sur ce type de produit.

Une autre illustration de ce principe est donnée par le nommage des contrôles serveurs ASP.NET personnalisés. Si vous faites appel à ces contrôles pourtant fournis par Microsoft, ils s'insèrent logiquement dans la hiérarchie Microsoft.Web.UI.WebControls et non dans System.Web.UI.WebControls

La démarche aurait donc dû être strictement la même pour System.Data.SqlClient, System.Data.OracleClient et System.Data.OleDb. D'ailleurs, ce cas démontre bien l'utilité d'une conception générique pour l'accès aux données car, si à l'avenir, Microsoft décide par une quelconque mésaventure de modifier en cours de route sa hiérarchie de Namespaces, certains développeurs risquent de s'arracher les cheveux. Mais n'extrapolons pas, nous en sommes loin et l'éditeur de Redmond gardera assurément une compatibilité ascendante.

Enfin, tout ce que nous venons d'énoncer n'a pas été inventé. Java, l'homologue d'ADO.NET avec JDBC adopte strictement la même démarche. JDBC est une API standard servant de référence à l'implémentation de Providers ou "Drivers" tiers. A ce titre, toute son architecture est bâtie autour du concept d'interfaces restreignant la visibilité d'un utilisateur sur l'implémentation de ces drivers. La seule API d'accès est JDBC et quelque soit la base sous-jacente utilisée, le code source du client reste inchangé. Quant au nommage utilisé pour les packages (équivalent des NameSpaces .NET), l'implémentation du Provider Oracle en Java est tout simplement situé dans Oracle.jdbc.XX et non dans un quelconque package java.sql.OracleClient.

Nous sommes conscient que l'approche politique et technique des deux communautés diffèrent pour diverses raisons et qu'il est difficile dans pareil situation d'émettre des jugements tranchés en faveur de l'une d'entre elle. Mais si chacune s'inspirait des succès et échecs de l'autre, cela ne pourrait que servir le dessein des développeurs que nous sommes et tout le monde y trouverait son compte.

 

Conclusion

Dans cet article, nous avons mis l'accent sur la programmation générique tout en nous interrogeant sur les préconisations générales en terme d'accès aux données. Il est vrai qu'en lisant les exemples fournis par les ouvrages, sites ou documentations officielles, nous ne pouvons qu'être plongé dans cet univers nous enfermant dans des développements spécifiques. Lorsque nous prenons du recul pour nous interroger sur ces choix de conception et d'architecture, il s'avère qu'ils nuisent aux règles élémentaires d'encapsulation et d'attributions des responsabilités. Il n'appartient qu'à vous de les changer, surtout que le Framework met à votre disposition tous les outils et interfaces nécessaires.

D'autre part, les conditions actuelles de votre projet vous font sûrement penser qu'aucune migration de base ne sera jamais effectuée et que pareil Design Pattern ne vous apporte aucune réelle valeur ajoutée. Malgré tout, réfléchissez-y, pourquoi se priver d'une conception générique et peu coûteuse qui vous épargnera une charge de travail considérable le jour J ? Et puis, il faut se garder de dire "Fontaine, jamais je ne boirai de ton eau" ;-) ...

Par ailleurs, cet article fait état de certaines incohérences existantes au sein de la hiérarchie des classes ADO.NET. Dysfonctionnements, en partie liés à la jeunesse du Framework et qui semblent avoir été pris en compte par Microsoft dans les versions futures. Espérons que Microsoft continuera à oeuvrer dans cette voie.

Pour finir, gardez à l'esprit que nous devons sans cesse nous interroger sur le bien fondé de nos développements. Il ne suffit pas d'appliquer aveuglément telle ou telle documentation, si officielle soit-elle. Pour y arriver, le respect strict des règles de conception objet et d'attribution des responsabilités sera la clé du succès.


Auteur : Sami Jaber

Copyright : DotNetGuru  © - Septembre 2002

 

Ressources

"Database independant Data Access" http://www.codeguru.com/columns/DotNet/DotNet200208.html

Note : Cet article est ce qu'il est commun d'appeler un Anti-pattern. L'auteur, bardé d'innombrables certifications en tout genre (MCSD, MCSE, MCDBA, ...) propose de gérer la généricité à l'aide d'un ordre switch case suivant chaque types différents de bases de données susceptibles d'être utilisées. Cette approche, cela va de soi, va à l'encontre du concept de "généricité" ou plutôt  "d'indépendance" que nous venons de voir dans cet article.

Téléchargez les fichiers sources

Projet Visual Studio (RTM + SP2) : GenericADONet.zip