mardi 16 octobre 2012

Pattern Visitor, ne baissons pas les bras


Suite au post très intéressant de Xavier Nopre sur le Single Responsibility Principle et son approche beans+services, Nicolas Capponi a animé une séance du dojo du club agile pour explorer différents solutions qui s'offrent à nous pour ce problème.

Personnellement j'ai choisi d'utiliser le temps pour m'affuter en Visitor, car comme dis Xavier ce n'est pas le pattern le plus facile. D'autres personnes ont essayé d'autres solutions. 

Voici deux variantes du Visitor pattern. L'un qui expose les objets métier (et donc utilise des getters) l'autre qui fait passer des primitives pour résoudre ce problème (pas forcément mieux, juste différent). 

Extrait de la solution avec objets métier:
// original code https://gist.github.com/3879131
// and blog post by Xavier Nopre
// http://xnopre.blogspot.fr/2012/10/developpement-et-conception-mon.html
// 2 whole solutions can be found at https://github.com/martinsson/Dojo61-SR,
// tags: visitorWithRealObjects, visitorWithoutGetters
public class Cart {
//...
public void accept(CartVisitor visitor) {
for (Product product : products) {
product.accept(visitor);
}
visitor.visit(this);
}
}
view raw Cart.java hosted with ❤ by GitHub
class Mailer implements CartVisitor {
private String productContent = "";
private String cartContent;
public void visit(Product product) {
productContent += "- " + product.getName() + " au prix de "
+ product.getPrice() + "\n";
}
public void visit(Cart cart) {
cartContent = "Bonjour,\nVotre panier compose le " + cart.creationDate
+ " comporte les elements suivants :\n";
}
public String computeMailContent(Cart cart) {
cart.accept(this);
return cartContent + productContent;
}
}
view raw Mailer.java hosted with ❤ by GitHub


Les autres solutions explorés, dont j'aimerais bien voir le résultat, était délégation à une autre classe ou introduction d'une petite interface pour gérer le cas d'utilisation en question (par exemple Mailer pour contenir computeMailContent()). Ces deux solutions me semblent être des bonnes solutions temporaires en attendant de les extraire pour de bon. Par contre une fois extraite il me semble qu'on va tomber sur soit le visitor soit la solution que mentionne Xavier: beans+services. Non?

Conclusion
Les visitor qu'on trouve ici, est-ce vraiment trop complexe? Personnellement je pense que nous gagnerions à simplement pratiquer ce pattern pour savoir le mettre en oeuvre avec aise quand cela est préférable. Certes il faut le faire plusieurs fois pour avoir l'impression de maitriser, mais nous sommes nombreux  à être informaticiens plus de 10 ans, alors qu'il suffit avec ~10h de pratique étalé sur quelques mois pour le maitriser en profondeur. Qu'en pensez-vous?

mardi 24 janvier 2012

Tester entrées et sorties en Clojure

En faisant ce kata en Clojure j'étais sous pression de temps alors je n'ai pas fait une partie en TDD - l'interaction avec la console. Ca ne me dérange pas plus que ça mais je voulais voir comment on pourrait le faire en TDD. Il se trouve que c'est extrêmement simple!

Dans cet exercise on doit construire une caisse de primeur qui accepte en entrée (console) des fruits et qui pour chaque entrée affiche le total sur la sortie (console). Pour cela je vais construire une fonction buy  qui va s'appuyer sur la fonction read-line qui lit une ligne du console et println qui affiche une ligne sur la console

Premier besoin : je ne veux pas de boucle fini dans ma fonction - quand j'entre une ligne vide la fonction sort. La syntaxe ici vient de Midje - un framework de test qui a la particularité (pour un langage fonctionnel) d'être orienté outside-in.
(facts "it exits when we enter an empty line"
(buy) => anything ;; il n'est pas important ce que renvoie la fonction buy
(provided
(read-line) => "") ;; étant donnée que read-line renvoie chaîne vide
;; // fin du premier test
(buy) => anything
(provided
(read-line) =streams=> ["b" ""])) ;; read-line renvoie d'abord "b" puis chaîne vide

Ici j'aime vraiment la facilité avec laquelle on arrive à faire un bouchon de read-line - on déclare simplement que ceci est un fait (fact) si read-line renvoie les valeurs énumérés

Deuxième besoin : On doit afficher le total de l'achat après chaque ligne d'entrée.
(fact "it prints the total for every line of input"
(buy) => anything
(provided
(read-line) =streams=> ["a" "c" ""]
(println "total: " 100) => anything :times 1 ;; after adding the "a" (apple)
;; that costs 100, println should
;; be called with "total: 100"
(println "total: " 175) => anything :times 1)) ;; after adding "c" (cherries)
;; that cost 75, println should
;; be called with "total: 175"

Ici c'est facile, court et ça reste lisible mais il y a du bruit :

  • on est obligé de déclarer la valeur de retour attendue de println (qui renvoie toujours nil), j'ai mis anything pour signifier que ce n'est pas important
  • chaque contrainte sur println (d'abord total 100 puis total 175) se déclare sur une ligne. Nous n'avons pas l'expressitivité de du bouchon avec =streams=>.  Dans mes rêves ça aurait été  
(fact "it prints the total for every line of input"
(buy) => anything ;; toujous sans importance
(provided
(read-line) =streams=> ["a" "c" ""])
(side-effects
(println) =receives=> [["total: " 100]
["total: " 175]]))

Et sinon le code ça rend quoi?
; just enter an empty line and you're ready for the next customer
(defn buy []
(loop [items []]
(let [item (read-line) ;; reads from console
basket (concat (csv-to-col item) items) ;; add item (can be several items separated by comma) to existing items
total (apply basket-price basket)] ;; total basket price
(if-not (empty? item)
(do ;; only one instruction can follow if-not,
(println "total: " total) ;; and since we need to both print the total
(recur basket)))))) ;; and recur back to the loop point we wrap
;; in a do statement
view raw buy.clj hosted with ❤ by GitHub

Ce à quoi je m'attendais pas en commençant cette exercise c'est que bien qu'on gère de la mutabilité (le prix total du panier s'incrémente) aucune fonction n'est mutable et aucun variable mutable n'est nécessaire. L'état du panier est contenu dans la recursion

Pour moi c'est fascinant de découvrir ce moyen de gérer l'état visible d'une application sans mutabilité.