Hello, Le truc que je n'aime pas avec les présentations que j'ai vu sur DCI c'est pas tant l'"extend" par objet, c'est plutôt d'avoir une classe par "méthode". En fait, pour être précis, ce sont surtout les *présentations* qui ne m'ont pas plues car un peu trop dogmatiques. Les "flavors" de DCI sont peut-être une solution propre à pas mal de problèmes, mais l'important c'est d'identifier les cas où cette approche est pertinente et ce qu'elle apporte. Pour moi l'avantage de cette approche c'est de "router" vers les bonnes méthodes en fonction du contenu des données. J'estime que les modules apportent déjà l'encapsulation et donc DCI est redondant si le but n'est que l'encapsulation. Ensuite, pour "l'extend", je ne suis pas sûr que ce soit une raison suffisante pour rejeter DCI. Après tout, les perfs pourries de extend sont surtout un problème d'implémentation dans MRI, ce genre de truc peut varier d'une implémentation à l'autre. Du reste, si tes modules sont bien isolés, c'est trivial de changer du code qui fait un "extend" en une classe qui prend un objet et là où tu avais un "self" tu as l'objet en question: au final, foo.bar(machin) (DCI) ou Bar.new(foo).machin (contextes, cf. les liens d'Olivier, j'entrevoyais un problème proche ici: http://unchoke.me/blog/2012/08/15/on-states-and-behaviours/ ) c'est du kiffkiff niveau lisibilité.
A mon avis, les contextes et DCI marchent bien quand il y a plusieurs prismes par lesquels regarder un objet, et que le prisme dépend d'un état contenu dans l'objet, et, qui n'est pas déterminé au "compile time" (i.e., quand tu édites ton code pour du Ruby). En gros si t'as un stream d'évènements JSON ou XML qui t'arrive dans une socket, et que tu dois tantôt les convertir des fois en pomme, parfois en prunes, et tantôt en poire ; si en plus tu as des fruits rouges et des fruits verts, ton problème se résume à trouver les bonnes méthodes dans une matrice 3x2. Si tu fais du code un peu soigné, tu ne vas pas faire une méthode spécifique à "pomme verte" (même si c'est valable dans une première itération) pour éviter que ce code path existe quand tu as une "poire". Tu ne vas pas non plus écrire une sous classe pour chaque des six combinaisons fruit/couleur (surtout que, dans cette analogie, tu pourrais avoir des éléments qui ont plusieurs couleurs). En revanche, ce serait raisonnable d'avoir 5 modules (3 qui répondent plus ou moins à une interface Fruit et 2 à Couleur) et tu vas "étendre" (ou encapsuler) à la volée chaque JSON/XML que tu reçois. Enfin, si tu as une méthode qui a besoin d'une couleur+d'un fruit, alors il te faudra peut-être une classe/méthode/fonction qui se base sur ces deux valeurs. En clair, DCI/Contextes permet de sélectionner quelles méthodes sont applicables sur quelle état des données d'entrées. Dans le cas où tu as un framework web tel que Rails qui, pour ton bien, fait le job d'encapsuler les requêtes dans le modèle correspondant au contenu des données (l'URL), ça ne semble pas universellement judicieux de refaire cette partie du travail. Je te conseille de démarrer "fat model" puis de modulariser progressivement en faisant juste des module MyModule ... end autour des méthodes qui sont liées (souvent une grosse méthode plein de if/then et une panoplie de "one-line-helpers" genre is_machin_chose?(bidule)). In fine, le but est de router les bonnes data aux bonnes méthodes et DCI peut servir, fais confiance à ton XP/flair, des fois le truc genre "gros sale avec des if et des else et des variables locales comparées à des variables d'instances" est pertinent si tu ne trouves pas de moyen immédiat de rendre ton code plus propre. Faire du Ruby ça demande de savoir fuir les problèmes par la première porte de sortie, ce qui est très difficile quand on est curieux (c'est du vécu). L'important c'est de se mettre un TODO: refactor XYZ et d'avoir la discipline pour revenir dessus. D'expérience, quand on a ce genre de questionnements quand on code c'est que soit le cerveau n'est plus très frais (i.e., oui, on devient vraiment temporairement plus con) soit on a l'impression d'avoir beaucoup de temps devant soi et qu'on peut se permettre de procrastiner-positivement. Dans le second cas, mets toi une deadline (en t'autorisant un peu de recherche perso pour gagner un peu d'XP). Dans le premier cas, autant passer son temps sur des problèmes moins intellectuellement difficiles mais tout aussi important/utiles pour le projet, genre demander à un tiers si la couleur du titre lui semble laide (ce qui risque d'avoir un impact plus important sur ton projet que si tu utilises extend ou machin.new(chose)). Let me know si ça résonne un peu en toi :). --Lucas, donneur de coup de pieds au cul à toute heure :P Le 3 juin 2013 21:35, Simon Courtois <[email protected]> a écrit : > Concernant ton gist une recommandation, évite de faire des classes qui > héritent de Struct.new. > Ça crée déjà une classe donc tu ajoutes une niveau inutile. > > Tu peux faire ceci à la place: > > MaClasse = Struct.new(:a, :b) do > def plop > "toto" > end > end > > Pour le reste question de point de vue, je trouve qu'on a plus de fierté à > faire notre travail du mieux qu'on peut ;) > > Simon Courtois > > On Monday 3 June 2013 at 21:29, Guirec Corbel wrote: > > J'ai fait un gist avec ma solution : > https://gist.github.com/GCorbel/5699834. Je penses que je suis content > avec ça. > > Tout de même, c'est pas facile de vouloir faire son métier comme il faut. > Ha que j'étais bien quand je faisais du PHP pourri qui bugait tout le temps > et qui n'avait aucune architecture mais que je m'en foutais. C'est une > grosse prise de tête de vouloir toujours faire au mieux. Je ne suis jamais > sûre d'avoir fait du bon code... > > > Le 3 juin 2013 13:07, Simon Courtois <[email protected]> a écrit : > > C'est un edge case et selon moi dans le cas du remplacement de DCI par > SimpleDelegator il ne s'applique pas plus que ça. > Cela dit, si on est est dans une situation de "contexte" ça me choquerait > pas que InfravisionPotionModule implémente un "observe" décoré. > Au final c'est ce que je fais dans Draper quand j'en ai besoin. > > Pour le reste, je pense que tu as raison de rester sur des Service > objects, c'est une bonne façon de fonctionner. > > Bonne journée, > > > > Simon Courtois > > On Monday 3 June 2013 at 18:59, Guirec Corbel wrote: > > Simon, As-tu vu ça : > http://devblog.avdi.org/2012/01/31/decoration-is-best-except-when-it-isnt/? > Il peut y avoir quelques soucis à utiliser SimpleDelegator. > > Je pense que je vais continuer à faire comme je fais. Mettre de la logique > dans le contrôleur et utiliser des services objects quand il y a un logique > un peu plus complexe. À chaque fois qu'il y a besoin de callbacks il est > mieux de le remplacer par un service object. Si jamais j'ai besoin > d'utiliser un héritage je ferais un rôle à la place. > > Un exemple de code que j'aime bien : > > class MessagesController < InheritedResource::Base > def create > sender = User.find(params[:send_id]) > receiver = User.find(params[:receiver_id]) > > notifier = MessageNotifier.new(sender, receiver) > notifier.call > > MessageGeocoder.new(notifier.message).call > > create! > end > end > > class MessageNotifier < Struct.new(:sender, :receiver, :message) > def self.call(sender, receiver) > MessageNotifier.new(sender, receiver).call > end > > def call > receiver.extend(CollectorRole) > #traitement > end > end > > class MessageGeocoder < Struct.new(:message) > def self.call(message) > MessageGeocoder.new(message).call > end > > def call > #traitement > end > end > > module CollectorRole > def paintings > #find paintings > end > end > > Je crois que c'est propre. Ça serait facile de factoriser la méthode > "self.call". La différence entre un service object et un context c'est que > j'ai l'impression que le context doit prendre tout le traitement faire dans > l'action d'un contrôleur. Un service object fait quelque chose de plus > petit et on peut en utiliser plusieurs par action. > > Ça reste facile à comprendre et a tester. Je ne me sens pas tyranniser par > le DCI. Avec le DCI ça m'aurait gêner d' utiliser "@user = User.all" dans > un contrôleur. Avec un service object je fais ce que je veux. > > Vous en pensez quoi? > > > Le 3 juin 2013 12:00, Olivier El Mekki <[email protected]> a écrit : > > Hello, > > Personnellement, dans le DCI, l'idée d'avoir des méthodes disponibles > sur le model uniquement lorsqu'on en a besoin m'intéresse peu : ça > m'intéresse que mon code soit lisible, donc je vais utiliser des modules > ou des concerns, mais peu m'importe que des méthodes soient disponibles > sur mon instance alors qu'elle n'en a pas besoin. > > Le concept de context m'a en revanche beaucoup plus séduit, notamment > parce qu'il me permet de sortir de l'enfer du STI et permet de résoudre > des problèmes que le STI lui-même ne permet pas de résoudre. J'utilise > aujourd'hui quotidiennement des contextes via cette implémentation : > https://gist.github.com/oelmekki/474dcc99649a82986dc3 > > L'idée est de faire gérer les validations, filtrages de paramètres et > before / after save (qui seraient conditionnels) par une classe dédiée. > Ça permet d'éviter d'avoir deux models (ou plus) pour gérer des > validations spécifiques de ce qui est finalement une seule resource. > Ça permet également de prendre en compte ce principe de rôle (sans > toutefois nécessiter que le rôle soit définit dans une classe, dans > mon implémentation). Parce qu'après tout, lorsqu'un admin edit un > utilisateur, ce n'est pas la même chose que lorsqu'un utilisateur > s'edite lui même (pas les mêmes validations, pas les mêmes callbacks, > pas les mêmes attributs autorisés, etc). > > La documentation inclue dans ce fichier en dit plus long. > > Et effectivement, pas question de forcer son usage dans toutes les > actions : on doit pouvoir faire un `Foo.all` si c'est tout ce qui > est nécessaire :) > > Note : pour exécuter ce code, il faut utiliser active_record, > strong_parameters et cette lib : > https://gist.github.com/oelmekki/4a82dce1d9c2a5d66936 > > On 10:40 Mon 03 Jun , Guirec Corbel wrote: > > Bonjour à tous, > > > > J'ai lu quelques trucs à propos du DCI (Data Context Integration) et je > ne > > sais pas quoi en penser. J'aimerais avoir votre avis sur la question. > > > > Pour ceux qui ne connaissent pas, je vous conseil de lire ceci : > > http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby. > > > > J'aime bien le concept de rôle. Dans l'application que je fais j'ai des > > utilisateurs et un type d’utilisateur spécial, le collectionneur. Si j'ai > > bien compris le concept il faut écrire un role comme ceci : > > > > module CollectorRole > > def paintings > > Painting.where(user_id: self.id) > > end > > end > > > > Et pour avoir toutes les peintures d'un collectionneur je pourrais faire > > ceci : > > > > user = User.new > > user.extend CollectorRole > > user.paintings > > > > Je n'aurais donc que deux modèles minimaux, User et Painting, > représentant > > uniquement les données (présente dans la base donnée). Si je veux ajouter > > un comportement à mon utilisateur je l'ajouterai dans un rôle. Je respect > > donc plus facilement le SRP (Single Responsability Principle). Ça > remplace > > un héritage. > > > > Là ou j'ai du mal c'est au niveau du context et du contrôleur. Un exemple > > qui fonctionne pas trop mal serait dans le cas ou je voudrais faire une > > fonctionnalité qui permet aux utilisateurs de contacter les > > collectionneurs. Je ferais un truc du genre : > > > > class UserContactCollectorContext > > attr_reader :user_from, user_to > > > > def self.call(user_from, user_to) > > UserContactCollectorContext.new(user_from, user_to).call > > end > > > > def initialize(user_from, user_to) > > @user_from, @user_to = user_from, user_to > > @user_to.extend CollectorRole > > end > > > > def call > > @paintings = @user_to.paintings > > # traitement et envoi du mail > > end > > end > > > > Et dans mon contrôleur j'aurais ceci : > > > > class MessagesController < ApplicationController > > def send_message_to_collector > > UserContactCollectorContext.call(User.find(params[:user_from]), > > User.find(params[:user_to])) > > end > > end > > > > Ce code me convient. Là ou ça ne me convient pas c'est quand je veux tout > > simplement la liste des utilisateurs. Il faudrait faire un contrôleur du > > genre : > > > > class UsersController < ApplicationController > > def index > > @users = UserAllContext.call > > end > > end > > > > Il y a un exemple similaire ici : > > > https://github.com/randx/rails-dci-example/blob/master/app/controllers/documents_controller.rb > > > > Franchement, créé un context pour chaque action de ce type je trouve que > > c'est trop. C'est plus simple de faire "@users = User.all". L'avantage > > c'est que ça sépare vraiment la logique. Le contrôleur n'a pas à savoir > ce > > que fait le context. S'il y a un mail envoyé, un géocodage ou tout le > > reste, le contrôleur ne le sait pas. C'est vraie pour l'inverse. Le > context > > ne sait pas ce que fait le contrôleur. On a donc un séparation franche > > entre la logique métier et la partie système. > > > > J'ai lu le livre CleanRuby. Je penses que Jim Gay va trop loin. Il créé > un > > context "framework agnostic" qui appel des fonctions du contrôleur. J'ai > du > > mal à comprendre pourquoi il écrit un livre prônant la valeur du "Skinny > > Controller" alors qu'il a du code comme ceci : > > https://github.com/radiant/radiant/blob/master/app/models/page.rb. > > > > Qu'en pensez-vous? Est-ce que quelqu'un d'entre vous à mis en pratique ce > > concept? Quels sont vos impressions? > > > > Merci à tous! > > > > -- > > -- > > Vous avez reçu ce message, car vous êtes abonné au groupe "Railsfrance" > de Google Groups. > > Pour transmettre des messages à ce groupe, envoyez un e-mail à l'adresse > [email protected] > > Pour résilier votre abonnement envoyez un e-mail à l'adresse > [email protected] > > --- > > Vous recevez ce message, car vous êtes abonné au groupe Google > Groupes Railsfrance. > > Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le > concernant, envoyez un e-mail à l'adresse > [email protected]. > > Pour plus d'options, visitez le site > https://groups.google.com/groups/opt_out . > > > > > > > -- > Olivier El Mekki. > > -- > -- > Vous avez reçu ce message, car vous êtes abonné au groupe "Railsfrance" de > Google Groups. > Pour transmettre des messages à ce groupe, envoyez un e-mail à l'adresse > [email protected] > Pour résilier votre abonnement envoyez un e-mail à l'adresse > [email protected] > --- > Vous recevez ce message, car vous êtes abonné au groupe Google > Groupes Railsfrance. > Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le > concernant, envoyez un e-mail à l'adresse > [email protected]. > Pour plus d'options, visitez le site > https://groups.google.com/groups/opt_out . > > > > -- > -- > Vous avez reçu ce message, car vous êtes abonné au groupe "Railsfrance" de > Google Groups. > Pour transmettre des messages à ce groupe, envoyez un e-mail à l'adresse > [email protected] > Pour résilier votre abonnement envoyez un e-mail à l'adresse > [email protected] > --- > Vous recevez ce message, car vous êtes abonné au groupe Google > Groupes Railsfrance. > Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le > concernant, envoyez un e-mail à l'adresse > [email protected]. > Pour plus d'options, visitez le site > https://groups.google.com/groups/opt_out . > > > > > -- > -- > Vous avez reçu ce message, car vous êtes abonné au groupe "Railsfrance" de > Google Groups. > Pour transmettre des messages à ce groupe, envoyez un e-mail à l'adresse > [email protected] > Pour résilier votre abonnement envoyez un e-mail à l'adresse > [email protected] > --- > Vous recevez ce message, car vous êtes abonné au groupe Google > Groupes Railsfrance. > Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le > concernant, envoyez un e-mail à l'adresse > [email protected]. > Pour plus d'options, visitez le site > https://groups.google.com/groups/opt_out . > > > > > -- > -- > Vous avez reçu ce message, car vous êtes abonné au groupe "Railsfrance" de > Google Groups. > Pour transmettre des messages à ce groupe, envoyez un e-mail à l'adresse > [email protected] > Pour résilier votre abonnement envoyez un e-mail à l'adresse > [email protected] > --- > Vous recevez ce message, car vous êtes abonné au groupe Google > Groupes Railsfrance. > Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le > concernant, envoyez un e-mail à l'adresse > [email protected]. > Pour plus d'options, visitez le site > https://groups.google.com/groups/opt_out . > > > > > -- > -- > Vous avez reçu ce message, car vous êtes abonné au groupe "Railsfrance" de > Google Groups. > Pour transmettre des messages à ce groupe, envoyez un e-mail à l'adresse > [email protected] > Pour résilier votre abonnement envoyez un e-mail à l'adresse > [email protected] > --- > Vous recevez ce message, car vous êtes abonné au groupe Google > Groupes Railsfrance. > Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le > concernant, envoyez un e-mail à l'adresse > [email protected]. > Pour plus d'options, visitez le site > https://groups.google.com/groups/opt_out . > > > -- -- Vous avez reçu ce message, car vous êtes abonné au groupe "Railsfrance" de Google Groups. Pour transmettre des messages à ce groupe, envoyez un e-mail à l'adresse [email protected] Pour résilier votre abonnement envoyez un e-mail à l'adresse [email protected] --- Vous recevez ce message, car vous êtes abonné au groupe Google Groupes Railsfrance. Pour vous désabonner de ce groupe et ne plus recevoir d'e-mails le concernant, envoyez un e-mail à l'adresse [email protected]. Pour plus d'options, visitez le site https://groups.google.com/groups/opt_out .
