jeudi 4 octobre 2012

Développement et conception : mon approche P(N)OO

Ces derniers temps, j’ai eu plusieurs occasions pour échanger au sujet de l’architecture du code, comment concevoir ses classes, ses objets, leurs liens, leurs interactions, leurs tests, etc … Il y a eu des échanges avec des développeurs qui ont leurs visions, qui s’interrogent, qui cherchent la bonne voie vers l’excellence. Et il y a eu des interventions auprès d’équipes qu’il m’a fallu guider, qui attendaient de moi une réponse, et avec qui j’ai partagé ma vision, plutôt pragmatique même si ce n’est pas la pure beauté du geste …

Je vous la propose aujourd’hui en espérant vos avis et réactions.



La Programmation Orientée Objet (POO) : c’est bien !

Depuis que ça existe, ou presque, je fais de la POO, Programmation Orientée Objet … du moins, je le pensais … Car, comme tous les développeurs actuellement (ou presque), j’utilise des objets, comme par exemple en Java. Prenons un exemple en imaginant le développement d’une application de vente dans laquelle il nous faudra un « panier » avec une liste de produits, une référence à un client, une date, un état, etc … Le début de l’objet Java représentant ce panier pourrait ressembler à ça :
public class Carts {
 
 private List<product> products;
 
 private Client client;
 
 private Date date;
 
 private Status status;
 
 ...
 
}

Nous allons poursuivre en enrichissant notre objet, selon la POO telle que je l’ai comprise…

Nous aurons besoin d’accéder à ses attributs, nous allons donc ajouter des accesseurs :
 public void setClient(Client client) {
  this.client = client;
 }

 public Date getDate() {
  return date;
 }

 public void setDate(Date date) {
  this.date = date;
 }

 public Status getStatus() {
  return status;
 }
 
 public void setStatus(Status status) {
  this.status = status;
 }
 
 public List<product> getProducts() {
  return products;
 }

Nous avons également besoin de modifier ses données par des méthodes utilitaires :
 public void addProduct(Product product) {
  products.add(product);
 }

 public void removeProduct(Product product) {
  products.remove(product);
 }

Dans le cycle de vie de la vente, il nous faudra le prix total à payer, nous ajoutons donc une méthode utilitaire :
 public float computeTotalPrice() {
  float total = 0;
  for (Product product : products) {
   total += product.getPrice();
  }
  return total;
 }

Pour des besoins de gestion, une fois la vente réalisée, nous devons stocker le panier dans une base de données, nous allons donc demander au panier de nous donner une requête d’insertion en base SQL :
 public String computeSqlInsertRequest() {
  return "insert into carts date=? clientId=? ....";
 }

A noter que cette requête ne sera pas forcément valable pour tous les SGBD (exemple classique : la date), il faudra certainement pouvoir demander des variantes.

Pour la partie vente en ligne sur internet, nous devons générer un mail avec un résumé du contenu, nous ajoutons donc une nouvelle méthode à notre objet « Panier » qui dispose de toutes les informations nécessaires :
 public String computeMailContent() {
  String content = "Bonjour,\nVotre panier composé le " + date
    + " comporte les éléments suivants :\n";
  for (Product product : products) {
   content += "- " + product.getName() + " au prix de "
     + product.getPrice() + "\n";
  }
  return content;
 }

OK, mais là c’est bon pour un mail en texte brut, mais il faut gérer les mails au format HTML …

Bon, je m’arrête là, mais vous me voyez venir : ma classe « Carts » commence à devenir fourre-tout, elle va vite devenir énorme, et regrouper tout et n’importe quoi. Tout ça pour répondre à différents besoins autour des données contenues dans un « panier ».

Alors, dans une récente discussion, un copain me répond : « pattern visiteur » !

Pattern visiteur : la solution ?

Le pattern visiteur est en effet une alternative à la dérive ci-dessus : il permet d’extraire certains traitements dans des objets séparés.

