Navigation

La vertu des architectures simples

Un article de ODcWiki.

Jump to: navigation, search
Image:Auteur.png Auteur : hdarmet.


Question : combien de lignes Java sont-elles nécessaires pour animer un écran web affichant une liste d’entités persistantes et permettant de créer, modifier et supprimer ces entités ? Réponse : 24. Vingt-quatre lignes seulement, si l’on fait abstraction de la définition de l’entité elle-même. Ces vingt-quatre lignes représentent l’ensemble du code nécessaire pour que l’information fasse l’aller et le retour entre la base de données et l’utilisateur. Elles ne cachent aucun service, DAO ou configuration supplémentaires pour que notre page existe.

Cette page a un comportement très simple, certes, mais nombreuses sont les pages dans nos applications qui sont dans ce cas. Et puis, définir une page plus complexe nécessite plus de lignes, bien sûr. Mais on reste dans le même ordre de grandeur : quelques centaines tout au plus. Il reste bien sûr à définir la page elle-même en pseudo-HTML (une facelet Seam/JSF dans notre cas). Ainsi que l’entité éditée. Il est amusant de noter que la définition de la page elle-même est notoirement plus longue (une cinquantaine de lignes) que le code Java qui la rend intelligente.

@Name("editAgencies")
@Scope(ScopeType.CONVERSATION)
public class AgencyEditWebBean {
 
	@In
	EntityManager entityManager;
 
	List<Agency> agencies = null;
	 
	@Begin(join=true)
	public List<Agency> getAgencies() {		
		if (agencies==null) {
			agencies = entityManager.createQuery("from Agency").getResultList();
		}
		return agencies;
	}
 
	public void createAgency() {
		Agency agency = new Agency("No name");
		agencies.add(agency);
		entityManager.persist(agency);
	}
	
	public void deleteAgency(Agency agency) {
		entityManager.remove(agency);
		agencies.remove(agency);
	}
}

Les 24 lignes nécessaires pour animer une liste en édition avec Seam

Voici la facelet qui dessine le tableau d’édition des agences. Noter que l’écran inclue la vérification des saisies utilisateur (tag <s:decorate>) :

<!DOCTYPE composition PUBLIC 
        "-//W3C//DTD XHTML 1.0 Transitional//EN" 
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
        xmlns:s="http://jboss.com/products/seam/taglib"
        xmlns:ui="http://java.sun.com/jsf/facelets"
        xmlns:f="http://java.sun.com/jsf/core"
        xmlns:h="http://java.sun.com/jsf/html"
        xmlns:rich="http://richfaces.org/rich"
        template="layout/template.xhtml">
 
<ui:define name="body">    
    <h:messages styleClass="message"/>
 
    <rich:panel>
      <f:facet name="header">Liste des agences</f:facet>
 
      <h:panelGrid columns="1">
        <rich:dataTable value="#{editAgencies.agencies}" 
            var="agency" height=”100px”>  
      
          <rich:column width=”200px”>
            <f:facet name="header">Nom</f:facet>
            <s:decorate>
              <h:inputText value="#{agency.name}" size=”40/>
            </s:decorate>
          </rich:column>
 
          <rich:column>
            <f:facet name="header">Num&#233;ro</f:facet>
            <s:decorate>
              <h:inputText value="#{agency.adresse.number}" />
            </s:decorate>
          </rich:column>
 
          <rich:column width=”400px”>
            <f:facet name="header">Rue</f:facet>
            <s:decorate>
              <h:inputText value="#{agency.adresse.street}" size=”80/>
            </s:decorate>
          </rich:column>
 
          <rich:column>
            <f:facet name="header">Code postal</f:facet>
            <s:decorate>
              <h:inputText value="#{agency.adresse.zipcode}" />
            </s:decorate>
          </rich:column>
 
          <rich:column>
            <f:facet name="header">action</f:facet>
            <h:commandButton value="del" 
                action="#{editAgencies.deleteAgency(agency)}" />
          </rich:column>
 
        </rich:dataTable>
 
        <h:commandButton value="Cr&#233;er une agence" 
            action="#{editAgencies.createAgency}"/>
      </h:panelGrid>
    </rich:panel>
  </ui:define> 
 
</ui:composition>

Vingt-quatre lignes – qui sont en plus très lisibles – réclament un effort d’écriture minime. Un véritable écran complexe nécessite 200-300 lignes. Cela représente, selon le niveau d’un développeur, et l’outillage à sa disposition, entre deux heures et deux journées de travail. Ce temps comprend l’écriture de la page web qui l’accompagne. Ainsi que l’effort de test.

Image:vertuArchiSimple0.jpg

Il est pourtant inlassablement répété que la généralisation des applications Web et l’utilisation des langages à objet – Java en particulier – n’à pas été probant du point de vue de la productivité. Et qu’il reste préférable d’utiliser PHP, Ruby ou revenir sur PowerBuilder ou Delphi pour développer rapidement une application simple. Pourquoi ?

