1ère partie d'une série de billets sur le sujet de l'architecture objet et services.
Développer objet c'est bien, architecturer c'est mieux. PAC (ou MVC) et SOA apportent des réponses efficaces sur de grosses plateformes où sont gérés de multi-projets suceptibles d'interagirent entre eux : réutilisation d'objets, de modules, de services, tout cela permet de rationnaliser les développements et de construire sur le long terme un véritable système d'information.
Le monde objet est un domaine vaste, cela va des principes de base de la POO (héritage ...trop souvent mal utilisé d'ailleurs, encapsulation, ...), aux Design Patterns qui permettent d'optimiser son code (un peu comme des bonnes pratiques) afin de le rendre plus souple et plus lisible et donc in fine plus évolutif et maintenable (notion fondamentale en ingénierie logicielle), aux architectures orientées objet et services.
Nous avons tatonné sur plusieurs modèles avant de trouver un (bon) compromis sur la découpage d'un projet, cela méritait une série de billets. Parmi les différents essais, on trouvera principalement 2 modèles :
- architecture 2-tiers : une (grosse) couche présentation dans un projet Web et une couche de traitements combinée à la DAL.
- architecture n-tiers : 1 couche présentation (ie : Web), 1 couche connecteur (interfaces des contrôleurs, implémentation des contrôleurs (traitements), fabrique d'objets), 1 couche DAL (interfaces de service, contrôleurs du service, DAO), 1 couche modèle d'entités (interfaces des entités, classes d'entités)
La 1ère, trop simple, n' apportait pas suffisamment d'abstraction, et surtout formait 2 pavés monolithiques : toute application Web se retrouvait dans un même et seul projet, ce qui ne facilite pas le déploiement dans le cas de correction de bugs par exemple. Egalement une trop forte adhérence entre l'application Web et la couche de traitement (BLL ou Business Logic Layer), les classes contenues dans cette dernière étaient directement instanciées : manque d'abstraction et de découplage.
La 2nd, au contraire, était trop compliquée à mettre en oeuvre pour chaque projet, malgré sa structure stricte, apportant abstraction (notamment par le nombre d'interfaces et de couches) et donc découplage.
Couches objet
Avec mon brillant, vu qu'il lira le billet ;-), collègue et architecte (dit le doc
) qui a su bien consolider ce type d'architecture, nous avons pu au fur et à mesure des projets développés, tirer les leçons de telle ou telle mise en place et accoucher d'un modèle architectural qui s'adapte assez bien à notre environnement : multi-projets Web sur la même plateforme, ORM NHibernate, divers progiciels utilisés.
Cette forme d'architecture se nomme la méthode PAC ou un dérivé en tout cas. Celle-ci permet d'architecturer sous forme de 3 projets (Présentation, Contrôleur, Implémentation : logique métier ET DAL) afin d'abstraire l'implémentation des traitements ou règles métier de la couche présentation. Cela signifie que la logique métier/couche métier n'est jamais visible de la couche présentation (a contrario de MVC), la couche Contrôleur sera le ciment entre les deux.
Parmi les couches objets, on trouvera :
- la couche présentation : couche de plus haut niveau, peut être une application Web, une application (Windows : fenêtrée, console, services windows, ...), ...elle ne connait que les interfaces de la couche métier, ainsi que le modèle ou domaine utilisé (on parlera d'entités = presque une vue des tables). En revanche, l'implémentation (la logique ou "comment on fait", l'algo dira-t-on), elle ne la connait pas, elle fait confiance (approche "contrat" grâce aux interfaces).
- la couche contrôleur : contient les interfaces des contrôleurs, qui ont pour but d'exposer les contrats (interfaces, classes abstraites), ou les méthodes (de la logique métier) qui seront proposées à la couche présentation. Elle offre aussi le moyen d'abstraire l'implémentation, et de rendre à la couche présentation une interface qui représente le "moteur". Pour cela (il faut bien instancier un moment donné), on trouvera souvent une classe Factory, chargée de faire ce lien. Cette couche fait aussi le lien entre les différentes couches et les entités utilisées (classes métiers ou le modèle), celles-ci sont contenues dedans.
- la couche métier ou l'implémentation des contrats contrôleurs : cette couche a pour objectif d'implémenter les contrats qui ont été exposés par la couche contrôleur à la couche présentation : on trouvera le code qui implémentent la logique. Egalement lors l'utilisation d'un ORM, on implémentera dans ce projet l'accès aux données (appelée aussi la DAL).
Pour que cela fonctionne, on introduit le principe d'abstraction, ou l'appel à l'implémentation réelle est reculée le plus possible, et rendue possible grâce aux Factory et autres interfaces qui permettent cela.
Un schéma pour résumer l'interaction entre ces 3 couches :
L'avantage de type d'architecture est de découpler toujours plus les composants logiciels.
Cas pratique
Afin d'illustrer ce modèle, je vous propose de mettre en place un module de recherche. Le code sera écrit en C# à l'aide de Visual Studio.
Projets sous Visual Studio
Nous créons 4 projets sous Visual Studio qui représentent la présentation (application Web), le contrôleur, et 2 implémentations possibles (couche métier) :
MyWeb : projet Web MyControler : bibliothèque de classes - contient les interfaces, les fabriques métiers ainsi que les classes entités et autres Helpers
Nous proposons divers implémentations pour effectuer la recherche :
SearchSQLRules : bibliothèque de classes - implémentation spécifique pour une recherche simple en base, typiquement dans les divers champs, sous forme de LIKE SearchLuceneRules : bibliothèque de classes - implémentation spécifique pour une recherche avec le moteur Opensource Lucene.NET
Pourquoi faire 2 implémentations (ou 3, ...) ? Tout simplement, imaginons que vous fassiez une application, et selon l'utilisateur, vous n'ayez pas les mêmes moyens, et selon, vous proposerez l'une ou l'autre solution, avec un minimum de modification de code ! Egalement, cela peut arriver de devoir faire le choix entre 2 implémentations en temps réel, ou bien pour basculer d'un système vers un autre, selon une stratégie bien établie.
Nous faisons simple pour le contrat :
- List<Result> Search(string keyword);
Pour rappel, les types génériques sont apparus en .NET 2.0. Dans notre cas, cela permet d'avoir une liste fortement typée, en lieu et place d'un ArrayList qui oblige un transtypage (cast) de l'objet contenu : 1 opération coûteuse et oblige de connaitre à l'avance le type, ce qui amène parfois de mauvaises surprises. Nous pourrions utiliser une interface IList pour le retour des éléments à ramener. Pour la suite de l'exercice, je ne le fais pas sciemment, cela sera expliqué dans le 2ème volet du sujet. De même on pourrait utiliser une interface IResult, et pour les mêmes raisons, cela ne sera pas utiliser dans notre exemple. La possibilité d'abstraction est aussi liée à la limite du langage ou de la technologie (services Web par exemple).
Interface et contrôleur
L'interface que les couches métiers devront implémenter afin de répondre à nos besoins, on insère cette interface dans un fichier nommé ISearchManager.cs
- public interface ISearchManager {
- List<Result> Search(string keyword);
- }
Le projet contrôleur contient alors :
- des interfaces métiers et techniques : l'interface ISearchManager, une interface IManagerControler qui servira à retourner les différents contrôleurs dont on a besoin (ici ISearchManager, souvent, on fera 1 Manager par cas d'utilisation / ensemble de fonctionnalités de même portée), 2 interfaces IContextContainer et IContextContainerFactory qui serviront pour l'instanciation de la DAL (via l'ORM utilisé) grâce au pattern d'injection de dépendance (principe d'IoC)
- on pourrait aussi y mettre des interfaces que les entités devront implémenter, cela donne une certaine garantie quant au bon usage des objets (qu'il ne manque rien du point de vue des propriétés)
- la fabrique des couches métiers sous forme d'un helper (classe utilitaire qu'on nommera MyConnectorHelper), chargée d'instancier dynamiquement l'IManagerControler (renverra par la suite l'instanciation de ISearchManager), ainsi que IContextContainerFactory (renverra par la suite IContextContainer)
- les entités métiers : ici la classe Result qui pourra contenir le chemin d'une page, le résumé trouvé, ...
bref, tout ce qui est transverse aux différentes couches (projets).
Pour résumer, l'arborescence du projet MyControler que j'ai l'habitude d'organiser :
Fin du 1er épisode qui explique les principes généraux. La prochaine fois, on s'attaquera aux implémentations, et autres fabriques d'objets. Enfin pour finir par une 3ème partie qui soulévera les défauts ou questions diverses, auxquelles on tentera d'y apporter quelques réponses ou du moins certaines pistes de recherche.
Et toi, cher visiteur, sauras-tu quoi faire dans la fabrique MyConnectorHelper, et de la manière dont nous allons nous prendre pour instancier les différents IManagerControler, ISearchManager, IContextContainer ou IContextContainerFactory ?