Mais d’après moi, ce pattern n’est pas la solution, pour la simple et bonne raison qu’il est trop compliqué, trop difficile à comprendre et à appréhender lorsqu’on relit du code. Je ne dis pas ça pour me défendre, car je l’avoue sans hésiter, j’ai beaucoup de mal avec ce pattern. Mais mes observations m’ont montré que bon nombre de développeurs ne le comprennent pas non plus. Je pense qu’il faut un état d’esprit particulier pour le maîtriser, et j’admire ceux pour qui ce pattern est une évidence.

Et je ne parle même pas des tests unitaires avec un tel pattern, je n’ose imaginer, surtout sans le maîtriser …
Toujours est-il qu’avec mon obsession d’une approche pragmatique, il me semble dangereux d’utiliser un pattern lorsqu’une grosse majorité de l’équipe ne le comprend pas. L’équipe doit produire du code maîtrisé par tous, et donc par chacun !

Et alors, comment fait-on ?

PNOO : Une « Programmation Non Orientée Objet » ?

Je ne sais pas comment l’appeler. Je ne sais même pas si cette approche aurait un nom. Je compte sur vous pour m’éclairer si vous avez des informations à ce sujet.

Toujours est-il que pour moi, la solution est de séparer :
  • Les objets contenant des données, qu’on peut appeler « beans », objets contenant uniquement des attributs, et éventuellement, des accesseurs. Sans détailler : si l’objet est interne à mon package, la classe est « package », les attributs aussi, pas besoin d’accesseurs ; à contrario, si je développe une librairie, alors mes beans sont plus « classiques », avec des accesseurs, et idéalement, ce sont des objets immutables
  • Les objets de traitement, sans attributs de données, uniquement des références aux collaborateurs et éventuellement, des données de réglages ou de contexte. Dans bon nombre de cas, ces « objets de traitements » peuvent être static

Cette approche a le mérite d’être simple, toujours comprise par tous, et facilite les tests unitaires, notamment leur apprentissage :
  • Les « beans » ne se testent pas, aucun intérêt, aucun ROI
  • Les objets de traitement sont faciles à tester, avec des beans de données en entrée (faciles à préparer) et des beans de données en sortie (faciles à vérifier), et les collaborateurs sont faciles à « mocker »

A noter, évidemment, qu’on va respecter le principe « une classe, un rôle » !

Voilà mon approche, celle que je pratique, mais également que je propose aux équipes avec lesquelles je travaille et qui ont apparemment toujours bien réussi à monter en compétences vers une architecture évolutive et donc agile, en passant bien sûr par l’apprentissage des tests unitaires, en TDD, ça va de soi, ou presque … ;-)

Je sais que cette approche ne plait pas aux « puristes » mais elle a le mérite d’être simple et pragmatique. Et vous ? Comment faites-vous ? Quelles sont vos pratiques d’équipe ?

J’attends donc vos avis, critiques ou adhésions, et commentaires ! Merci

