"Data Transfer Object" Pattern, Data Access & Web Service

Introduction

Ce tutoriel expose un cas d'utilisation du design Pattern "Data Transfer Object". Ce dernier s'intègre dans une solution .Net permettant l'accès aux données via les Web Services.

Présentation du besoin

Pour les différents exemples, nous établissons le besoin suivant:

Accès au différentes fonctions d'interaction avec la table « Person » : Ajout, Modification, Suppression et Récupération.

Colonnes Type Description
ID int Primary Key, Not Null
Name varchar(50) Null
Age int Not Null

Tableau : Table "Person"

Analyse du besoin

Bien entendu, un besoin tel que se révèle assez simple : nous pourrions tout à fait coder directement les quatre méthodes.

Néanmoins, comme tout bon développeur que nous sommes, nous souhaitons quand même rendre la chose la plus évolutive et générique possible. C'est dans ce cadre que nous pouvons présenter l'architecture suivante :

Figure : Architecture Web Services basé sur le design pattern DTO

A ce niveau là, vous allez me dire : « il est bien beau le schéma mais ça sert à quoi ». Je vous propose donc de poursuivre la suite du document qui va expliquer module par module le rôle de chacun.

Développement

Conseil : créer une solution contenant un projet distinct pour chaque composant.
(les références projet sont exprimées par les double flèches du schéma ci-dessus)

Utilisation du « Data Transfer Object » Pattern

Une utilisation de ce patron (couplé au design pattern : « Assembler ») décompose ce besoin en terme d'objets métiers sous 3 parties distinctes. Tout ceci dans le but de rendre plus simple l'utilisation des objets métiers correspondant aux besoins fonctionnels.

Les « Domain Objects »

Il s'agit là d'un simple flux contenant des données génériques. Il existe nativement dans le framework .Net sous la forme de classes telles que : DataSet, DataReader...

Pour notre exemple, la classe utilisée sera « DataSet ».

Les « Data Transfer Objects »

Présentée par le "Data Transfer Object" pattern (url: http://msdn.microsoft.com/en-us/library/ms978717.aspx ), une classe DTO est un conteneur métier.

Chaque classe respecte les spécifications fonctionnelles tant au niveau de ses champs que des propriétés, mais n'implémente aucune logique métier au niveau des méthodes. En effet, il ne s'agit là que d'un conteneur permettant le transport des informations métier.

Nous ajouterons à chaque classe DTO la sérialisation XML qui permettra, à terme, la communication avec n'importe quelle plate-forme et n'importe quel langage.

Code :
Fichier de classe « Person.cs » :

[XmlRoot(ElementName="Personne")]
public class Person
{
    private int _id = -1;
    private string _name = null;
    private int _age = -1;

    [XmlAttribute(AttributeName="Identifiant")]
    public int Id { get { return _id; } set { _id=value;} }
    [XmlAttribute(AttributeName="Nom")]
    public string Name { get { return _name; } set { _name=value;} }
    [XmlAttribute(AttributeName="Age")]
    public int Age { get { return _age; } set { _age=value;} }

    public Person()
    {
    }

    public Person(int id, string name, int age)
    {
        _id = id;
        _name = name;
        _age = age;
    }
}

Fichier de classe « PersonList.cs »

public class PersonsList : List<Persons>
{
    public PersonsList()
    : base()
    {
    }
}

La classe Assembler

Cette classe constitue le lien entre les DTO et les DO. Elle permet, via des méthodes statiques, la conversion d'un DTO vers un DO et inversement.

Dans ce cadre là, nous pourrons facilement fournir un flux XML contenant des objets DTO. A l'inverse, nous pourrons interpréter facilement un flux XML en DTO.

Code :

Fichier de classe statique « Assembler » :

public static class Assembler
{
    public static DataSet CreatePersonDataSet(PersonList listPerson)
    {
        DataSet rDataset = new DataSet("Person");
        DataTable table = rDataset.Tables.Add();

        table.Columns.Add("id", typeof(int));
        table.Columns.Add("name", typeof(string));
        table.Columns.Add("age", typeof(int));

        foreach(Person curPerson in listPerson)
        {
            DataRow curRow = table.NewRow();
            curRow[0] = (int) curPerson.Id;
            curRow[1] = (string) curPeron.Name;
            curRow[2] = (int) curPerson.Age;

            table.Rows.Add(curRow);
        }

        return rDataSet;
    }

    public static PersonList CreatePersonDTO(DataSet ds)
    {
        PersonList rList = new PersonList();

        foreach(DataRow curRow in ds.Tables[0].Rows)
        {
            rList.Add(new Person(
            (int) curRow["id"],
            (string) curRow["name"],
            (int) curRow["age"]);
        }

        return rList;
    }
}

« Business Data Access Objects » & « Data Access Objects »

Ces deux composants, normalement distincts, sont exposés ici comme un seul et unique composant. Celui-ci s'occupe de toutes les fonctionnalités concernant l'accès aux données : récupération des personnes, ajout / modification / suppression d'une personne.

Code :

Fichier « DataAccess.cs »

public class DataAccess
{
    private string _connectionString = null;

    public string ConnectionString
    { get { return _connectionString; } }

    public DataAccess(string connectionString)
    {
        _connectionString = connectionString;
    }

    public void AddPerson(int id, string name, int age)
    {
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            //-- INSERT --
            using (SqlCommand command = new SqlCommand("INSERT INTO Person VALUES(@id, @name, @age)", connection))
            {
                command.Parameters.Add(new SqlParameter("@id", id));
                command.Parameters.Add(new SqlParameter("@name", name));
                command.Parameters.Add(new SqlParameter("@age", age));
                command.ExecuteNonQuery();
            }
            connection.Close();
        }
    }

    public void UpdatePerson(int id, string name, int age)
    {
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            //-- UPDATE --
            using (SqlCommand command = new SqlCommand("UPDATE Person SET name=@name, age=@age WHERE id=@id", connection))
            {
                command.Parameters.Add(new SqlParameter("@name", name));
                command.Parameters.Add(new SqlParameter("@age", age));
                command.Parameters.Add(new SqlParameter("@id", id));
                command.ExecuteNonQuery();
            }
            connection.Close();
        }
    }

    public void DeletePerson(int id)
    {
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            //-- DELETE --
            using (SqlCommand command = new SqlCommand("DELETE Person WHERE id=@id", connection))
            {
                command.Parameters.Add(new SqlParameter("@name", name));
                command.Parameters.Add(new SqlParameter("@age", age));
                command.Parameters.Add(new SqlParameter("@id", id));
                command.ExecuteNonQuery();
            }
            connection.Close();
        }
    }

    public DataSet GetPersons()
    {
        DataSet ds = new DataSet("Person");
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
            connection.Open();
            //-- SELECT --
            using(SqlDataAdapter da = new SqlDataAdapter( "SELECT id, name, age FROM Person", connection))
            {
                da.Fill(ds);
            }
            connection.Close();
        }
        return ds;
    }
}

