Un exemple d'architecture .Net complète

icon Tags de l'article :

Décembre 13, 2011

 

Ces derniers temps, j'ai pas mal réfléchi à la meilleure façon de séparer un projet .Net en couches. Classes métier, accès simplifié à la BDD, tests unitaires avec bouchons, injection de dépendances, mapping, ... Tellement de problématiques à gérer lorsqu'on crée une architecture .Net.

Du coup je me suis fixé un objectif simple : créer une architecture de solution .Net "propre, avec

  • des couches bien séparées (classes métier, logique métier, accès aux données, services, tests unitaires, interfaces utilisateur),
  • aucun Framework pour les tests unitaires (histoire que le code reste simple à appréhender et à comprendre pour un débutant),
  • de l'injection de dépendances pour pouvoir brancher facilement les tests unitaires sur des "bouchons",
  • enfin, un accès aux données simplifié, sans SQL ni procédures stockées (donc avec Linq et Entity Framework).

Au final, j'ai créé un simpliste "moteur de blog", capable d'afficher des articles puisés dans une BDD SQL Server 2008 Express. Pour éviter que ce beau projet .Net ne se perde dans les méandres de mon disque dur avant de sombrer dans l'oubli, je me suis dit que le partager avec vous serait le meilleur moyen de lui offrir une seconde vie. Qui sait, peut être sera-t-il utile à de nombreuses personnes ! Smile

Le voici donc : projet HTA.DemoBlog.zip (1,54 mb). Le code source est sous licence LGPL, donc vous pouvez l'utiliser comme bon vous semble ! Wink

Voici, déjà, ce qu'il y a à noter sur cette architecture (pour chaque couche, j'ai créé un solution folder) :

1) Couche Business

  • BusinessEntities : les objets métier, qu'on manipulera dans l'application. L'intérêt est d'avoir des objets complètement détachés de la BDD et qu'on pourra manipuler dans une autre application, dans une bibliothèque de classes, dans un module à part, ... Les objets ne sont vraiment que des conteneurs indépendants de tout le reste.
  • BusinessLogic : Tous les traitements métier. Ici on retrouvera la logique métier, les Try Catch, les calculs compliqués, les logs, ... Ce code sera celui utilisé par les applications (qu'il s'agisse de Winforms, d'ASP.Net, de WPF, ...). Cette couche interagit avec la couche DataAccess et utilise les objets BusinessEntities.

2) Couche Resources

  • DataAccessInterfaces : les interfaces qui définiront les méthodes de la couche DataAccess.
    Intérêt double : pouvoir utiliser des bouchons (mock) qui implémentent cette interface dans les tests unitaires, afin d'avoir des tests unitaires indépendants de la BDD et donc qui peuvent être lancés à chaque build / release.
    L'autre intérêt est de pouvoir changer de source de données ou les méthodes qui interagissent avec la BDD sans avoir à recoder des choses dans d'autres couches (vu qu'elles implémentent ces interfaces et non pas les objets DataAccess directement).
  • DataAccess : la couche qui interroge la BDD. Elle renvoie des objets BusinessEntities et possède des méthodes définies dans DataAccessInterfaces. Vu qu'elle est détachée, on peux utiliser ce qu'on veut pour stocker nos données : Entity Framework, Linq 2 Sql, des procédures stockées, des fichiers XML, ... Si on doit changer de source de données ou de façon d'exploiter notre source de données, on n'aura qu'à recréer une couche qui implémentera les interfaces DataAccessInterfaces, et le tour sera joué.

3) Couche Tests

  • Tests.Mock : des classes bouchon (donc avec des données en dur) qui implémentent les interfaces DataAccessInterfaces.
  • Tests : les tests unitaires. Ces tests testent la couche BusinessLogic via les mocks plutôt que via la couche DataAccess (afin de ne tester que la logique métier, sans risquer de modifier les données en base).

4) Couche Interfaces (UI)

  • La couche qui contiendra les interfaces utilisateur (WebApp, Winforms, WPF, ASP.Net MVC, ...) qui utiliseront uniquement le code métier dans la couche BusinessLogic et qui manipulera des objets BusinessEntities.

5) Couche Core

  • La couche Core contient des méthodes génériques utiles au projet mais qui pourront être réutilisées / utiles pour d'autres projets. (Cryptographie, Logs, ...)

6) Couche Database

  • Le projet Database permet de gérer le contenu de la BDD SQL Server, de mettre à jour les procédures stockées et de modifier les scripts de déploiement, sans avoir à le faire à la main.

7) Couche Service

  • Cette couche permet de créer des services qui utilisent la couche métier et donc de rendre le code accessibles de l'extérieur. (Pour l'utiliser, par exemple, via une application web distante ou via une application Windows Phone 7).

 L'architecture ressemble donc à ceci :

 

Voyons maintenant le code un peu plus en détail.

Dans un premier temps, intéressons-nous à la classe ArticleBE, de la couche BusinessEntities, qui définit la structure d'un article :

    public class ArticleBE : ICloneable
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public string Author { get; set; }
        public string Tags { get; set; }
        public DateTime Date { get; set; }

        public object Clone()
        {
            ArticleBE result = new ArticleBE()
            {
                Id = Id,
                Date = Date,
                Author = Author.Clone() as string,
                Content = Content.Clone() as string,
                Tags = Tags.Clone() as string,
                Title = Title.Clone() as string
            };
            return result;
        }
    }

Comme vous pouvez le voir, le code est vraiment le plus simple possible. Nous avons juste nos propriétés et la méthode Clone (j'expliquerais l'intérêt de la méthode Clone plus tard). Pas de dépendances, pas de contraintes, pas de spécificités, ... ce code peut véritablement être réutilisé n'importe où. Il est possible, dans le cas d'une application demandant une exposition de services, de rendre notre classe Serializable.

Maintenant, nous allons travailler sur la couche DataAccessInterfaces. Dans cette couche, nous allons définir les interfaces et donc les méthodes que notre couche d'accès aux données devra mettre à disposition. Ici, étant donné que nous ne travaillons que sur une table, nous n'allons créer qu'une interface, à savoir IArticleDA :

public interface IArticleDA : IDisposable
    {
        List GetArticles();
        ArticleBE GetArticleById(int idArticle);
        void AddArticle(ArticleBE article);
        void EditArticle(ArticleBE article);
        void DeleteArticle(ArticleBE article);
        void Save();
    }

Le code, encore une fois, est assez simple. Nos classes qui implémenteront IArticleDA devront nous fournir toutes ces méthodes Dispose, puisque nous implémentons IDisposable.

Maintenant que cette interface est définie, nous allons voir la partie accès aux données. Comme vous pouvez le voir, j'ai ajouté au projet DataAccess un ADO.Net Entity Data Model pour pouvoir interroger la base de données très facilement via Linq. Cependant, pour pouvoir transformer nos objets Entity en objets BusinessEntities, j'ai créé une class ArticleMap dans le dossier Mapping :

public static class ArticleMap
    {
        internal static List Map(List
articles) { if (articles == null || articles.Count == 0) { return new List(); } List result = new List(); foreach (Article article in articles) { result.Add(Map(article)); } return result; } internal static ArticleBE Map(Article article) { if (article == null) { return null; } ArticleBE result = new ArticleBE(); result.Id = article.IdArticle; result.Author = article.Author; result.Content = article.FullContent; result.Date = article.Date; result.Tags = article.Tags; result.Title = article.Title; return result; } internal static Article Map(ArticleBE article, Article result = null) { if (result == null) { result = new Article(); } result.Author = article.Author; result.FullContent = article.Content; result.Date = article.Date; result.Tags = article.Tags; result.Title = article.Title; return result; } }

Le code est, encore une fois, très simple. Déjà, toutes les méthodes sont en internal car elles ne doivent être accessibles qu'aux classes de notre projet DataAccess. Le mapping Article => ArticleBe est simple, mais le mapping ArticleBE => Article a une petite spécificité. En effet, dans le constructeur, j'ai passé un paramètre facultatif : un objet Article.

Pourquoi ? Lorsqu'on veut mettre à jour un objet Article en base depuis un objet ArticleBE, plutôt que d'avoir à modifier chaque propriété dans notre code d'accès aux données, il suffit d'appeler la méthode Map avec comme second paramètre notre objet Article à mettre à jour. (Il s'agit de la méthode la plus simple que j'ai pu trouver pour mettre à jour un objet Entity à partir d'une classe BusinessEntities.)

Intéressons-nous ensuite au code de notre classe ArticleDA, qui implémente IArticleDA :

public class ArticleDA : IArticleDA
    {
        private HTADemoBlogEntities _entities = null;

        public ArticleDA()
        {
            _entities = new HTADemoBlogEntities();
        }

        public List GetArticles()
        {
            return ArticleMap.Map(_entities.Article.OrderByDescending(e => e.IdArticle).ToList());
        }

        public ArticleBE GetArticleById(int idArticle)
        {
            return ArticleMap.Map(_entities.Article.Where(e => e.IdArticle == idArticle).FirstOrDefault());
        }

        public void AddArticle(ArticleBE article)
        {
            _entities.Article.AddObject(ArticleMap.Map(article));
        }

        public void EditArticle(ArticleBE article)
        {
            Article existingArticle = _entities.Article.Where(e => e.IdArticle == article.Id).FirstOrDefault();

            if (existingArticle != null)
            {
                ArticleMap.Map(article, existingArticle);
            }
        }

        public void DeleteArticle(ArticleBE article)
        {
            Article existingArticle = _entities.Article.Where(e => e.IdArticle == article.Id).FirstOrDefault();

            if (existingArticle != null)
            {
                _entities.Article.DeleteObject(existingArticle);
            }
        }

        public void Save()
        {
            _entities.SaveChanges();
        }

        public void Dispose()
        {
            if (_entities != null)
            {
                _entities.Dispose();
            }
        }
    }

Rien de très compliqué. J'ai juste une méthode Save (déclarée dans IArticleDA) qui permet de sauver en base les modifications faites à nos objets Article, à l'aide de SaveChanges. Ca nous permet, dans le cas où nous avons de nombreux articles à créer ou à modifier, de ne pas faire de SaveChanges à chaque fois. Nous n'appellerons la méthode qu'à la fin de notre traitement métier.

Voyons maintenant la couche BusinessLogic, avec la classe ArticleBL :

public class ArticleBL : IDisposable
    {
        private IArticleDA _dataFactory = null;

        public ArticleBL()
            : this(new ArticleDA())
        {

        }

        public ArticleBL(IArticleDA dataFactory)
        {
            _dataFactory = dataFactory;
        }

        public List GetArticles()
        {
            try
            {
                return _dataFactory.GetArticles();
            }
            catch (Exception ex)
            {
                LogError.Log(ex);
                return new List();
            }
        }

        public ArticleBE GetArticleById(int idArticle)
        {
            try
            {
                if (idArticle < 0)
                {
                    throw new ArgumentException();
                }

                return _dataFactory.GetArticleById(idArticle);
            }
            catch (Exception ex)
            {
                LogError.Log(ex);
                return null;
            }
        }

        public void AddArticle(ArticleBE article)
        {
            try
            {
                if (string.IsNullOrEmpty(article.Title)
                    || string.IsNullOrEmpty(article.Author)
                    || article.Date == DateTime.MinValue)
                {
                    throw new ArgumentException();
                }

                _dataFactory.AddArticle(article);
            }
            catch (Exception ex)
            {
                LogError.Log(ex);
            }
        }

        public void EditArticle(ArticleBE article)
        {
            try
            {
                if (article.Id < 0
                    || string.IsNullOrEmpty(article.Title)
                    || string.IsNullOrEmpty(article.Author)
                    || article.Date == DateTime.MinValue)
                {
                    throw new ArgumentException();
                }

                _dataFactory.EditArticle(article);
            }
            catch (Exception ex)
            {
                LogError.Log(ex);
            }
        }

        public void DeleteArticle(ArticleBE article)
        {
            try
            {
                if (article.Id < 0)
                {
                    throw new ArgumentException();
                }

                _dataFactory.DeleteArticle(article);
            }
            catch (Exception ex)
            {
                LogError.Log(ex);
            }
        }

        public void Save()
        {
            try
            {
                _dataFactory.Save();
            }
            catch (Exception ex)
            {
                LogError.Log(ex);
            }
        }

        public void Dispose()
        {
            try
            {
                if (_dataFactory != null)
                {
                    _dataFactory.Dispose();
                }
            }
            catch (Exception ex)
            {
                LogError.Log(ex);
            }
        }
    }

Ici, le code devient un peu plus intéressant. Pour ne pas manipuler directement d'objets ArticleDA (ce qui permet de changer très facilement de source de données), le constructeur de ma classe demande en paramètre un objet IArticleDA. Si aucun objet implémentant IArticleDA n'est passé au constructeur, la classe utilisera le constructeur vide qui créera automatiquement un objet ArticleDA qu'elle passera au constructeur avec paramètre.

Vous pouvez également voir que les tests "métier", à savoir une tentative de lecture d'un article ayant un Id < 0, la tentative d'ajout d'un article sans titre, ... déclencheront la levée d'une ArgumentException. Le code de chaque méthode étant dans un Try Catch, cette exception sera attrapée et logguée (voir classe LogError dans la couche Core).

Enfin, la méthode Dispose appellera la méthode Dispose de notre classe implémentant IArticleDA, ce qui coupera la connexion à la BDD.

Voyons maintenant un exemple d'utilisation de nos méthodes métier. Si nous nous rendons dans le contrôleur ArticleController de l'application MVC, nous pouvons voir ceci :

public class ArticleController : Controller
    {
        public ActionResult Index(string message = "")
        {
            ViewBag.Message = message;

            using (ArticleBL business = new ArticleBL())
            {
                return View(business.GetArticles());
            }
        }

        public ActionResult Details(int id)
        {
            using (ArticleBL business = new ArticleBL())
            {
                return View(business.GetArticleById(id));
            }
        }

        public ActionResult Create()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Create(ArticleBE article)
        {
            using (ArticleBL business = new ArticleBL())
            {
                business.AddArticle(article);
                business.Save();
            }

            return RedirectToAction("Index", new { message = "The article has been created." });
        }

        public ActionResult Edit(int id)
        {
            ArticleBE article = null;

            using (ArticleBL business = new ArticleBL())
            {
                article = business.GetArticleById(id);
            }

            if (article == null)
            {
                return RedirectToAction("Index", new { message = "This article doesn't exist." });
            }

            return View(article);
        }

        [HttpPost]
        public ActionResult Edit(ArticleBE article)
        {
            using (ArticleBL business = new ArticleBL())
            {
                business.EditArticle(article);
                business.Save();
            }

            return RedirectToAction("Index", new { message = "The article has been updated." });
        }

        public ActionResult Delete(int id)
        {
            ArticleBE article = null;

            using (ArticleBL business = new ArticleBL())
            {
                article = business.GetArticleById(id);

                if (article == null)
                {
                    return RedirectToAction("Index", new { message = "This article doesn't exist." });
                }

                business.DeleteArticle(article);

                business.Save();
            }

            return RedirectToAction("Index", new { message = "The article has been deleted." });
        }

Et oui, pour utiliser nos données en base, il suffit de faire un using. A la sortie du using, la méthode Dispose de l'objet sera appelée, et donc la connexion sera automatiquement coupée. Cette méthode a ses avantages (sécurité, pas de risques de fuite mémoire) comme ses inconvénients (nombreuses connexions / déconnexions), mais rien ne vous empêche de la modifier.

Regardons enfin, en détail, les tests unitaires. Déjà, dans la couche Tests.Mock, le code est assez parlant :

public class MockArticleDA : IArticleDA
    {
        private List articles = new List()
        {
            new ArticleBE() {
                Id = 1,
                Title = "Demo 1",
                Tags = "demo,article,.Net",
                Date = DateTime.UtcNow,
                Content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vehicula fringilla quam, nec interdum arcu faucibus id. Sed placerat elementum venenatis. Quisque tempor sem a mi elementum at elementum orci aliquet. Morbi venenatis convallis orci. Sed vitae felis vel sapien cursus viverra. Nulla et elit non ante suscipit dictum. Proin id erat ligula. Vestibulum vehicula augue vel nunc consequat porta. Nam tincidunt lobortis lacus, eu varius velit vestibulum vitae. Sed quis nisi erat, at egestas odio. Cras tempor mauris et dui blandit placerat euismod libero sagittis.",
                Author = "demoUser",
            },
            new ArticleBE() {
                Id = 2,
                Title = "Demo 2",
                Tags = "demo,article,PHP",
                Date = DateTime.UtcNow,
                Content = "PHP : Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vehicula fringilla quam, nec interdum arcu faucibus id. Sed placerat elementum venenatis. Quisque tempor sem a mi elementum at elementum orci aliquet. Morbi venenatis convallis orci. Sed vitae felis vel sapien cursus viverra. Nulla et elit non ante suscipit dictum. Proin id erat ligula. Vestibulum vehicula augue vel nunc consequat porta. Nam tincidunt lobortis lacus, eu varius velit vestibulum vitae. Sed quis nisi erat, at egestas odio. Cras tempor mauris et dui blandit placerat euismod libero sagittis.",
                Author = "demoUser",
            },
            new ArticleBE() {
                Id = 3,
                Title = "Demo 3",
                Tags = "demo,article,Java",
                Date = DateTime.UtcNow,
                Content = "JAVA : Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vehicula fringilla quam, nec interdum arcu faucibus id. Sed placerat elementum venenatis. Quisque tempor sem a mi elementum at elementum orci aliquet. Morbi venenatis convallis orci. Sed vitae felis vel sapien cursus viverra. Nulla et elit non ante suscipit dictum. Proin id erat ligula. Vestibulum vehicula augue vel nunc consequat porta. Nam tincidunt lobortis lacus, eu varius velit vestibulum vitae. Sed quis nisi erat, at egestas odio. Cras tempor mauris et dui blandit placerat euismod libero sagittis.",
                Author = "demoUser",
            }
        };

        public List GetArticles()
        {
            // we clone the list to avoid to avoid any ref problem
            return articles.Select(e => e.Clone() as ArticleBE).ToList();
        }

        public ArticleBE GetArticleById(int idArticle)
        {
            ArticleBE result = articles.Where(e => e.Id == idArticle).FirstOrDefault();

            if (result != null)
            {
                // we clone the item to avoid to avoid any ref problem
                return result.Clone() as ArticleBE;
            }

            return null;
        }

        public void AddArticle(ArticleBE article)
        {
            article.Id = articles.Max(e => e.Id)   1;
            articles.Add(article);
        }

        public void EditArticle(ArticleBE article)
        {
            ArticleBE result = articles.Where(e => e.Id == article.Id).FirstOrDefault();
            if (result != null)
            {
                result.Author = article.Author;
                result.Content = article.Content;
                result.Date = article.Date;
                result.Tags = article.Tags;
                result.Title = article.Title;
            }
        }

        public void DeleteArticle(ArticleBE article)
        {
            ArticleBE result = articles.Where(e => e.Id == article.Id).FirstOrDefault();
            if(result != null) 
            {
                articles.Remove(result);
            }
        }

        public void Save()
        {

        }

        public void Dispose()
        {

        }
    }

On voit donc, ici, qu'on implémente bien notre interface d'accès aux données IArticleDA. Ainsi, pour tester notre couche de logique métier sur cette classe, il nous suffira de passer un objet MockArticleDA dans le constructeur de notre classe métier, et le tour est joué !

Notre classe MockArticleDA possède une liste d'articles créée à chaque construction d'un objet. La spécificité, ici, est que j'utilise la méthode Clone de nos objets ArticleBE pour ne renvoyer que des copies de ces objets à notre classe de tests. Cela permet d'éviter de modifier involontairement les données de nos objets par référence (par exemple dans le cas d'un test de modifications des données sans sauvegarde).

Enfin, je vous laisse vous intéresser à la classe ArticleBLTest, dont voici 2/3 méthodes :

[TestInitialize()]
        public void MyTestInitialize()
        {
            IArticleDA mockDataFactory = new MockArticleDA();
            business = new ArticleBL(mockDataFactory);
        }

        [TestMethod()]
        public void DeleteArticle_Generic_Test()
        {
            // params
            int nbArticles = 3;
            // --

            // we check if we have 3 articles
            List articles = business.GetArticles();
            Assert.AreEqual(nbArticles, articles.Count);

            // we delete the article
            business.DeleteArticle(articles.First());

            List actual = business.GetArticles();
            Assert.AreEqual(nbArticles - 1, actual.Count);
        }

        [TestMethod()]
        public void GetArticleById_With_Wrong_Parameters()
        {
            // params
            int fakeId = -1;
            // --

            ArticleBE article = business.GetArticleById(fakeId);

            Assert.IsNull(article);
        }

        [TestMethod()]
        public void AddArticle_With_Wrong_Parameters()
        {
            // params
            string author = "Me";
            string content = "It's not working !";
            string title = "Test article";
            string tags = "great,article,news";
            DateTime now = DateTime.Now;
            int nbArticles = 3;
            // --

            // we check if we have 3 articles
            List articles = business.GetArticles();
            Assert.AreEqual(nbArticles, articles.Count);

            ArticleBE article1 = new ArticleBE()
            {
                Author = author,
                Content = content,
                Tags = tags,
                Title = title
            };

            ArticleBE article2 = new ArticleBE()
            {
                Content = content,
                Date = now,
                Tags = tags,
                Title = title
            };

            ArticleBE article3 = new ArticleBE()
            {
                Author = author,
                Content = content,
                Date = now,
                Tags = tags,
            };

            // we create the articles. it shouldn't work
            business.AddArticle(article1);
            business.AddArticle(article2);
            business.AddArticle(article3);

            articles = business.GetArticles();

            // we check if we still have 3 articles
            Assert.AreEqual(nbArticles, articles.Count);
        }

Il est à noter que la méthode MyTestInitialize est appelée avant chaque test unitaire. Ensuite, pour chaque test, on vérifie les données déjà présentes, les données à la fin du test, et, en cas d'erreur, on verra tout de suite que le test a échoué. Comme vous avez pu le voir, mes méthodes de tests ont des noms très clairs, afin de savoir, au premier coup d'oeil, pourquoi nos tests ont pu échouer.

Je pense avoir fait le tour des spécificités du code de cette minuscule application. Si vous ne comprenez pas quelque chose, ou si vous avez besoin d'une précision, n'hésitez pas à demander dans les commentaires.

Enfin, pour déployer cette application chez vous, il vous suffit de restaurer la BDD dans un SQL Server 2008 (script Backup.bak, dans le zip fourni) et de modifier les chaînes de connexion dans les 3 fichiers de configurations (HTA.DemoBlog.DataAccess, HTA.DemoBlog.WebService et HTA.DemoBlog.Tests).

N'hésitez donc pas à tester cette "application" (HTA.DemoBlog.zip (1,54 mb)) et à me donner votre avis. Wink

Bonne journée à tous !

P.S. Oui je sais, le code n'est pas commenté, mais étant donné la simplicité des classes, je n'ai pas pensé que c'était nécessaire.

P.S.2 Quand j'aurais le temps, je ferais probablement une autre version un peu plus poussée avec Moq StructureMap.

P.S.3 Je crois que c'est mon article le plus long depuis le début de mon blog. 7869 mots, pour presque 30 000 caractères. L'équivalent d'un document Word de 15 pages ! Wouah !

image modifiée de Bert Kaufmann, sous licence CC

10 commentaires

Laurent - 06/01/2012 à 13:24:13

Sympa ton article Tommy, c'est le genre d'architecture que l'on fait pout notre projet.

Nous par contre on décompose un peu plus les projets de composants ou fonctionnalités communes (code pur, UI, UI avec composants de framework tiers) et aussi un projet par module (car on fait de la composition).

C'est tout de même pas mal d'avoir des éléments de comparaisons aussi de temps en temps, faudra que je me penche dans la solution (j'ai fait que lire l'article), je veux voir ce qui se fait ailleurs en termes d'injection de dépendances etc ;)

@répondre #lien

Julien Plée - 27/06/2012 à 14:15:41

Bonjour,

Je viens de faire une lecture rapide de votre post. L'architecture me semble très bien! Je trouve qu'il y a un bon niveau de découpage et donc de découplage. Et que l'injection de dépendances est utilisée avec parcimonie.

Seulement voila, j'ai peut être lu trop vite mais je n'ai pas compris l'intérêt de la méthode Edit de la classe ArticleDA qui n'utilise pas le résultat retourné par le mapping, le mapping ne faisant qu'une copie de dataObject dans le businessObject.
J'ai râté quelque chose?

Merci!

@répondre #lien

Tommy - 27/06/2012 à 17:03:44

Bonjour Julien,

Merci, oui cette architecture était très bien au moment où j'ai travaillé dessus, mais aujourd'hui je l'ai bien améliorée (j'ai supprimé tous ses défauts). J'essaierais de publier une mise à jour si je trouve le temps...

En fait, pour la méthode Edit, il y a une petite subtilité. Lorsqu'on appelle la méthode Map, on l'appelle avec 2 paramètres : le nouvel objet, et l'objet existant. On n'est pas dans le cas d'un mapping objet en base => objet métier mais dans le cas d'un mapping objet métier => objet en base.
Ainsi, la méthode Map, au lieu de créer un objet Article (copie de l'objet ArticleBE) va remplacer les valeurs dans l'objet concerné. Ainsi en faisant un Save() derrière, on aura bien mis à jour notre objet.

J'avais fait ceci pour éviter d'avoir à remplacer toutes les valeurs à la main directement dans le code métier, car éditer un objet Linq2Sql sans passer par chacun de ses attributs, c'est super galère... Du moins je crois (cet article a déjà 7 mois :-o)

Si tu as d'autres questions n'hésite pas

@répondre #lien

Julien Plée - 27/06/2012 à 17:12:11

Ah ok!
On a modifié l'objet dont la référence est trackée par le container d'entities donc au moment du save(), on l'embarque. Merci!

Par contre, je suppose qu'il va falloir un cache pour y mettre, soit ArticleDA, mais en fait je penche plutôt pour ArticleBL.
Cela, afin de conserver les références aux dataObjects entre les différents postback où l'utilisateur va éditer l'objet et celui où il va valider ses changements... à moins qu'on opte pour un choix d'enregistrer toute modification donnant lieu à un aller retour avec le serveur...

Mais bon, je pense que ça va demander un article complet de discuter des potentiels problèmes d'accès aux objets en mémoire et la performance qui en découle :)

@répondre #lien

Julien Plée - 27/06/2012 à 20:02:06

Ok, je me réponds, on fait bien le save après toute modification (cf. le contrôleur)

@répondre #lien

Tommy - 28/06/2012 à 14:28:26

Après j'adorerais aborder en détail beaucoup de ces problématiques, le problème c'est que je manque de temps.

Pis c'est sans compter que j'ai amélioré mon architecture démo, qu'en ce moment j'ai plutôt tendance à utiliser de la sérialisation xml que des BDD pour mes applications (vive le cache serveur), ...

Mais comme toujours, je manque de temps. Je commence à me demander si je ne devrais pas faire mes articles sous forme de videocasts / screencasts.
A creuser...

Si tu as des questions n'hésite pas :-)

@répondre #lien

Sampaya - 20/12/2012 à 09:21:25

A quand un article avec en plus moq et structuremap ?

@répondre #lien

Thomas - 07/10/2013 à 10:09:42

Bonjour,

L'article est extrêmement intéressant et propose un découpage projet très propre et très lisible. Pour la section de l'injection de dépendances il est possible de réaliser cela avec des frameworks de type Unity de Microsoft (http://msdn.microsoft.com/en-us/library/dd203101.aspx).
Pour les reste, l'article est un condensé de bonnes pratiques :)

Néanmoins, la source à télécharger (http://blog.howtommy.net/file.axd?file=2011/12/HTA.DemoBlog.zip) est inaccessible, pourriez-vous la remettre en place ?

Merci encore !

@répondre #lien

Tommy - 08/10/2013 à 08:28:18

Bonjour Thomas,

Effectivement, ayant changé de moteur de blog, l'article n'est plus disponible sur cette URL.

Voici la nouvelle URL :

http://blog.howtommy.net/img/2011/12/HTA.DemoBlog.zip

Je vais mettre l'article à jour. Merci !

[J'ai d'ailleurs une nouvelle architecture encore plus propre avec Unity pour l'injection de dépendances que je pourrais publier. Je ferais ça quand j'aurais le temps si ça intéresse des gens.]

@répondre #lien

jackboy - 16/02/2015 à 20:21:05

Salut,

As-tu mis à jour ton code, tel qu'indiqué dans ton commentaire du 27/06/2012 ?

Merci !

@répondre #lien

icon Flux RSS des commentaires de cet article

Les commentaires sont fermés pour cet article