Avantages et inconvénients de l'utilisation des callbacks pour la logique du domaine dans les Rails

Quels sont les avantages et les inconvénients de l'utilisation des callbacks pour la logique du domaine? (Je parle dans le contexte des projets rail et/ou Ruby.)

pour commencer la discussion, je voulais mentionner cette citation de la page Mongoïde sur les callbacks :

utiliser des callbacks pour la logique de domaine est une mauvaise pratique de conception, et peut conduire à des erreurs inattendues qui sont difficiles à déboguer quand rappels dans la chaîne arrêter exécution. C'est notre recommandation pour les utiliser uniquement pour transversaux des préoccupations, comme faire la queue pour des emplois de fond.

je serais intéressé d'entendre l'argument ou la défense derrière cette réclamation. S'applique-t-il uniquement aux demandes appuyées par la Mongo? Ou est-il destiné à s'appliquer à l'ensemble des technologies de base de données?

Il semble que Le Rubis sur des Rails de Guide à ActiveRecord des Validations et des Rappels pourrait en désaccord, au moins quand il s'agit de bases de données relationnelles. Prenez cet exemple:

class Order < ActiveRecord::Base
  before_save :normalize_card_number, :if => :paid_with_card?
end

à mon avis, ceci est un exemple parfait d'un simple rappel qui implémente la logique du domaine. Il semble rapide et efficace. Si je devais suivre le Conseil Mongolien, où cette logique irait-elle à la place?

20
demandé sur David J. 2012-06-14 22:18:25

6 réponses

j'aime vraiment utiliser les callbacks pour les petites classes. Je trouve qu'il rend une classe très lisible, par exemple quelque chose comme

before_save :ensure_values_are_calculated_correctly
before_save :down_case_titles
before_save :update_cache

ce qui se passe est immédiatement clair.

je trouve même cela testable; je peux tester que les méthodes elles-mêmes fonctionnent, et je peux tester chaque rappel séparément.

je crois fermement que les rappels dans une classe doit seulement être utilisés pour des aspects qui appartiennent à la classe. Si vous voulez déclencher des événements lors de save, par exemple envoyer un mail si un objet est dans un certain état, ou logging, j'utiliserais un Observer . Cela respecte le principe de responsabilité unique.

Rappels

L'avantage de rappels:

  • tout est en un seul endroit, donc cela rend facile
  • code très lisible

le désavantage des callbacks:

  • puisque tout est un lieu, il est facile de briser le principe de la responsabilité unique
  • pourrait faire pour les classes lourdes
  • ce qui se passe si une fonction de rappel échoue? elle suit toujours la chaîne? Conseil: assurez-vous que vos callbacks n'échouent jamais, ou mettez autrement l'état du modèle à invalide.

observateurs

le avantage des observateurs

  • code très propre, vous pourriez faire plusieurs observateurs pour la même classe, chacun faisant une chose différente
  • l'exécution des observateurs n'est pas couplée

L'inconvénient des observateurs

  • au début, il pourrait être étrange comment le comportement est déclenché (regardez dans l'observateur!)

Conclusion

Donc, en résumé:

  • utiliser les callbacks pour les choses simples, liées au modèle (valeurs calculées, valeurs par défaut, validations)
  • utiliser des observateurs pour des comportements plus transversaux (par exemple, envoi de courrier, propagation de l'état,...)

et comme toujours: tout conseil doit être pris avec un grain de sel. Mais d'après mon expérience, les observateurs ont une très bonne échelle (et sont aussi peu connus).

Espérons que cette aide.

28
répondu nathanvda 2015-01-29 21:54:19

EDIT: j'ai combiné mes réponses sur les recommandations de certaines personnes ici.

résumé

sur la base de quelques lectures et réflexions, je suis arrivé à quelques déclarations (provisoires) de ce que je crois:

  1. La déclaration "à l'Aide de rappels pour le domaine de la logique est une mauvaise pratique de la conception" est faux, comme l'a écrit. Il surestime le point. Les Callbacks peuvent être un bon endroit pour la logique du domaine, utilisé de manière appropriée. La question ne devrait pas être si la logique du modèle de domaine devrait aller dans les callbacks, il est quel genre de logique de domaine a du sens d'aller dans.

  2. La déclaration "à l'Aide de rappels pour le domaine de la logique ... peut conduire à des erreurs qui sont difficiles à déboguer quand rappels dans la chaîne d'interrompre l'exécution" est vrai.

  3. Oui, les rappels peuvent causer de la chaîne d' les réactions qui affectent d'autres objets. Dans la mesure où cela n'est pas vérifiable, il s'agit d'un problème.

  4. Oui, vous devriez être en mesure de tester votre logique d'entreprise sans avoir à sauvegarder un objet dans la base de données.

  5. si les callbacks d'un objet deviennent trop gonflés pour vos sensibilités, il y a d'autres conceptions à considérer, y compris (a) les observateurs ou (B) les classes d'aide. Ceux-ci peuvent manipuler proprement multi-objet opérations.

  6. le Conseil "de n'utiliser [des callbacks] que pour des préoccupations transversales, comme faire la queue pour des travaux de fond" est intrigant mais exagéré. (J'ai passé en revue préoccupations transversales pour voir si j'oubliais quelque chose.)

je veux aussi partager certaines de mes réactions aux billets de blog que j'ai lu qui parlent de cette question:

réactions au "ActiveRecord de Rappels Ruiné Ma Vie"

Mathias Meyer 2010 post, ActiveRecord de Rappels Ruiné Ma Vie , offre un point de vue. Il écrit:

chaque fois que j'ai commencé à ajouter des validations et des callbacks à un modèle dans une application Rails [...] Il s'est juste senti mal. J'ai l'impression d'ajouter du code qui ne devrait pas être là, qui rend tout beaucoup plus compliqué, et qui transforme l'explicite en code implicite.

je trouve que cette dernière revendication "se transforme en code implicite" pour être, bien, une attente injuste. On parle de Rails ici, non?! Une grande partie de la valeur ajoutée réside dans le fait que les Rails font des choses "par magie", par exemple sans que le développeur n'ait à le faire explicitement. Ne semble-t-il pas étrange d'apprécier les fruits des Rails et pourtant de critiquer le code implicite?

Code qui est seulement exécuté en fonction de la la persistance de l'état d'un objet.

je suis d'accord que cela semble peu recommandables.

Code qui est difficile à tester, parce que vous avez besoin de sauvegarder un objet pour tester des parties de votre logique d'entreprise.

Oui, cela rend les tests lents et difficiles.

donc, en résumé, je pense que Mathias ajoute un carburant intéressant au feu, bien que je ne trouve pas tout cela convaincant.

Réactions "Fou, Hérétiques, et Impressionnant: La Façon dont j'Écris les Rails" Applications

, Dans James Golick 2010 post, Fou, Hérétiques, et Impressionnant: La Façon dont j'Écris les Rails Apps , il écrit:

aussi, coupler toute votre logique d'affaires à vos objets de persistance peut avoir des effets secondaires bizarres. Dans notre application, quand quelque chose est créé, un callback after_create génère une entrée dans les logs, qui sont utilisé pour produire le flux d'activité. Que faire si je veux créer un objet sans me connecter, par exemple, dans la console? Je ne peux pas. Sauver et l'exploitation forestière sont mariés pour toujours et pour toute l'éternité.

plus tard, il en arrive à la racine:

la solution est en fait assez simple. Une explication simplifiée du problème est que nous avons violé le principe de Responsabilité Unique. Donc, nous allons utiliser les techniques orientées objet séparer les préoccupations de notre modèle logique.

j'apprécie vraiment qu'il modère son conseil en vous disant Quand il s'applique et quand il ne s'applique pas:

la vérité est que dans une simple application, les objets de persistance obèse pourrait ne jamais faire de mal. C'est quand les choses deviennent un peu plus compliquées que les opérations CRUDS que ces choses commencent à s'accumuler et deviennent des points douloureux.

9
répondu David J. 2012-06-20 17:11:13

cette question ici ( ignorez les échecs de validation dans rspec ) est une excellente raison pour ne pas mettre la logique dans vos callbacks: la testabilité.

votre code can ont tendance à développer de nombreuses dépendances au fil du temps, où vous commencez à ajouter unless Rails.test? dans vos méthodes.

je recommande de ne garder que la logique de formatage dans votre rappel before_validation , et de déplacer les choses qui touchent plusieurs classes dans un objet de Service.

donc dans votre cas, je déplacerais le normalize_card_number vers une validation avant, et ensuite vous pourrez valider que le numéro de carte est normalisé.

mais si vous aviez besoin de créer un profil de paiement quelque part, je le ferais dans un autre objet de flux de service:

class CreatesCustomer
  def create(new_customer_object)
    return new_customer_object unless new_customer_object.valid?
    ActiveRecord::Base.transaction do
      new_customer_object.save!
      PaymentProfile.create!(new_customer_object)
    end
    new_customer_object
  end
end

vous pouvez alors facilement tester certaines conditions, comme si elle n'est pas valide, si la sauvegarde ne se produit, ou si la passerelle de paiement jette une exception.

2
répondu Jesse Wolgamott 2017-05-23 12:10:39

à mon avis, le meilleur scénario pour utiliser les callbacks est quand la méthode qui le déclenche n'a rien à voir avec ce qui est exécuté dans le callback lui-même. Par exemple, un bon before_save :do_something ne doit pas exécuter de code lié à sauvegarde . C'est plus comme si un Observateur devait fonctionner.

les gens ont tendance à utiliser les callbacks seulement pour sécher leur code. Ce n'est pas mauvais, mais peut conduire à compliqué et difficile de maintenir le code, parce que la lecture de la méthode save ne vous dit pas tout ce qu'elle fait si vous ne le faites pas notice un callback est appelé. Je pense que c'est important pour le code explicite (en particulier dans Ruby et Rails, où tant de magie se produit).

Tout ce qui concerne sauver devrait être dans la méthode save . Si, par exemple, le rappel est de s'assurer que l'utilisateur est authentifié, qui n'a aucun rapport avec sauver , alors c'est un bon scénario de rappel.

2
répondu Wawa Loo 2012-06-19 12:26:33

Avdi Grimm ont quelques grands exemples dans son livre objet sur Rails .

vous trouverez ici et ici pourquoi il ne choisit pas l'option callback et comment vous pouvez vous en débarrasser simplement en supplantant la méthode ActiveRecord correspondante.

dans votre cas, vous finirez avec quelque chose comme:

class Order < ActiveRecord::Base

  def save(*)
    normalize_card_number if paid_with_card?
    super
  end

  private

  def normalize_card_number
    #do something and assign self.card_number = "XXX"
  end
end

[mise à jour après votre commentaire "c'est toujours de rappel"]

quand nous parlons de callbacks pour la logique de domaine, je comprends ActiveRecord callbacks, s'il vous plaît me corriger si vous pensez que la citation de Mongoid referer à quelque chose d'autre, s'il ya un "callback design" quelque part, je ne l'ai pas trouvé.

je pense ActiveRecord rappels sont, pour la plupart (la totalité?) partie rien de plus que du sucre syntaxique dont vous pouvez vous débarrasser par mon exemple précédent.

D'Abord, Je d'accord que cette méthode de callbacks cache la logique derrière eux : pour quelqu'un qui n'est pas familier avec ActiveRecord , il devra l'apprendre pour comprendre le code, avec la version ci-dessus, il est facilement compréhensible et vérifiable.

qui pourrait être pire avec le ActiveRecord rappelle son "usage commun" ou le "sentiment de découplage" qu'ils peuvent produire. La version callback peut sembler agréable au début, mais comme vous allez ajouter plus de callbacks, il sera plus difficile de de comprendre votre code (dans quel ordre sont-ils chargés, que l'on peut arrêter le flux d'exécution, etc...) et le tester (votre logique de domaine est couplée avec la logique de persistance ActiveRecord ).

quand je lis mon exemple ci-dessous, je me sens mal à propos de ce code, c'est l'odeur. Je crois que vous ne finissez probablement pas avec ce code si vous faisiez TDD/BDD et, si vous oubliez ActiveRecord , je pense que vous auriez simplement écrit la méthode card_number= . J'espère que cet exemple est assez bon pour ne pas choisir directement l'option de rappel et de penser à la conception d'abord.

à propos de la citation de MongoId je me demande pourquoi ils conseillent de ne pas utiliser le rappel pour la logique du domaine mais de l'utiliser pour faire la queue pour le travail de fond. Je pense que faire la queue pour le travail de fond pourrait faire partie de la logique du domaine et pourrait parfois être mieux conçu avec autre chose qu'un rappel (disons un observateur).

enfin, il y a quelques critiques sur la façon dont ActiveRecord est utilisé / mis en œuvre avec Rail à partir d'un point de vue de conception de Programmation Orientée Objet, Ce réponse contiennent de bonnes informations à ce sujet et vous trouverez plus facilement. Vous pouvez également vérifier le datamapper design pattern / ruby implementation project qui pourrait être le remplacement (mais combien mieux) pour ActiveRecord et n'ont pas sa faiblesse.

2
répondu Adrien Coquio 2017-04-12 07:31:17

Je ne pense pas que la réponse soit trop compliquée.

si vous avez l'intention de construire un système avec un comportement déterministe, les callbacks qui traitent des choses liées aux données telles que la normalisation sont OK, les callbacks qui traitent de la logique des affaires telles que l'envoi d'e-mails de confirmation ne sont pas OK .

de la programmation orientée objet a été popularisé avec un comportement émergent comme une meilleure pratique 1 , et dans D'après mon expérience, Rail semble être d'accord. Beaucoup de gens, y compris le gars qui a introduit MVC , pensent que cela provoque des douleurs inutiles pour les applications où le comportement runtime est déterministe et bien connu à l'avance.

si vous êtes d'accord avec la pratique du comportement émergent OO, alors le modèle d'enregistrement actif du comportement de couplage à votre graphique d'objet de données n'est pas si important. Si (comme moi) vous voyez/avez ressenti la douleur de comprendre, déboguer et en modifiant de tels systèmes émergents, vous voudrez faire tout ce que vous pouvez pour rendre le comportement plus déterministe.

maintenant, comment concevoir des systèmes OO avec le juste équilibre de couplage lâche et le comportement déterministe? Si tu connais la réponse, écris un livre, je l'achèterai! DCI , Domain-driven design , et plus généralement les GOF patterns sont un début : -)


  1. http://www.artima.com/articles/dci_vision.html , " Où est-ce qu'on s'est trompés?". Ce n'est pas une source primaire, mais cela correspond à ma compréhension générale et à mon expérience subjective des suppositions dans la nature.
1
répondu Woahdae 2012-11-11 21:37:17