Comme vous l'aurez compris, l'idéal consisterait à séparer la partie métier de l'accès aux données. L'accès aux données ne s'occuperait que la connexion et l'exécution des requêtes sur la base. Ainsi, nous pourrions facilement changer d'un type de base de données à un autre (ex : Sql Server, Oracle, Base Access, MySQL...) sans affecter la logique métier d'accès aux données.

Pour ce faire, je ne pourrais que vous conseiller d'utiliser une librairie très connue des développeurs .Net : « Enterprise Library 3.1 ». Celle-ci intègre un composant nommé « Data Access Application Block » qui correspond tout à fait à ce besoin. Il permet la sélection du type de la base de données via le fichier de configuration.

Url : http://msdn.microsoft.com/en-us/library/aa480453.aspx

Les « Web Services »

Il ne nous reste donc plus qu'à donner l'accès aux données aux utilisateurs. Une couche Web Service se révèle idéale pour la situation. Celle-ci fera interface entre utilisateur et exécution des demandes.

Code :

Fichier « Persons.asmx »

public class WebService : System.Web.Services.WebService
{
    public WebService() { }

    [WebMethod]
    public void AddPerson(int id, string name, int age)
    {
        DataAccess da = new DataAccess("connection");
        da.AddPerson(id, name, age);
    }

    [WebMethod]
    public void UpdatePerson(int id, string name, int age)
    {
        DataAccess da = new DataAccess("connection");
        da.UpdatePerson(id, name, age);
    }

    [WebMethod]
    public void DeletePerson(int id)
    {
        DataAccess da = new DataAccess("connection");
        da.DeletePerson(id, name, age);
    }

    [WebMethod]
    public string GetPersons()
    {
        DataAccess da = new DataAccess("connection");
        DataSet ds = da.GetPersons();

        //Create DTO
        PersonList personList = Assembler.CreatePersonDTO(ds);

        //Serialize to XML
        StringWriter sw = new StringWriter();
        XmlSerializer s = new XmlSerializer(typeof(personList));
        s.Serialize(sw, personList);
        return sw.ToString();
        //--
    }
}

Synthèse

Alors voila, la mise en place est terminée.

Mais, quels sont donc les avantages d'une telle architecture ?

Un tel projet devient alors une façade de services multi plateforme et multi langage. N'importe quel type de projet (application Windows, site web, service Windows...) pourra se greffer, interagir et interpréter très facilement (normalisation XML) les données, et ce, quelque soit le langage utilisé.

L'ajout d'une colonne dans la table consistera simplement à ajouter un membre à la classe DTO et deux lignes dans la classe Assembler.

Le changement de type de base de données n'interfèrera qu'avec la partie accès aux données (et en cas d'utilisation de la librairie « Enterprise Library », elle se révélera dans l'unique modification du fichier de configuration).

Ce document intitulé « "Data Transfer Object" Pattern, Data Access & Web Service » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous