![]() |
Les conteneurs légers du futur arrivent à grands pas par Sami Jaber (DotNetGuru.org) |
|
|
|
|
|
epuis
plusieurs mois, un regain d'intérêt pour les conteneurs légers semble se
dessiner sur fond de contestation grandissante à l'égard des Framework
d'entreprise de type EJB (Enterprise Java Beans). Ces nouveaux conteneurs
répondent aux doux nom de Spring,
Pico,
Avalon ou
HiveMind. Ils
sont devenus la clé de voûte d'une résistance accrue envers la domination quasi
monopolistique des conteneurs lourds, et partout les avis convergent pour mettre
en avant leur vertus. Si la plupart d'entre eux sont aujourd'hui disponibles
uniquement sur plateforme J2EE, les développeurs .NET commencent à s'y
intéresser de près avec le portage en cours de
Pico en C#. Nous allons vous présenter dans cet article cette nouvelle
manière de concevoir les applications J2EE ou .NET qui fait la part belle à
l'abstraction des composants et à l'indépendance technique vis à vis des API,
quelles qu'elles soient.
Enfin, il est important de souligner que tous ces conteneurs possèdent un point commun, ils implémentent le pattern IoC (Inversion Of Control) connu également sous le nom de "Dependency Injection" ou "Injection de code". Un pattern que nous vous détaillerons dans un premier temps avant de vous présenter la démarche plus générale proposée par ces conteneurs légers de nouvelle génération.
De tout temps, l'architecte a cherché à trouver des abstractions permettant de séparer les composants d'une application par zones de responsabilités. C'est dans cette optique que sont apparues les architectures multi-tiers et les différentes couches qui l'a composent. De la couche de présentation à la couche de données, nombreux sont ceux qui ont cherché à séparer le code métier d'une application du code dit "technique". Que ce soit à travers une démarche de type MDA (Model Driven Architecture), SOA (Service Oriented Architecture) ou AOP (Aspect Oriented Programming), le besoin reste le même. Subir le poids des évolutions techniques en épargnant au maximum le code métier.
Sun a également participé à cette vaste réflexion en proposant un Framework désormais connu de tous : les Enterprise Java Beans. Le but des EJB a toujours été de proposer un socle purement technique au développement de composants métier. Le développeur se focalisant uniquement sur l'aspect applicatif de son code en laissant le soin au serveur d'application de gérer les différents services techniques (Transactions, Persistance, Sécurité, Montée en charge). Or, si les EJB ont été bénéfiques en terme de structuration, ils ont échoué dans leur rôle de simplification et d'abstraction. Preuve en est la manière dont les applications ont dû gérer le passage des EJB 1.1 vers la norme 2.0. Nombreux sont ceux ayant dû combler le manque de support des objets dépendants (pattern ValueObject) par une implémentation spécifique. Et aujourd'hui, force est de constater qu'une migration vers les interfaces locales des EJB entité rendent parfois obsolète l'emploi d'objets DTO. Pour résumer, un Framework bien implémenté doit évoluer de manière interne sans imposer de modification structurelle aux composants applicatifs. C'est tout l'enjeu des conteneurs légers, proposer une infrastructure légère (lightweight) pouvant fonctionner dans une applet, une application Web, un composant Swing ou WinForm tout en fournissant la plupart des services techniques nécessaires aux applications d'entreprise. Et ce, sans exposer d'API technique.
Pour cela, les Framework IoC puisent leur inspiration dans les variantes du Design Pattern IoC que nous vous présentons dans cet article.
Il existe plusieurs manières de coupler un composant d'un autre. La première appelée couplage par implémentation consiste à créer une dépendance forte avec l'implémentation du composant tiers.
Prenons un exemple :
public class MyBusinessObject {
LoginAuthentification l =
new
LoginAuthentification(ConnString) ;
l.authenticate("Mister
Dupont","1RE54");
}
Cette manière est conseillée uniquement lorsque vous maîtrisez les évolutions de la classe LoginAuthentication dont la pérennité est assurée. En effet, non seulement l'assemblage du composant LoginAuthentication devra être connue et déployée en même temps que votre application, mais si plus tard, vous décidez de changer la manière dont vous authentifiiez vos clients (LDAP, Kerberos, XML, SGBD, etc ...), les modifications seront peut-être exponentielles.
D'où la seconde forme de couplage sur laquelle s'appuie le pattern IoC : le couplage par interface.
Exemple :
public class MyBusinessObject {
IAuthentification l =
new
MyLoginAuthentification(ConnString) ;
l.authenticate("Mister Dupont","1RE54");
}
public
interface
IAuthentification {
public
boolean authenticate(String login, String passwd);
}
Dans cette forme de couplage, les clients sont liés à des interfaces pérennes dans le temps. Et le changement d'implémentation se fait simplement en modifiant la création de l'objet de type MyLoginAuthentification par un autre objet du même type.
C'est un aspect essentiel du pattern IoC. Lorsque la dépendance intervient avec la classe concrète, l'ordre New impose la présence de la classe d'implémentation associée. L'inconvénient de cette écriture est évidemment l'impossibilité de déployer une autre version (par exemple à chaud) du composant d'authentification sans la réécriture préalable du code source. De plus, le composant MyBusinessObject ayant une référence directe sur son correspondant, la gestion du cycle de vie de IAutentification devient de sa responsabilité. On est ici très loin d'un modèle où tous les composants seraient branchés tels des "plugin" venant s'insérer sur un bus logiciel.
Pour illustrer le principe, voici les deux méthodes précédentes schématisées :
(Sans
IoC)
(Avec IoC)
Le principe consiste à inverser le contrôle avec une classe supplémentaire chargée d'initialiser les dépendances entre les différents composants. On ajoute une indirection et on sort ainsi des composants la responsabilité de créer les classes dépendantes. Dans la pratique, voici le code de la classe Initializer charger de réaliser l'inversion :
myobj =
new MyBusinessObject() ;
IAuthentification auth = new
AuthentificationImpl();
myobj.setAuth(auth);
Mais qu'est-ce donc cette fameuse méthode setAuth() qui n'existait pas à l'origine dans notre code ? Elle représente ce qu'on appelle un mode particulier "d'injection de code". Voyons sans plus attendre ce nouveau concept.
Martin Fowler est à l'origine du terme Dependency Injection ou Injection de code. Pour cet architecte de renom, IoC est un terme bien trop générique pour permettre une réelle distinction des différents types d'implémentations IoC existantes. En effet, jusqu'ici, il était plus commun de leur attribuer des numéros (IoC type 1, type 2, etc ...). L'injection de code est la méthode adoptée par un conteneur pour résoudre les dépendances entre composants.
L'injection par constructeur est la méthode utilisée par le conteneur Pico. Elle consiste à déclarer toutes les dépendances dans le constructeur du composant. Le conteneur s'appuiera sur les différents paramètres du constructeur pour résoudre et instancier les composants dépendants. Ainsi, notre exemple précédant aurait la forme :
public
class
BusinessObject {
IAuthentication _auth ;
public
BusinessObject(IAuthentication auth) { _auth=auth; }
}
Quant à notre classe Initializer, elle contiendrait le code suivant :
private
MutablePicoContainer Init() {
MutablePicoContainer pico = new
DefaultPicoContainer();
Parameter[] finderParams = null ;
pico.registerComponentImplementation(AuthenticationImpl.class,
IAuthentication.class, finderParams);
pico.registerComponentImplementation(BusinessObject.class);
return
pico;
}
Pour réaliser un test unitaire, il suffira simplement d'écrire :
MutablePicoContainer pico = configureContainer();
BusinessObject bo = (BusinessObject) pico.getComponentInstance(BusinessObject.class);
bo.authenticate("Raymond",
"pass");
Non seulement notre code client ne contient aucune API technique, mais la résolution des dépendances est fortement typée. Les opérations de cast (conversion) ne sont pas nécessaires, et nul besoin de récupérer dynamiquement une instance du composant associé via un mécanisme type reflection.
En revanche, cette méthode d'injection impose au client de résoudre tous les liens vers d'autres composants à l'initialisation dans son constructeur. Cela peut nuire à la lisibilité du code si les paramètres sont nombreux, et en présence d'un arbre d'héritage complexe, cette écriture peut vite s'avérer très lourde.
L'injection par accesseur est la méthode utilisée par Spring. Les dépendances sont formalisées dans un fichier de configuration XML (à noter que Pico gère également la lecture de ce genre de fichier) lu préalablement par le programme d'initialisation du conteneur. Lorsqu'une méthode de type Java Bean (setXX) est trouvée dans un composant, le programme l'invoque avec en passant une instance du bean référencé dans le fichier de configuration.
<?xml
version="1.0"
encoding="utf-8"
?>
<beans>
<bean
id="BusinessObject"
class="BusinessObject">
<property
name="authenticator">
<ref
local="IAuthentication"/>
</property>
</bean>
<bean
id="IAuthentication"
class="AuthenticationImpl">
<property
name="server">
<value>10.54.25.44</value>
</property>
</bean>
</beans>
Donne au niveau conteneur :
myobj =
new MyBusinessObject() ;
IAuthentification auth = new
AuthentificationImpl();
myobj.setAuthenticator(auth);
et au niveau du code utilisateur :
public
class
BusinessObject {
IAuthenticate _auth ;
public
void
setAuthenticator(IAuthentication auth) { _auth=auth;}
public
void
authenticate(String login, String pass) { _auth.authenticate(login,pass); }
}
C'est la méthode que nous avons employé dans l'exemple précédent pour vous illustrer le principe d'injection de code.
L'injection par interface est l'approche adoptée par le conteneur Apache Avalon. Elle consiste à tirer partie des interfaces pour résoudre les dépendances. Chaque composant devant implémenter une interface spécifique par type d'injection.
class BusinessObject implements InjectAuthentication, ...
IAuthentication _auth;
public
void
injectAuth(IAuthentication auth) {
_auth = auth;
}
public
interface
InjectAuthentication {
void
injectFinder(IAuthentication auth);
}
Les inconvénients de cette méthode sont nombreux. Non seulement la lisibilité du code est soumise à rude épreuve du fait de la multiplication des interfaces, mais l'injection ne devient plus totalement transparente pour le client car elle impose une forme d'écriture assez lourde.
Martin Fowler propose également le Design Pattern Service Locator pour gérer la dépendance entre composants. C'est la méthode de prédilection des EJB. En effet, dans la spécification J2EE, un composant EJB doit interroger l'annuaire JNDI pour disposer d'une référence vers un autre composant. L'interface locale ou distante est mise à profit pour manipuler un proxy de l'EJB Object. Nous n'entrerons pas dans le détail de ce Design Pattern qui existe sous d'autres variantes (Dynamic Service Locator, Segrated Interface, etc...) mais sachez qu'il impose une forme d'écriture bien particulière (appel d'un singleton ou d'un annuaire via lookup et conversion de type). Martin Fowler oppose à ce pattern sont inaptitude à s'adapter aux mock object (tests unitaires).
Maintenant que vous connaissez les différents types d'injection, attardons nous un instant sur leur apport. Si l'approche IoC et les conteneurs légers sont aussi populaires, c'est en partie grâce à l'abstraction qu'ils procurent. Prenons le cas du PetShopDNG qui constitue notre application de référence en terme de Design et d'architecture.
Dans ce PetShop, nous avons essayé de bâtir plusieurs couches autonomes indépendantes l'une de l'autre. Il est par exemple possible d'invoquer un composant métier distribué (.NET Remoting, Web Services) ou local (In-process) simplement en modifiant un fichier de configuration. Par ailleurs, la couche de persistance (DTM) est totalement masquée au profit de couches clientes manipulant des entités via leurs interfaces. Pour ce faire, il a fallu mettre en place des Abstract Factories permettant de découpler chaque objet. Non seulement cela implique une intrusion minimale dans le code, mais le court-circuitage de service technique est difficilement réalisable lorsque le Framework cible (en l'occurrence .NET) ne l'intègre pas nativement (pas de propagation automatique des transactions dans .NET Remoting par exemple, la sécurité non plus).
En résumé, l'abstract Factory et l'instanciation dynamique de code sont les parents pauvres de la démarche IoC. Pourquoi vouloir à tout prix gérer le cycle de vie d'une dépendance là où un objet intermédiaire pourrait s'en charger ? D'autre part, si des techniques comme la génération de code (AOP) permettent à juste titre d'insérer un traitement entre émetteur et fournisseur, l'IoC produit un résultat similaire pour un moindre effort.
Pour mieux comprendre, imaginons que l'initialiseur du
conteneur léger (appelé également Bean Factory) s'attache à lier les composants
entre eux via des proxies et non plus des références directes.
Le code de l'initialiseur aurait la forme suivante :
public
class
TxProxy implements IAuthentication {
IAuthenfication _realObject ;
TxProxy (IAuthenfication realObject) { _realObject=realObject;}
boolean authenticate(String login, String pass) {
return
_realObject.authenticate(login, pass);}
}
<beans>
<bean
id="TxProxy"
class="TxProxy"></bean>
<bean
id="BusinessObject"
class="BusinessObject">
<property
name="authenticator">
<ref
local="IAuthentication"/>
</property>
</bean>
<bean
id="IAuthentication"
class="TxProxy">
<property
name="server">
<value>10.54.25.44</value>
</property>
</bean>
</beans>
Avec un tel mécanisme, le conteneur n'a plus qu'à passer une référence de type IAuthentication, en réalité un Proxy (par exemple généré) vers l'objet réel à la classe BusinessObject. En pratique, les conteneurs utilisent plusieurs types de mécanismes pour mettre en oeuvre les services techniques :
La programmation par aspect (AOP)
Les dynamic proxies (réservé à Java 1.3) ou RealProxy (.NET)
Les intercepteurs
Spring propose dans le même registre l'équivalent des custom attributes de .NET en Java, nommés Custom Metadata dans ce Framework. La logique du Proxy est utilisée également pour masquer la complexité des EJB (ou ServicedComponent d'ailleurs) aux applications clientes. En fournissant une vue IoC aux API clientes par le biais de classes abstraites internes, Spring assure un découplage maximum. Pas un seul import (using en C#) de packages EJB (ou ServicedComponent) dans ce composant.
private MyComponent myComponent;
public
void
setMyComponent(MyComponent myComponent) {
this.myComponent
= myComponent;
}
Il y a quelque chose de magique dans cette forme d'écriture. Le simplissime
poussé à l'extrême allant jusqu'à faire penser à certains que nous arriverons un
jour à réaliser des applications
sans une
seule ligne de code.
Notez au passage le transfert de la complexité vers le fichier de configuration :
<bean
id="myComponent"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property
name="jndiName"><value>myComponent</value></property>
<property
name="businessInterface"><value>com.mycom.MyComponent</value></property>
</bean>
<bean
id="myController"
class
=
"com.mycom.myController">
<property
name="myComponent"><ref
bean="myComponent"/></property>
</bean>
Cette simplicité est parfaitement décrite par l'auteur de Spring, Rod Johnson : "The web tier code has no dependence on the use of EJB. If we want to replace this EJB reference with a POJO or a mock object or other test stub, we could simply change the myComponent bean definition without changing a line of Java code"
La plupart des conteneurs IoC proposent une abstraction des
mécanismes transactionnels. Dans le monde EJB, JTA est le standard de fait mais
n'est pas supporté par tous les éditeurs (il est nécessaire de disposer d'un
Datasource compatible JTA). Sans compter qu'en mode local, le gestionnaire de
transaction de votre outil de mapping préféré pourra très bien faire l'affaire.
Sur le sujet, Rod Johnson propose d'autres arguments : "Isn’t JTA the best
answer for all transaction management? If you’re writing an application that
uses only a single database, you don’t need the complexity of JTA. You’re not
interested in XA transactions or two phase commit. You may not even need a
high-end application server that provides these things. But, on the other hand,
you don’t want to have to rewrite your code should you ever have to work with
multiple data sources.
Imagine you decide to avoid the overhead of JTA by using JDBC or Hibernate
transactions directly. If you ever need to work with multiple data sources,
you’ll have to rip out all that transaction management code and replace it with
JTA transactions. This isn’t very attractive and led most writers on J2EE,
including myself, to recommend using global JTA transactions exclusively. Using
the Spring transaction abstraction, however, you only have to reconfigure Spring
to use a JTA, rather than JDBC or Hibernate, transaction strategy and you’re
done. This is a configuration change, not a code change."
Côté Microsoft, en attendant Indigo, les choses sont un peu plus compliqués. MSDTC et COM+ restent la référence. Le code source suivant illustre un composant Indigo mettant en oeuvre une gestion explicite des Transactions. Lorsque les Framework IoC .NET seront opérationnels (c'est à dire d'ici quelques années ;-)), il ne serait plus utile de convertir la transaction EnterpriseServices en transaction locale de la base de données. Un mécanisme unifié sera proposé qui encapsulera les implémentations spécifiques disparates. Avouez que ce ne sera pas un luxe.
Toujours est-il que pour cela, les éditeurs de Framework IoC .NET devront trouver une abstraction assez générique permettant d'intégrer MSDTC et toutes ses implémentations Indigo-esque. Un projet loin d'être évident vu la complexité des API et leur adhérence à l'OS (COM+ 1.5 ajoute par exemple certaines fonctionnalités sous XP).
La persistance est un des aspects importants de la conception IoC. Spring fournit plusieurs "connecteurs" permettant de tirer partie de différents outils ou API. En standard JDO et Hibernate sont proposés. Le code client est lié à des interfaces de type DAO via une injection de code. L'accès à l'outil de mapping se fait de manière transparente. Pour mieux vous en convaincre, nous ne saurions trop vous conseiller de jeter un oeil à l'application d'exemple jpetstore de la société ibatis, porté pour l'occasion sous Spring Framework. Les classes métier son pures, les contrôleurs et façades fonctionnelles ne contiennent aucune adhérence vers des API techniques. Et le mode de persistance est "pluggable" à souhait.