29 commentaires:

  1. Ce commentaire a été supprimé par l'auteur.

    RépondreSupprimer
    Réponses
    1. Salut Raph !

      Merci pour ta réaction !

      La confusion est courante. Les EJB (Entreprise Java Beans) sont des objets qui hérite d'un framework imposant un cadre et des méthodes, pas forcément liées à la véritable représentation de l'objet.

      Lorsque je parle de "beans" ou "data beans", il ne s'agit pas d'EJB bien sûr !

      Pour répondre à ta question, les puristes fuient la 1ère solution, trop fourre-tout et vite illisible, et adopte vite des patterns, notamment le fameux "pattern visiteur" dont je parle. Donc je pense qu'ils sont plutôt sur la 2e solution (et moi la 3e) .... ;-)

      Merci

      Supprimer
    2. Ce commentaire a été supprimé par l'auteur.

      Supprimer
  2. Salut !
    Je suis pour les POJO, mais de moins en moins fan des NOJO ! http://puttingtheteaintoteam.blogspot.fr/2008/10/is-that-pojo-or-nojo.html
    J'aime bien l'approche Playframework 1, avec des POJO qui ont des méthodes all(), getByKey(), etc.
    Martin Fowler a écrit un truc sur les NOJO, mais je ne l'ai pas retrouvé... (il n'appelle pas ça comme ça).

    RépondreSupprimer
    Réponses
    1. Salut Thierry,

      Merci pour ton avis.

      Effectivement, ce que j'appelle "data beans" semblent correspondre à ces NOJO ...

      Moi aussi, j'aime bien l'approche de Play! 1 sur la partie @Entities, OK, c'est ... pratique. Mais les classes deviennent vite fourre-tout également, et il devient vite compliqué d'utiliser une de ces @Entity ailleurs dans le code.

      Et puis, notre code n'est pas fait que d'@Entities, nous avons d'autres traitements et objets ("internes"), donc ma réflexion porte essentiellement sur le reste justement.

      Supprimer
  3. En y réfléchissant, je m'aperçois qu'en Javascript, je fais des NOJO et que ça me plait bien. Mon commentaire précédent est donc à nuancer...

    RépondreSupprimer
  4. Salut Xavier (et Raph et Thierry :))
    Pour une fois je vais laisser un commentaire, j'aime bien ce sujet. D'autant plus que je suis 100% d'accord et que c'est le style de programmation que j'utilise depuis qques années déjà.
    Une version qui pousse l'idée un peu plus loin est l' "Entity-Component-System Programming".
    En résumé :
    - des Systèmes (avec le + possible de code en static) qui font les traitements en manipulant les Composants
    - des Composants qui sont l'équivalent de tes Beans ou des NOJO de Thierry
    - des Entités qui représentent tes 'objets' (1 panier ou 1 voiture ou ...). Chez moi une entité == un id (et j'associe des composants à une id, un peu comme un BDD)

    Voir http://t-machine.org/index.php/2007/11/11/entity-systems-are-the-future-of-mmog-development-part-2/ par ex ou http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/

    RépondreSupprimer
    Réponses
    1. Salut Pierre-Eric,

      Merci pour ta réaction !

      Oui, c'est l'idée. Par contre, concernant les méthodes (voire les classes) statiques, je suis bien adepte tant qu'il n'y a qu'une implémentation possible (type classes utilitaires) mais l'inconvénient que je vois de plus en plus est du côté des tests, pour "mocker" une telle classe.

      Certe, il y a PowerMock, mais en accompagnant des équipes relativement novices en TU, je m'aperçois que ça rajoute une complexité pour rédiger les TU, notamment à cause de la syntaxe, de la mauvaise lisibilité et, très important, de l'absence de visibilité, dans l'architecture, des liens entre classes (une classe en utilise une autre qui est "static", ça ne se voit pas de l'extérieur, contrairement à un collaborateur qu'on lui donne dans son constructeur).

      J'aurais donc tendance à ne pas en abuser, et à garder dans certains cas un système interface + impl.

      Supprimer
  5. À vrai dire je n'aime pas les Mocks :)
    Pour les tests unitaires je ne teste que les méthodes privées et/ou sans dépendance externe => hop aucun Mock à faire et uniquement des tests ultra-courts et rapides à éxecuter.

    Pour les méthodes publiques/avec dépendance, je préfére des tests fonctionnels (toujours sans Mock, à part peut-être pour les entrées/sorties ; cf FitNesse) ou pas de tests du tout.
    Ça pousse à simplifier ces méthodes et à les découper en méthodes static/privées qui ne travaillent qu'avec les arguments qu'elles recoivent, qui ne modifient pas d'état cachés, etc => plus faciles à tester.
    Ainsi, le code restant non-testé unitairement est très simple et les bugs également (NPE parce qu'on a oublié de lui passer un collaborateur, etc)

    RépondreSupprimer
    Réponses
    1. Je comprends ... mais je ne te rejoins pas sur ce point ;-)

      Pour moi, dans une architecture rigoureuse "1 classe = 1 rôle", on découpe en plusieurs classes qui interagissent entre elles. On se retrouve avec 2 types de classes de traitement :
      - les classes "feuilles" qui font du traitement "pur" et n'ont pas de collaborateurs : TU faciles !
      - les classes "intermédiaires" qui ont finalement surtout un rôle "d'aiguillage" et là, les mocks sont indispensables

      A aider les développeurs dans l'apprentissage des TU, je m'aperçois qu'il y a des réticences à cette architecture très découpée (question d'habitude en fait), et qu'il peut y avoir une crainte des mocks (souvent par manque de maîtrise) : n'ayez pas peur des mocks ! ;-)

      En plus, bien utilisés et bien écrits, les mocks simplifie énormément les tests.

      Quant aux tests fonctionnels, il en faut, ils sont complémentaires (tout comme les tests intermédiaires d'intégration), mais ils sont souvent plus lourds à écrire et donc à maintenir, et ne peuvent pas couvrir les cas aux limites : attention au ROI, à utiliser avec modération je pense ...

      Supprimer
    2. Ancien fan des tests fonctionnels, j'en suis un peu revenu - tout au moins pour ce qui est des tests automatisés (les specs executables, je suis toujours pour !).
      Je suis d'accord avec la pyramide des tests de Mike Cohn, qu'on trouve notamment ici : http://www.cftl.fr/index.php?id=71
      J'aime beaucoup "Doing the software right" & "Doing the right software".

      Supprimer
    3. Xavier : effectivement j'ai peur des Mocks :) Mais je persiste à penser qu'il vaut mieux réorganiser le code pour pouvoir le tester simplement (sans mock) plutôt que de complexifier les tests. Pour les classes d'aiguillage par ex, on pourrait envisager d'extraire la logique dans des sous-fonctions sans dépendances et tester uniquement celles-ci.

      Thierry : je n'utilise peut-être pas le bon vocabulaire. Pour moi du test fonctionnel, c'est du test automatisé basé sur des specs (toujours l'exemple de FitNesse). Du coup je ne vois pas quel aspect des TF te gêne ?

      Supprimer
    4. Les tests Fitnesse pour aider à la communication entre les spécifieurs et les développeurs : je vote pour !
      Les tests Fitnesse dans une chaine de test automatisée destinée à limiter les bugs : bof. Je préfère des tests unitaires.

      PS : pour les Mocks, je suis comme toi, tant que je peux m'en passer je suis content. Mais quand ils sont utiles, why not.

      Supprimer
    5. Disons que le risque avec les Mocks quand on n'a pas trop l'habitude de s'en servir, c'est de passer plus de temps à débogger le TU que la classe elle-même. Maintenant, j'ai quand même bien du mal à imaginer un code correctement testé sans Mocks, surtout lorsqu'on pratique le TDD, puisque dans ce cas, l'architecture découle directement des TUs, et la granularité accrue de l'architecture fait que les "classes d'aiguillage" représentent alors une quantité non négligeable de code.

      En fait, il faut trouver le bon niveau de granularité de l'architecture, et j'avoue que sur ce point, je ne suis plus tout à fait en phase avec le "1 rôle = 1 classe". Déjà, ça ne marche qu'en Java, mais surtout, du fait du très grand nombre de classes qui en découle, la réutilisabilité des composants n'est finalement pas si bonne que ça. J'aurais donc plutôt tendance à dire qu'un code est correctement découpé tant qu'il est facile à tester, peu importe ce qu'il y a dedans. Au fond, si les TUs sont simples à lire et à écrire (et à modifier !), on s'en fou un peu qu'il y ait 1 ou 10 rôles par classe, non ?

      Après, tester les méthodes privées pour éviter les Mocks, j'ai personnellement bien du mal à trouver ça plus simple. Au niveau maintenance, chaque fois que je suis tombé sur ce type de code, c'était un vrai cauchemar à refactorer (cf http://bruno-orsier.developpez.com/anti-patterns/james-carr/#LII-E). Pour moi, un TU doit tester le contrat public du composant. D'une part, c'est signe d'un bon découplage du composant par rapport au monde extérieur, mais en plus, ça documente son usage à moindre coût (cf les doctests de Python).

      Supprimer
  6. Effectivement, on voit bien que tu n'es pas très à l'aise avec le pattern visiteur, car ta solution n'est pas une alternative à l'utilisation du pattern visiteur, ni l'inverse d'ailleurs, bref ça n'a rien à voir.

    Le pattern visiteur permet de déporter les traitements polymorphiques d'un objet. Dans ton exemple, il n'y a pas de traitements polymorphiques, donc aucun intérêt à utiliser le pattern visiteur, sauf à compliquer inutilement le code bien sûr. Imaginons que tu veuilles surcharger la méthode computeMailContent dans une sous-classe de Carts, mais ceci sans changer ton abstraction, qui ne manipulerait donc que des Carts. Comment comptes-tu t'y prendre ? On est là en présence d'un traitement polymorphique, et ta solution ne suffit plus, car il faut pouvoir appliquer le bon traitement en fonction du type de l'objet.

    Alors, comment faire ? 3 solutions :

    1) laisser les traitements polymorphiques dans l'objet, mais ce n'est pas idéal, pour toutes les raisons que tu as cité;

    2) déporter les traitements en s'appuyant sur des instanceof pour détecter le type réel de l'objet. C'est la pire des solutions, car sous son air de fausse simplicité, on ne sait jamais vraiment si on a bien implémenté tous les traitements polymorphiques de l'objet, en tout cas pas avant d'avoir retesté tout le produit. De plus, dans la plupart des langages, cette solution requiert un overhead qui la rend aussi moins performante que les 2 autres solutions;

    3) déporter les traitements en s'appuyant sur le pattern visiteur.

    La solution (2) n'a pour moi aucun intérêt, sauf à écrire du code inmaintenable et à aimer corriger les bugs de régression. Reste donc à choisir entre la solution (1) et la solution (3). Là, je dirais qu'il faut faire appel au bon sens. Inutile de sortir le pattern visiteur s'il n'y a qu'un seul traitement polymorphique simple à implémenter. Ce serait comme prendre un bazooka pour écraser une mouche ! Mais s'il y a beaucoup de traitements, ou s'ils sont trop complexes pour s'appuyer sur le seul objet (besoin d'un traceur, de collaborateurs, etc), alors le pattern visiteur est tout indiqué et constitue une réponse à la fois propre et élégante à ce problème.

    Et concernant le fait de ne pas être à l'aise avec le pattern visiteur, j'ai envie de dire que ça vient à force de pratique et de persévérance. J'imagine que lorsqu'un développeur te dis "c'est trop compliqué de faire des TUs", tu ne lui réponds pas "ok ben laisse tomber alors", n'est-ce pas ? ;-)

    Un dernier mot sur le fait de ne pas faire que de la POO. En fait, il n'y a qu'en Java où l'on croit qu'il faut obligatoirement faire de la POO pour faire du développement logiciel. La plupart des autres langages permettent la programmation multi-paradigme, c'est-à-dire qu'on peut choisir le paradigme le mieux adapté à la situation, voire même mixer plusieurs paradigmes en même temps. Je ne vois donc pas en quoi cette approche déplairait aux "puristes", sauf peut-être aux extrémistes de la POO, d'autant qu'elle est très largement utilisée dans certaines bibliothèques de référence (par exemple, la STL de C++).

    RépondreSupprimer
  7. Salut Xavier,

    Tu poses effectivement une question essentielle qui n'est pas simple à répondre.

    Néanmoins, placer le débat entre les puristes et les non-puristes n'est à mon avis pas la bonne approche.

    Il vaut mieux se poser les questions suivantes :
    1. Quelle est la (les) meilleure(s) solutions au problème et pourquoi, indépendamment du fait si on maîtrise ou non cette (ces) solution(s). Est-ce le pattern visiteur, la PNOO, autres choses ? Il ne faut pas hésiter à discuter avec plusieurs personnes qui connaissent des langages, des approches différentes. Si vous utilisez un langage objet, il n'est pas inutile de passer du temps à comprendre pourquoi ce paradigme est apparu et ce qu'il apporte ou non par rapport à d'autres ;

    2. Une fois avoir répondu à la question 1, se pose la question du terrain. Est-ce que je suis capable de mettre cette solution en place (suivant ma maîtrise ou non, du temps nécessaire...) ? Si ce n'est pas le cas par exemple pattern visiteur ou autres, alors il vaut mieux se rabattre sur ce qu'on maîtrise PNOO ou autres. Par contre, il faut aussi se donner les moyens de s'entraîner sur la ou les solutions retenues lors de la question 1 afin que la fois suivante tu puisses la (les) mettre en place.

    Rémy

    N.B. 1 : je ne vois pas le problème des tests unitaires avec le pattern visiteur. Au contraire, cela est généralement plus simple car il sépare bien les responsabilités entre le parcours de l'arbre et le traitement.

    N.B. 2 : je rejoins Olivier Kozak sur le fait qu'on ne peut pas dire qu'une solution n'est pas bonne juste parce qu'elle paraît compliquée. Son exemple sur le TDD est assez bon.

    N.B. 3 : @Olivier Kozak, il existe maintenant plusieurs librairies pour faire de la programmation fonctionnelle. La communauté Java a pas mal évoluée sur ce point et je ne pense pas qu'elle imagine qu'il faut faire que de la POO pour faire du développement logiciel.

    RépondreSupprimer
    Réponses
    1. Oui ma remarque est quelque peu caricaturale. Je sais bien que tous les développeurs Java ne sont pas des extrémistes de la POO ;-)

      Cela dit, je me souviens d'une discussion qui m'avait marquée sur la revue d'une fonction C++ d'une bibliothèque utilisée par des développeurs Java. Au final, après avoir expliqué en vain que la POO n'avait aucun intérêt dans ce cas, je me suis quand même résigné à mettre cette fonction dans une classe. Quand Xavier parle des "puristes" en opposant sa solution à de la POO "pure", je retrouve un peu cette idée selon laquelle la POO serait le seul paradigme valable. Or, dans les autres langages que j'utilise (Python et C++), cette idée est déjà plus nuancée, la POO n'étant jamais qu'un paradigme parmi d'autres.

      Supprimer
  8. Je suis assez d'accord avec le caractère optionnel de l'approche "objet = données + méthodes".
    Dans le produit sur lequel je travaille, on a un peu l'équivalent des "beans" - sauf que l'on appelle ça "modèle du domaine" et que ce modèle est défini à la base par des interfaces pour des raisons de polymorphisme (pour la même donnée, certaines implémentations font du stockage simple, d'autres ont une petite logique intégrée qui utilise les autres données de l'objet)
    A côté des ce "modèle du domaine", on a des composants qui font du traitement (transformation d'objets de données, entrées sorties, etc.) avec, la plupart du temps, des références à des collaborateurs pour seuls membres de classe.

    Toutefois, je crains que l'approche "beans + traitements" présentée dans le billet ne "passe pas à l'échelle". Elle va très certainement fonctionner pour un logiciel monolithique de taille relativement modeste mais dès que le logiciel grossit, on est obligé d'introduire un groupement en "packages" (cf l'article "Granularity" d'uncle Bob)
    Et la ça coince car il faut introduire des contrats entre les packages et une approche trop simple ne peut pas intégrer cette notion, tout en respectant le "Dependency Inversion Principle" (= des classes de traitement faiblement couplées) et le "Common Reuse Principle" (= des classes de traitement groupées par usage).

    Donc, chez nous, les classes de traitement ne sont pas statiques :
    - chacune d'entre elles implémente une interface dite de "comportement" (behavior).
    - ces interfaces de comportement sont standardisées pour maximiser la cohérence entre la multitude de traitements existants.
    - les classes de traitement sont regroupées en packages en respectant le "Common Closure Principle" (= des traitements indépendants sont dans des packages indépendants)
    - chaque package implémente une fabrique qui, pour un comportement demandé, va renvoyer la classe de traitement qui l'implémente
    Du coup, les interfaces de comportement matérialisent les contrats entre les packages et chaque traitement est identifié par le couple (package,comportement).

    Si ça intéresse quelqu'un d'en savoir un peu plus sur ces notions de packages et leurs implications au niveau de l'architecture logicielle et des tests, j'ai écrit une série de billets sur le sujet : http://agilitateur.azeau.com/post/2012/03/05/Conception-logicielle

    RépondreSupprimer
  9. Bonjour à tous,

    Et merci pour tous ces commentaires, nous avons battu le nombre de commentaires par article sur mon blog ! ;-)

    Je vais essayer de réagir point par point dans l'ordre, pas facile ...

    @Pierre-Eric : pour moi, les bons tests unitaires sont des tests de "classes" et non de "méthodes" : on teste la classe dans son ensemble, fonctionnellement, et donc forcément par ses classes publiques qui représentent son contrat. Les tests unitaires "par méthodes" sont pervers, et donc la solution de mettre les méthodes "protected", ou pire, "public" pour y accéder depuis les tests est un bad smell ...

    @Olivier : du coup, dans cette logique, le principe "1 classe - 1 role" est cohérent, et par rapport à ta remarque sur des tests simples (je suis d'accord, il faut qu'ils soient simples), par expérience, plus tu t'écartes de ce principe et plus les tests sont compliqués et nombreux (pour une même classe). Ce principe clarifie vraiment le code et sa relecture, à condition d'avoir de bons nommages pour toutes les classes "collaboratrices" et leurs méthodes

    @Thierry : j'ai beau avoir exploré différentes pistes, j'en reviens toujours à la pyramide des tests de Mike Cohn, nous sommes donc en phase. Par contre, les tests fonctionnels, de haut niveau, "de bout en bout", via l'IHM et automatisés ne sont pas à proscrire, notamment dans une chaîne d'intégration continue, et notamment pour limiter les régressions : ces tests sont complémentaires et ont leurs apports. Par contre, je trouve que leur ROI est faible (gros efforts de mise en place et de maintenance), donc il faut en limiter l'usage à des vérifications de cohérence d'ensemble en fonctionnement nominale de l'application, les tests unitaires se chargeant des cas aux limites. On ne limite donc le nombre, on rejoint la pyramide de Mike Cohn. Et pour compléter, il y a des situations où la pyramide pourrait s'inverser : j'ai notamment rencontré ce cas là dans une équipe dont l'application fait surtout du lien IHM-BDD donc peu de logique métier dans le code : dans un tel cas, les tests unitaires ont moins d'intérêts et les tests fonctionnels en gagnent

    (suite dans commentaire suivant)

    RépondreSupprimer
    Réponses
    1. (suite)

      @Olivier : tu as raison, c'est pas parce que c'est compliqué que c'est une mauvaise solution ou qu'il faut écarter cette solution, ce n'est pas ce que je voulais dire. D'ailleurs, ma pratique actuelle des tests unitaires et du TDD sont bien le résultat de persévérance, personnelle et collective, notamment avec un certain ... OKO ;-) . Par contre, je suis de nature pragmatique, et une bonne solution est une solution que l'équipe, dans son ensemble, va non seulement comprendre, mais maîtriser et être capable de mettre en oeuvre, et mon observation est que le pattern visiteur ne va pas dans ce sens (et c'est peut-être dommage, mais c'est ainsi ...). Maintenant, OK, il faut faire monter les développeurs en compétence, et là 2 remarques : je suis mal placé pour pousser ce pattern (pas assez de maîtrise) et il y a tellement à faire avant ...

      @sanlaville : je savais que je n'aurai pas du utiliser le terme de "puriste" ... ;-) ... Je voulais juste insister que le fait qu'il y a des solutions, voir des voies entières de conception qui sont "belles", voir qui sont "bien", sincèrement, mais je ne me sens pas avoir ce niveau de "puriste" (ce n'est pas péjoratif), je suis peut-être trop pragmatique ....

      @Olivier : polymorphisme : je ne fais plus d'héritage, plus du tout, pour moi c'est une mauvaise conception (en tout cas en Java, le langage que je maîtrise le mieux actuellement), et je remplace cet héritage par de l’agrégation (et il me semble que c'est encore un certain OKO qui m'a fait prendre conscience de cela, non ?). Par contre, il y a de l'héritage dans la conception des modèles de données de Play! et c'est pratique (save(), update(), ...) mais c'est marginal

      @Oaz : l'approche que je pratique me semble valable quelque soit la taille du logiciel, mais effectivement, c'est du "bas niveau". En se mettant à un niveau supérieur de l'architecture, cette vision disparaît dans des "boites" ou "composants" ou encore "services", qui pour moi correspondent à un "package" en Java voir à un service entier. Et là, l'important est le contrat de cette boite, présenté par exemple sous forme d'interface (et peu importe ce qu'il y a dans la boite, on ne le voit plus à ce niveau-là). C'est peut-être ce qu'il y a dans tes articles que je n'ai pas encore lu, merci pour le lien.

      Supprimer
    2. Je suis totalement d'accord avec toi sur le fait qu'il vaut mieux une architecture moins optimale mais comprise par toute l'équipe que l'inverse. Mais je pense aussi qu'on ne peut pas faire l'impasse sur les patterns du GOF et que c'est une compétence que tout développeur devrait se donner comme objectif d'acquérir à terme. Le pattern visiteur est un excellent pattern, mais comme tous les patterns du GOF, il faut savoir s'en servir à bon escient, ce qui n'était clairement pas le cas dans ton exemple.

      Et concernant le polymorphisme et l'héritage, c'est exactement le même combat. Non ce ne sont pas des mauvaises pratiques. C'est juste que pendant des années, ils ont été utilisés à l'extrême pour résoudre des problèmes pour lesquels ils n'étaient pas la meilleure solution, mais ça ne veut pas dire non plus qu'ils n'ont plus d'utilité du tout aujourd'hui. Bref, attention de ne pas basculer d'un extrême à l'autre...

      Supprimer
  10. Sujet très intéressant!

    Finalement l'approche PNOO "non objet" c'est ce que Spring, JPA and co. nous ont un peu forcé à faire toutes ces dernières années.
    Des beans vides de toute intelligence (mais plein de getters/setters merci la verbosité de Java) et des services "techniques" qui contiennent toute la logique métier.
    Ce qui est marrant c'est que beaucoup de "pro objet" ont souvent été en même temps pro spring et pro "archi en couche" alors que ça mène souvent à une incompatibilité de concepts.

    Je remercie Play Framework pour avoir montré une alternative aux développeurs Java en supprimant cette notion de service et en recentrant la logique autour du modèle "métier", approche que je préfère au final même si ça demande à revoir un peu la manière de tester et mocker les choses.

    Loic

    RépondreSupprimer
    Réponses
    1. Salut Loïc,

      Et merci pour ton retour.

      Effectivement, tu as raison et je n'y avais pas pensé, c'est exactement dans la lignée de ces frameworks récents ... marrant ...

      Pour Play!, voir cet article très intéressant de Philippe et voir mon commentaire sur Play! V1 vs V2 (faudrait que je fasse un post à ce sujet dès que j'aurai le temps) :
      http://k33g.github.com/2012/10/05/NOMORESCALA.html#comment-672268932

      Supprimer
  11. Oui j'ai vu ton commentaire c'est un gros sujet de discussion en ce moment...personnelement je commence à vraiment apprécier play2 notamment grâce à ses API asynchrones en Scala... mais le ticket d'entrée est plus gros qu'avec play 1 effectivement.

    RépondreSupprimer
    Réponses
    1. Justement, je trouve que c'est un des problèmes : pour apporter toute les possibilités et la puissance de la scalabilité, dont on a pas forcément besoin, le reste c'est "dé-simplifié" ... Si tu as des liens vers des discussions à ce sujet, je suis preneur !

      Supprimer
  12. Salut Xavier,

    C'est dommage que tu n'ait pas pu assister au dojo. J'ai mis un peu de code et mes réactions ici: http://martinsson-johan.blogspot.fr/2012/10/pattern-visitor-ne-baissons-pas-les-bras.html

    En résumé bien que le visitor n'est pas la solution miracle je ne pense pas qu'il faille se résigner. Surtout pas quelqu'un aussi dévoué au métier de l'informatique que toi.

    A bientôt j'espère.

    RépondreSupprimer
    Réponses
    1. Salut Johan,

      Ce fût un honneur pour moi que cet article vous ait inspiré pour une séance de coding-dojo, et je regrette vraiment de ne pas avoir pu y assister.

      Je pense que le pattern Visitor a de bonnes qualités, mais pour moi, une "bonne" solution est une solution comprise, voire maitrisée, par toute l'équipe : peut-être faut-il oeuvrer pour cela ? ...

      La suite sur ton blog.

      Supprimer
  13. Salut Xavier,
    J'ai retrouvé (par hasard) l'article de Martin Fowler au sujet des POJO 'vides' (les NOJO) : http://www.martinfowler.com/bliki/AnemicDomainModel.html

    RépondreSupprimer