A la recherche de la productivité perdue

Demandez à un architecte Java pourquoi l’utilisation de Java est elle moins efficace que PHP ou Ruby. Il vous répondra : parce que Java permet de faire des applications « propres », entendez : performantes, maintenables, évolutives. Et que, ma bonne Dame, mon bon Monsieur, cela se paie. Et cher. Mais la qualité, c’est bien connu, est un investissement rentable : ce qui est perdu aujourd’hui sera regagné demain. Largement.

Continuez : demandez-lui ce qu’il faut faire pour bien faire ? Il répondra N-tiers. Une couche MVC pour commencer animera l’écran. Elle sera composée d’une page JSP, d’un bean qui l’animera, et d’un bean qui portera et validera les données. Cet écran sera connecté à un service métier. Qui à son tour sera connecté à un DAO (Data Access Object : service spécialisé dans l’accès aux données). Ce DAO fera appel à la base de données sous-jacente, le plus souvent via un ORM (JPA, Hibernate, IBatis). Tout ce petit monde se tenant la main grâce à un ou plusieurs frameworks (Struts, EJB, Spring) et une configuration (ou plusieurs !) spécifiée dans un fichier XML.

Image:vertuArchiSimple1.jpg

Soit sept éléments (en tenant compte des entités), là où mon exemple donné en début d’article n’en nécessite que trois. La complexité est plus importante, le nombre de lignes nécessaires beaucoup plus grand (au moins deux fois) et bien sûr l’effort induit s’en ressent. Cet effort est-il vraiment utile ? Obtient-on réellement des applications plus performantes, plus maintenables et plus évolutives ? J’en doute. Et cet article –un peu polémique– est là pour vous faire part de ce doute.

Ok, ok, dans l’exemple Seam donné plus haut, l’architecture utilisée est 2 tiers et avec état (stateful). Et il est « bien connu » que le N tiers a des vertus que le 2 tiers n’a pas. Et qu’une architecture sans état est plus « scalable » (excusez l’anglicisme). Mais à quoi sert la « scalabilité » pour une application qui n’est utilisée que par quelques dizaines ou quelques centaines d’utilisateurs ? Il existe de grandes applications qui doivent être scalables. Mais il existe aussi une foule d’applications d’envergure plus modeste qui ne le nécessitent pas. Or, faire scalable coûte cher. Il faut savoir le justifier. Et non en faire un axiome.

Mon expérience personnelle m’amène à penser que le N tiers utilisé systématiquement n’est pas une solution pertinente (après des années d’évangélisation en faveur du N tiers pourtant). De meilleures performances, une plus grande maintenabilité et une meilleure évolutivité sont obtenues lorsque le code est plus simple. En résumé, la simplicité est aussi un facteur majeur de qualité. Et rendre un code plus complexe est une manière de le dégrader. Il faut donc peser le pour et le contre avant d’adopter une architecture. Aucun choix n’est évident.

L’enfer est pavé de bonnes intentions

Afin de ne pas parler de façon trop abstraite, je vais revenir sur l’utilisation de JPA ou d’Hibernate dans un environnement 2 tiers et N tiers. Cela illustrera parfaitement le fait que les effets induits par une architecture peuvent aller complètement à l’inverse des gains que l’on en attend. Les managers ainsi que les DAO sont là pour assurer une indépendance la plus complète entre l’IHM et le métier. Dans la pratique – pour par exemple obtenir la liste des clients – un écran (une action struts, un backing bean JSF, une panel Wicket) invoque la méthode « getClients() » du manager. Celui-ci n’ayant pas vraiment de valeur à ajouter, se contente de déléguer la requête à un ClientDAO muni lui aussi d’une méthode « getClients() ».

La méthode « getClients » de ClientDAO, avec ou sans l’aide d’un framework comme Spring ou EJB3 ouvre une session Hibernate ou un EntityManager JPA, effectue la requête et renvoie la liste des objets remontés depuis la base de données. Avant de conclure, notre DAO ferme la session Hibernate/l’entityManager afin de libérer les ressources mobilisées pour l’opération. Nos deux méthodes « getClients » sont maigres : entre cinq et huit lignes.

Mais notre écran, lui, aimerait bien afficher les données propres aux clients (nom, prénom) mais aussi des informations périphériques (liste des contrats). Et là, c’est le drame : ces objets ne sont pas présents dans la grappe retournée par le DAO via le manager. Comme la session Hibernate ou l’entity manager a été fermé, les objets clients remontent une exception.

Pour corriger le problème, nous décidons de complexifier la requête émise par le DAO afin qu’elle ramasse au passage les contrats de chaque client . Hibernate/JPA sait très bien faire ça. Il s’avère qu’un autre client aimerait lui aussi présenter la liste des clients. La méthode « getClients() » du manager lui semble tout indiquée. Il s’avère cependant que ce n’est pas la liste des contrats qui intéresse notre nouvel écran, mais la liste de ses contacts commerciaux. Il faut donc que notre requête Hibernate ramasse aussi la liste de ses contacts en sus des contrats.

Non seulement notre nouvelle requête est devenue significativement complexe, mais elle est devenue très peu performante (sans aller dans le détail : la base de données ramène le produit cartésien Contrat/Contact). Et il faut s’attendre à l’apparition d’un troisième écran faisant appel à notre méthode « getClients » avec encore un autre besoin (la liste des prestations effectuées par exemple). Il faut donc séparer les besoins. Notre DAO et donc notre manager aura une méthode « getClientsAvecContrats », « getClientsAvecContacts », « getClientsAvecPrestations », etc. Que c’est-il passé ? Le besoin IHM (afficher les informations relatives à un ensemble de clients avec ses contrats) s’est trouvé projeté sur la partie métier … L’indépendance recherchée est, en grande partie, perdue.

Continuons. Notre écran affichant le client et ses contrats évolue : dans sa nouvelle version, seuls les clients qui ont signé un contrat depuis moins d’un an sont concernés. Que fait le développeur de l’écran ? Modifie-t-il la méthode « getClientsAvecContrats » ? Eh bien non : cette méthode étant peut-être utilisée par un autre écran dont il n’a pas connaissance, il va créer une nouvelle méthode (dans le manager et le DAO) : « getClientsAvecContratDansLAnnee ». Et laisser une méthode morte « getClientsAvecContrats » qu’en fait personne d’autre n’utilisait … Ainsi, rapidement les DAO et managers sont encombrés d’un ensemble de méthodes très spécifiques, dont seule une partie est utilisée. Notre maîtrise du code de l’application s’est dégradée.

Si nous utilisons une architecture 2 tiers, aucun manager ou DAO n’est nécessaire. La gestion de la session Hibernate (ou de l’entity manager JPA) peut être confiée à l’écran. Celui-ci peut conserver cette session (en mode étendu) entre deux requêtes et la laisser ouverte. Les objets clients sauront se compléter tout seuls : la requête Hibernate restera simple. De plus, si le besoin d’un écran change, le développeur sera assuré que sa modification n’aura pas d’impact imprévu.

Il existera toujours des objets de services. Mais ils n’implémenteront que de véritables opérations métier comme la génération d’une facture liée à un contrat ou l’émission d’une alerte pour toutes les factures échues non payées. Ces services resteront indépendants de l’IHM, même s’ils sont invoqués par tel ou tel écran.

Conséquence : tous les avantages que l’on attendait du N tiers sont obtenus … en 2 tiers !

Adopter un sage pragmatisme

D’accord, je triche un peu :-) . La notion de service dont il est question dans le paragraphe précédant ressemble furieusement à du 3 tiers. Et c’est exact. Le trois tiers a des vertus : il permet en effet d’isoler le code purement métier et d’en faciliter l’écriture et la maintenance. Il permet ensuite que ce code puisse être invoqué en de multiples endroits. Il serait aberrant de négliger de tels avantages. Ce qui est dommage, c’est de vouloir appliquer cette architecture N tiers partout, y compris là où elle est mal adaptée : dans la manipulation de données à partir d’une IHM. Quand il s’agit d’amener des informations vers un écran, de permettre à un utilisateur de les enrichir avant de les renvoyer vers une base de données, le 2 tiers avec état est idéal. Et cela n’empêche en rien nos écrans 2 tiers d’appeler ponctuellement les véritables services métier.

Mais cela revient à faire cohabiter, voir à mixer 2 tiers et 3 tiers. J’entends déjà (j’ai déjà entendu pour être franc) les objections : c’est rompre avec l’orthogonalité de l’architecture, c’est y apporter la confusion. Je ne suis pas de cet avis : il est beaucoup plus simple de manipuler ensemble plusieurs architectures, chacune adaptée à une typologie de problèmes, que de mettre en œuvre la super solution répondant à tout, aux prix d’une complexité localement injustifiée.

Image:vertuArchiSimple2.jpg

Pour conclure, prenons une métaphore : le camion est probablement le seul véhicule permettant de résoudre à peu très tous nos problèmes de transport : pour un déménagement, pour transporter de grandes quantités de marchandises, le camion est nécessaire. Je sais qu’il est certes possible d’aller acheter son pain, d’emmener ses enfants à l’école ou de partir en vacances, en camion. Mais nous préférons tous utiliser plutôt la voiture pour ces besoins plus modestes. Nous adaptons le moyen utilisé au problème à résoudre. En termes d’architecture, il est sage de suivre aussi ce principe.

Boîte à outils
Dernière modification de cette page le 8 août 2008 à 11:27