Nous ne nous attarderons pas sur cet aspect des Framework IoC car la couche de présentation ne constitue pas la partie la plus sensible en terme de Design. Par défaut, le modèle proposé par les outils du type Struts sont jugés trop contraignant par les auteurs des Framework IoC. La plupart d'entre eux fournissent des Framework alternatifs. Mais Spring propose un Plug-In permettant d'abstraire Struts pour exposer un modèle MVC IoC. Dans le monde .NET, les Framework IoC pourront ainsi proposer un modèle permettant de tirer partie aussi bien d'APIs telles que IUP (Interface User Process Application Block) ou ASP.NET.
Les conteneurs légers IoC fournissent la plupart des services techniques proposés par les conteneurs lourds de type EJB sans l'adhérence aux API tant décriée. Il est aussi étonnant de constater le niveau de maturité atteint par les différents conteneurs du marché. Une maturité qui tranche un peu avec le silence ambiant autour de ces concepts. Excepté Martin Fowler et Rod Johnson, très peu de documentations sont pour l'heure disponibles sur les conteneurs légers. Et tant qu'il n'y aura pas un standard dans ce domaine, les travaux resterons en l'état. Car pour l'heure, si tout le monde s'accorde à dire que les Framework d'entreprise (structurant) seront à terme remplacés par les conteneurs légers, aucun compromis semble trouvé sur la méthode d'injection à adopter. Pico et Spring se livre une bataille féroce et les grands architectes comme Martin Fowler conseillent une gestion multiples des modes d'injection afin d'éviter les écueils.
Autant dire que J2EE 1.5 compatible IoC n'est donc pas prêt de voir le jour.
Auteur : Sami JABER
Copyright : DotNetGuru © Mars 2004
-------------------------------------------------------------------
ERRATA
DNG a reçu ce mail que nous avons tenu à vous montrer. L'auteur, Sylvain Wallez est commiter du projet Avalon et apporte quelques éclaircissements sur ce conteneur rapidement abordé dans l'article. Un mail qui va bien au delà de la simple présentation mais qui préfigure des fonctionnalités de l'outil.
--
De : Sylvain Wallez
Envoyé : lundi 15 mars 2004 21:13
À : webmaster@dotnetguru.org
Objet : Votre article sur les conteneurs légers
Bonjour,
Je viens de lire avec grand intérêt votre article sur les conteneurs légérs. Je
suis committer sur les projets Cocoon et Avalon, et membre de la fondation
Apache. J'aimerais apporter quelques précisions sur Avalon qui n'est pas très
bien décrit dans votre article.
Tout d'abord, Avalon est l'ancêtre des conteneurs IOC, puisqu'il a démarré en
2000 (il est issu des premières phases d'architecture de Cocoon 2) et que Pico a
été créé par d'anciens développeurs d'Avalon. A ce titre, il est plus complexe
mais aussi plus complet.
Avalon n'est pas exactement basé sur "l'injection par interface". Le framework
définit des interfaces qui définissent le cycle de vie d'un composant, et qui
sont appélées par le conteneur pour lui fournir un certain nombre
d'informations. Deux interfaces de ce cycle de vie sont essentielles :
Configurable et Serviceable. Configurable permet au conteneur de passer au
composant une configuration (sorte de "DOM léger") venant d'un document de
configuration externe global qui décrit l'ensemble des composants et leurs
paramètres. L'interface Serviceable permet au conteneur de passer au conteneur
un "ServiceManager" qui est du type "service locator", sans avoir les
inconvénients du modèle JNDI (on fait très facilement des tests avec mock
objects).
Exemple:
class MyComponentImpl implements IMyComponent, Configurable, Serviceable {
IAuthenticator _auth;
String _defaultuser;
public void service(ServiceManager manager) {
_auth = (IAuthenticator)manager.lookup(IAuthenticator.ROLE);
}
public void configure(Configuration config) {
_defaultUser = config.getChild("default-user").getValue();
}
}
Le document XML d'assemblage ne définit pas les dépendances, mais la liste des composants pour chaque rôle dans le système:
<system>
<my-component class="MyComponentImpl">
<default-user>John</default-user>
</my-component>
<authenticator class="AuthenticatorImpl">
<server>10.54.25.44</server>
</authenticator>
</system>
Cette approche n'est pas aussi souple que celle de Spring, car la
configuration globale ne fait qu'identifier les implémentations (et paramètres)
des différents rôles intervenant dans le système des composants, mais pas leur
dépendances qui est gérée par lookup.
Toutefois, je trouve que Spring va trop loin et doit rapidement dépasser les
limites de la complexité maîtrisable dans de grands systèmes (cf Cocoon qui,
avec toutes ses options, peut avoir plusieurs dizaines de composants).
Enfin, contrairement à tous les autres conteneurs légers, Avalon a la capacité
de gérer les objets non réentrants. La configuration de Spring, par exemple,
produit une seule instance pour chaque "bean" nommé. Avalon peut gérer un pool
d'instances qui sont données à la demande lors du lookup. Cette fonction est
bien évidemment essentielle pour les applications serveur.
Avalon impose aussi une adhérence aux interfaces du cycle de vie, mais des
travaux sont en cours, et la prochaine version proposera aussi une injection par
accesseurs.
Il aurait aussi été intéressant de mentionner OSGi, plateforme de composants
issue du monde de l'embarqué et adoptée par Eclipse 3.0. OSGi permet très
facilement de (télé)charger et décharger des composants à chaud, ce qui est
essentiel pour les clients riches, pour lesquels Eclipse 3.0 a à mon avis un
grand avenir.
Enfin, j'approuve la conclusion : autant les services de base peuvent être
assurés par simple injection de dépendances et sans aucune adhérence à une API
particulière, autant les utilisations plus avancées nécessitent forcément des
API qui vont au-delà des propriétés JavaBean et nécessiteront donc une
normalisation pour être largement utilisées.
Je ne sais pas si votre article est susceptible d'évoluer, mais j'espère en tout
cas que ces explications vous auront intéressé !
Cordialement,
Sylvain Wallez
--
Sylvain Wallez Anyware Technologies
http://www.apache.org/~sylvain http://www.anyware-tech.com
{ XML, Java, Cocoon, OpenSource }*{ Training, Consulting, Projects }
Références
Martin Fowler : Dependency Injection Pattern
Spring Framework : TheServerSide.com (un article à lire absolument pour mieux comprendre)