Avant de rentrer dans le vif du sujet en présentant un exemple d'utilisation des traits s'appuyant sur l'adaptateur d'Atoum, j'ai fais un bref retour sur l'aspect théorique pour ensuite enchaîner sur leur impact sur les performances, la façon de les tester, et présenter quelques inconvénients.
Ma conférence s'est bien évidemment clôturée par la traditionnelle série de questions, qui a quelques peu dérivée, pour mon plus grand plaisir je dois l'avouer, sur le domaine des tests unitaires.
L'AFUP a ensuite offert une collation aux quelques 35 participants, au cours de laquelle des discussions plus informelles ont pu avoir lieu.
Et paradoxalement, si j'ai trouvé mon rythme un peu trop rapide et ma conférence insuffisamment travaillé, j'ai eu d'excellents retours au cours de ces conversations.
Il faut dire que j'ai eu beaucoup de mal à trouver un cas pratique de mise en application des traits, car par habitude, j'essaye lorsque je code de tenir compte des limitations du modèle objet de PHP.
Je fais donc en sorte de ne pas rencontrer le genre de problème que les traits permettent de régler.
Je pense donc que mon ressentie négatif provient en grande partie de cette difficulté que j'ai rencontré pour trouver un exemple acceptable de mise en œuvre des traits dans un cas réel.
Je m'estime donc au final relativement satisfait de ma performance
, même si j'essayerais de revoir quelques points si j'avais à la refaire à l'avenir.
Dans tous les cas, ce rendez-vous a été une excellente initiative de l'AFUP et j'espère qu'elle fera en sorte de renouveler l'exercice à l'avenir, surtout si elle parvient à faire venir des intervenants du niveau de Stephan.
J'ai d'ailleurs discuté beaucoup avec ce dernier, aussi bien des traits en particulier que de PHP en général, et il m'a confirmé qu'il n'est pas entièrement satisfait de l'implémentation actuelle, notamment au niveau de l'introspection et de la gestion des propriétés.
En conséquence, il est très possible qu'il y ait quelques ajustement à effectuer sur l'implémentation actuellement présente dans le trunk, peut être avant même la sortie de la prochaine version.
Il m'a également dit qu'il avait le même ressenti au niveau de la communauté des développeurs du langage.
En effet, tout comme moi, il la trouve relativement hermétique et relativement fermée aux idées nouvelles, même s'il estime être chanceux puisque les traits ont été acceptés sans aucun problème par les membres du PHP Group.
Bref, j'ai passé une excellente soirée en compagnie d'amis et de personnes de qualité, et j'en redemande.
Et pour ceux qui n'ont pas pu faire le déplacement, mon support de conférence est bien évidemment disponible.
19 réactions
1 De Cyrano - 17/12/2010, 07:00
Je peux te confirmer que c'était une excellente conférence, et le rythme était très bien. Merci pour les slides, avec ses nombreux exemples de code, ce sera d'autant plus facile pour appréhender le sujet clairement. Vivement la sortie de la version de PHP qui l'implémentera
2 De Eric - 17/12/2010, 07:33
Bonjour,
Je ne suis pas très convaincu par l'exemple d'utilisation : il s'agit juste
d'un acesseur et d'un mutateur sur une variable. Il me semble que d'autres
langages ont des solutions plus élégantes pour traiter ce cas ?
Mais la plus grosse question qui me vient est :
Quel est l'avantage du trait par rapport à une simple injection d'objet ?
(i.e : J'ai un objet à la place de mon trait qui est passé a mon objet via le constructeur ou un mutateur)
3 De mageekguy - 17/12/2010, 08:49
@Eric : Pour les solutions plus élégantes issues d'autres langages, je suis preneur d'exemples, et je suis certain que Stephan Marr également.
Et si tu connais un autre moyen qu'un trait pour éviter de dupliquer le code des méthodes
getAdatper()
etsetAdapter()
dans les classes qui en ont besoin, et qui n'ont pas forcément de relation entre elle, je suis également preneur.Et l'injection de dépendance n'est pas du tout la même chose qu'un trait, même si les deux techniques/concepts sont totalement complémentaires.
La preuve en est que dans le cas d'exemple de la conférence, il ne s'agit en effet que d'un accesseur et d'un mutateur, mais ils permettent de faire l'injection de dépendance de l'adaptateur au niveau du constructeur.
Une injection de dépendance ne te dispense pas de dupliquer le code permettant de la réaliser dans toutes les classes qui la nécessite pour une même classe.
Le trait, lui, te permet d'éviter cette duplication de code, en centralisant le code nécessaire à cette injection dans un unique fichier qui sera copié/collé dans les classes qui en ont besoin.
En gros, un trait, c'est la possibilité de faire un require_once() dans une classe et en dehors d'une méthode, et lors de la compilation et non du runtime.
4 De MathRobin - 17/12/2010, 09:39
Je confirme aussi que le rythme n'était pas si soutenu que ça puisque finalement, il n'y a eu aucune question du type "j'ai rien compris", c'est donc qu'on a réussi à suivre
L'exemple de l'adapter était d'ailleurs très bien choisi, c'est un cas pratique qu'on rencontre tous un jour ou l'autre, que ce soit vers un fichier, une base ou autre. Je pense qu'on sait tous à quel point ça peut être barbant et peu maintenable de recopier ce code.
Désolé de n'avoir pu rester pour discuter avec toi et les autres de tout ça.
(pour la petite blague, certes, j'ai pas de soucis de dictionnaire avec Android, mais j'avais quasi pas de réseau, contrairement à toi qui avais l'air de t'en sortir pas trop mal)
5 De Eric - 17/12/2010, 09:41
-> En C#
public string Name
{
}
Ok, donc je cherche un exemple qui :
- ne soit pas un couple mutateur/accesseur sans traitement.
- dont l'implémentation par un trait est meilleur ( + lisible ou +performant ou +simple ou...) que l'injection de dépendance.
6 De Guile - 17/12/2010, 09:57
J'aurais eu une question sur les traits (je n'ai pas eu droit à ma place) : je ne vois pas l'intérêt d'écrire explicitement la partie "insteadof". Pourquoi est-il nécessaire de dire qu'on choisit une méthode de traits au lieu de toutes les autres? Si je dis "je choisis la bille bleue", je ne me vois pas rajouter "au lieu de prendre la rouge, la verte, la jaune, la noire, ..."
En fait, je trouve que la partie "insteadof" est ultra laborieuse par rapport à l'intérêt que j'en vois dans l'immédiat. Donc j'aimerais connaître les raisons de cette spécification bien particulière (il doit y en avoir!).
Merci pour la réponse
7 De mageekguy - 17/12/2010, 11:29
@Eric : Encore une fois, tu confonds deux choses très différente.
L'objectif des traits n'est nullement de remplacer l'injection de dépendance, mais de supprimer la duplication de code rendue obligatoire dans certain cas particulier à cause du modèle objet de PHP qui ne supporte pas les mixin ou l'héritage multiple.
Les deux concepts ne sont tout simplement pas comparable.
La notion de performance n'a pas non plus à intervenir dans l'équation, puisqu'un trait ne peut absolument rien apporter à ce niveau.
Quand au traitement, tu peux y mettre absolument tout ce que tu veux, et en conséquence, tu peux utiliser un trait pour tout autre chose qu'un accesseur.
En conséquence, ton exemple en C# n'est pas recevable, vu qu'il n'est applicable qu'aux accesseurs, et que les traits sont largement plus générique que cela.
Il se trouve juste que l'accesseur de l'adaptateur d'Atoum est l'exemple que j'ai choisi, à la fois par facilité, et parce que c'est très facile à comprendre lors d'une conférence, ou le présentateur n'a pas le temps de passer 30 minutes à expliquer une problématique.
Ne te focalise donc pas sur cet exemple et regarde les choses de manière plus
: je te le répète, les traits ne sont pas fait pour remplacer l'injection de dépendance ;).8 De mageekguy - 17/12/2010, 11:36
@Guile : Le
insteadof
a pour but de permettre la gestion des collisions, lorsque par exemple deux traits utilisés par une même classe implémente chacun une méthode du même nom.Dans ce cas, la gestion des collisions est de la responsabilité du programmeur, vu que le compilateur ne peut pas deviner la méthode qu'il faut privilégier.
Le développeur utilise alors le mot-clef
insteadof
pour indiquer au compilateur laquelle des deux méthodes définie dans chacun des traits il doit inclure au final dans la classe.Le mécanisme d'aliasing, via
as
, a également la même vocation, même si la solution apportée, qui consiste à l'une ou l'autre des méthodes, est complètement différente.9 De Eric - 17/12/2010, 13:26
Je ne critique absolument pas tes slides, ni ton exemple, qui est effectivement bon pour comprendre les traits au niveau de leur utilisation. J'essaye juste d'aller un peu plus loin et de voir quels sont les bonnes pratiques qui vont m'obliger ou au contraire m'empêcher d'utiliser ces traits.
> je te le répète, les traits ne sont pas fait pour remplacer l'injection de dépendance
J'ai bien compris ! J'essaye juste de te dire l'inverse :
Chaque fois que tu utilise un trait (hors cas triviaux), tu ferais mieux de transformer ton trait en classe et l'injecter dans ta classe qui utilise ton trait.
Je tiens la phrase précédente pour vrai, jusqu'à preuve du contraire Voici pourquoi :
Si jamais je constate que j'utilise plusieurs fois des méthodes identiques dans plusieurs classes, alors, en général, je me dis qu'il y a un objet qui essaye d'émerger : je prend toutes les méthodes que je transforme en nouvelle classe et que j'injecte dans les classes précédentes.
Maintenant, on me dit : il y a les traits... Alors ok, mais dans quel mesure cela invalide-t-il ma solution et dans quelle mesure utiliser un trait sera une meilleur pratique.
> dans certain cas particulier à cause du modèle objet de PHP
Lesquels ?
10 De mageekguy - 17/12/2010, 14:10
@Eric : Notamment lorsque tu as deux classe dérivée d'une superclasse qui ont un besoin commun.
Et je t'invite à essayer de remplacer les méthodes de
setAdapter()
etgetAdapter()
par un objet, peut être que cela te fera comprendre que tu fais légèrement fausse route.Je pense également qu'une lecture du support de conférence de Stephan te sera profitable :).
11 De Eric - 17/12/2010, 15:36
Désolé d'insister
Si j'ai bien compris, les données du problème sont :
class A{}
class B extends A{}
class C extends A{}
soit
f()
, le besoin commun, alors, pourquoi pas :class D extends A(){ f() }
class B extends D{}
class C extends D{}
J'ai bon ?
Pour le cas
setAdapter
/getAdapte
r, comme il n'y a pas de traitement, dans un langage plus expressif que PHP, on aurait pu mettre :public Adapter adapter;
Mais bon, dans ce cas, on voit tout de suite que ca n'est pas les traits qui manque à PHP :), donc ok, les traits sont une manière particulièrement complexe de résoudre le problème d'absence de typage des attributs.
12 De mageekguy - 17/12/2010, 15:44
@Eric : On va prendre un cas concret : Atoum.
Je ne peux pas deviner à l'avance les classes qui auront besoin de recourir à l'adaptateur, vu que je ne suis pas devin.
Si j'applique ta méthode, il faut donc que je fasse dériver l'ensemble des classes d'Atoum d'une super-classe, éventuellement abstraite, qu'il serait par exemple possible de nommer
adaptedClass
.Ainsi, j'aurais un adaptateur de disponible dans toutes les classes d'Atoum.
L'inconvénient, c'est que je vais avoir un adaptateur dans toutes les instances de classe alors que je n'en ai pas forcément besoin, donc une consommation de mémoire inutile.
Et si j'ai d'autres besoins commun à plusieurs classes de ma hiérarchie d'objets, ma super-classe
adaptedClass
portera de plus en plus mal son nom, puisqu'elle assurera d'autres services que celui de fournir un adaptateur, et elle deviendra un fourre-tout immonde de plus en plus délicat à maintenir, sans compter évidemment que cela n'arrangera pas la consommation mémoire.À cela, tu vas certainement me répondre qu'il n'y a besoin de dériver une classe de
adaptedClass
uniquement dans le cas ou cette classe a effectivement besoin d'un adaptateur.Et je te répondrais que si la classe en question dérive déjà d'une autre classe, tu es baisé !
Une solution pourrait être de passer par une interface, pour forcer l'implémentation des méthodes nécessaires, mais dans ce cas, tu te retrouve à dupliquer du code dans toutes les classes qui ont besoin d'un adaptateur.
Tiens, de la duplication de code... justement le truc que les traits sont censé permettre de supprimer...
En effet, avec un trait, tu peux injecter uniquement au besoin sans te soucier de l'héritage, et donc injecter ta fonctionnalité sans te soucier du modèle objet et uniquement en fonction des besoins.
Et comme tu peux injecter plusieurs traits dans un classe, tu peux injecter un trait par fonctionnalité commune.
Je conclurais en te disant que les traits n'ont nullement pour but de remplacer l'injection de dépendance, ni de pallier au typage faible de PHP.
Bref, ta technique est passable, et encore je suis gentil, car en réalité, je la trouve immonde, et elle ne répond au problème passablement que si tu n'as qu'une seule fonctionnalité commune à injecter.
Comme je l'ai dis lors de la conférence, je préfère encore faire de la duplication de code en me protégeant au maximum des bugs et des régressions à l'aide des tests unitaires que de recourir à cette magouille.
Les traits sont bien plus élégants et efficaces pour résoudre le problème.
Afin d'être clair, reprenons tes classes
A
,B
, etC
et supposons queA
etC
ont un besoinf()
commun, et queC
etB
ont un besoinf'()
commun.C
a donc besoin def()
etf'()
.Avec ta logique, tu fais donc une classe
X
qui contientf()
etf'()
et tu fais dériver deX
tes classesA
,B
etC
.Oups, où est la bassine, j'ai envie de vomir !
Avec les traits, tu fais un trait
T1
contenantf()
, un traitT2
contenantf'()
, et tu injecteT1
dansA
etC
,T2
dansB
etT1
etT2
dansC
.C'est tout de même beaucoup plus propre, non ?
13 De Guile - 17/12/2010, 17:35
@mageekguy J'avais bien compris l'utilité de gérer explicitement les collisions. Mon interrogation va plus particulièrement sur l'utilité de insteadof lui-même.
Dans l'exemple suivant :
trait T1 { function foo() {echo 'T1';}}
trait T2 { function foo() {echo 'T2';}}
class A {
use T1, T2 {
}
}
Je trouve que le texte "insteadof T2" est une redondance avec "T1::foo".
"Attention cher compilateur, ici je vais utiliser le foo de T1" et "Attention cher compilateur, ici je vais utiliser le foo de T1, et pas celui de T2" sont identiques pour moi.
Imaginons (soyons fou) qu'une classe utilise 100 traits (ouai jsuis fou!), faudra écrire
T1:foo insteadof T2, T3, T4, T5, T6, T7, T8 ... T98, T99, T100;
Merci la valeur ajoutée de la partie insteadof.
L'ajout de "insteadof est donc en trop, sauf si cas alambiqué...
Ma question reste donc toujours : quelle est la raison du insteadof dans le cadre des conflits.
14 De metagoto - 18/12/2010, 00:29
@Eric
Les traits permettent d'injecter des *methodes* à la compilation. C'est différent d'une simple injection d'objet (ou de toutes autres variables à l'execution). La classe host une fois dotée des nouvelles méthodes des traits peut donc se conformer à telle ou telle interface, si nécessaire. C'est clairement un nouveau paradigme par rapport à l'héritage ou la composition.
@Guile
Oui, insteadof est redondant. On pourrait s'en passer. D'un point de vue grammaticale, ça aurait fait bizarre d'avoir un simple "symbole;" plutôt que "symbol mot-clé symbol;" dans le bloc du use (enfin, ça c'est mon avis).
Dans les faits, insteadof a son utilité puisque le compilo maintient la liste des traits qui doivent être exclus pour une méthode particulière. Lorsque les méthodes sont mergées dans la classe, php se contente de regarder dans cette liste plutôt que de parcourir tous les traits pour déterminer ce qui doit être exclus. De plus, comme une méthode exclue peut tout de même être mergée car elle est renommée (mot-clé as), il est plus simple au final de laisser l'utilisateur être explicite sur ce qui doit être exclu et renommé. Ca fait des calculs/parcours en moins à faire à la compilation.
En ce qui me concerne, les traits seraient plutôt réservés aux "libristes", les mecs qui font les librairies quoi... D'ailleurs, l'exemple qui est mis en avant dans les documents (pour Smalltalk) concerne la mécanique/architecture de la librairie des collections. Le coté stateless des traits tend à réserver leur usages pour des constructions internes et donc peu ou pas du tout exposées aux utilisateurs.
Par exemple, implémenter streamWrapper avec des traits. Exposer ça aux utilisateurs serait, je pense, bien trop complexe: il faudrait des compromis sur les noms des variables (ou se reposer sur 50 méthodes abstraites pour maintenir un état). Par contre en interne, à la rigueur, pourquoi pas, si les libristes se mettent d'accord sur comment ils maintiennent les états (les variables membres).
15 De Eric - 18/12/2010, 07:24
Hey, c'est pas du jeu, je répondais à ta question : "Notamment lorsque tu as deux classe dérivée d'une superclasse qui ont un besoin commun.". Je trouve la solution immonde aussi !
Ma méthode c'est :
Départ:
class A { f();}
class B { f();}
Arrivé :
class F {f()};
class A { private F; setF(F) }
class B { private F; setF(F) }
C'est une technique de refactoring que j'utilise au quotidien et qui permet de simplifier considérablement un problème et de faire "emerger" de nouveaux objets plus réutilisable.
S'il se trouve que deux classes A et B ont des fonctions identiques (f()), alors, il existe probablement une classe F qui répond à ce besoin et qui est utilisé par A et B.
Tu notera la similitude de ma solution avec :
trait F {f()};
class A { use F }
class B { use F }
Cela me parait avoir les même lacunes que l'héritage (le code de la classe est bien plus difficile à suivre à cause des fonctions "caché" dans l'arbre d'héritage, dépendance forte entre le trait et tous les utilisateurs)
(J'ai trouvé ca : je ne suis donc pas débile, on est au moins deux à penser la même chose sur les traits : http://stackoverflow.com/questions/... réponse 1)
(In my experience delegation gives a much better and more reusable design in a dynamically typed object-oriented language like Smalltalk.)
16 De Cyrano - 18/12/2010, 09:36
@Guile
Je crois qu'il faut rester à un niveau pratique. En isolant dans un code des fonctionnalités dupliquées dans diverses classes, on les extrait pour créer des traits. Je pars de l'idée que dans un trait, il existera plusieurs méthodes. Mais on risque de se retrouver avec plusieurs traits ayant des méthodes ayant le même nom. Si dans une des classes tu as besoin d'utiliser plus d'un trait et qu'au moins deux d'entre eux ont une méthode ayant le même nom, il devient indispensable d'indiquer laquelle des deux tu veux pouvoir utiliser.
Corrige-moi mageekguy si je me trompe : l'idée du insteadof va surtout servir à factoriser du code existant en extrayant les duplications existantes dans des traits et on va forcément avoir des collisions de noms de méthodes. Dans une phase ultérieure, lorsqu'on développera dès le départ en utilisant la technique des traits, on pourra coder les traits en veillant à éviter d'utiliser des noms identiques dans différents traits, évitant ainsi à l'avenir de devoir recourir au insteadof : d'ici là, le insteadof va faciliter grandement la transition vers cette nouvelle manière de coder à partir de code existant.
17 De mageekguy - 18/12/2010, 19:57
@Eric : Tu dupliques donc le code de ton setF() dans chaque classe qui en a besoin et c'est justement ce que je fais dans Atoum et que j'ai supprimé grâce à un trait.
Tu vas y arriver, à voir la lumière ;).
18 De mageekguy - 18/12/2010, 19:58
@metagoto : Cool, je n'ai même plus besoin de répondre aux commentaires sur mon propre blog :D, mes lecteurs s'en charge pour moi et bien mieux que ce que j'aurais pu faire, en plus.
19 De sunseb - 20/12/2010, 05:09
Intéressant ces traits ! En gros, c'est une sorte d'héritage horizontal ?
Je pensais à un cas concret que j'ai vu récemment : une classe User utilisée pour gérer son profil, son compte, son accès, son mot de passe, etc.
class User {
}
$user->getGender();
On se retrouve rapidement avec une super-classe et des getters/setters dans tous le sens. Ce qui nous mène généralement à :
class User {
}
$user->getProfile()->getGender();
Un peu tordu comme implémentation (un objet dans l'objet). Avec les traits, on pourrait imaginer qqch comme :
class User {
}
$user->getGender();
C'est quand même beaucoup plus propre au niveau du code et ça permet de structurer le code de classes génériques qui ont beaucoup de méthodes.