在Rails中使用域逻辑回调的优缺点

您认为使用域逻辑回调的优缺点是什么? (我在Rails和/或Ruby项目的上下文中讨论。)

为了开始讨论,我想在回调中的Mongoid页面中提到这个引用:

使用域逻辑的回调是一种糟糕的设计实践,并且当链中的回调暂停执行时,可能导致难以调试的意外错误。 我们建议只将它们用于跨领域的问题,例如排队后台工作。

我很想听听这个说法背后的争论或辩护。 它是否仅适用于Mongo支持的应用程序? 或者它旨在应用于数据库技术?

对于ActiveRecordvalidation和回调的Ruby on Rails指南似乎可能不同意,至少在涉及关系数据库时。 举个例子:

class Order  :paid_with_card? end 

在我看来,这是一个实现域逻辑的简单回调的完美示例。 它似乎快速有效。 如果我要接受Mongoid的建议,那么这个逻辑会转向哪里?

我真的很喜欢为小class使用回调。 我发现它使一个类非常易读,例如类似的东西

 before_save :ensure_values_are_calculated_correctly before_save :down_case_titles before_save :update_cache 

现在可以清楚地知道发生了什么。

我甚至觉得这个可以测试; 我可以测试方法本身是否有效,我可以单独测试每个回调。

我坚信类中的回调应该用于属于该类的方面。 如果你想在保存时触发事件,例如,如果对象处于特定状态时发送邮件,或者记录,我会使用观察者 。 这尊重单一责任原则。

回调

回调的优势:

  • 一切都在一个地方,所以这很容易
  • 非常易读的代码

回调的缺点:

  • 因为一切都是一个地方,所以很容易打破单一责任原则
  • 可以为重型课程
  • 如果一个回调失败会发生什么? 它仍然沿着链条? 提示:确保您的回调永不失败,或以其他方式将模型的状态设置为无效。

观察员

观察者的优势

  • 非常干净的代码,你可以为同一个类制作几个观察者,每个人都做不同的事情
  • 观察者的执行没有耦合

观察者的缺点

  • 起初,如何触发行为可能会很奇怪(请注意观察者!)

结论

简而言之:

  • 使用回调来处理简单的,与模型相关的东西(计算值,默认值,validation)
  • 使用观察者进行更多的交叉行为(例如发送邮件,传播国家……)

和往常一样:所有的建议都必须采取一些盐。 但根据我的经验,观察者的表现非常好(并且也鲜为人知)。

希望这可以帮助。

编辑:我已经将我的答案结合在一些人的建议上。

摘要

基于一些阅读和思考,我已经对我所相信的一些(暂定的)陈述:

  1. 语句“使用域逻辑的回调是一种糟糕的设计实践”是错误的,如所写的那样。 它夸大了这一点。 回调可以是适当使用的域逻辑的好地方。 问题不应该是域模型逻辑应该进入回调,它是什么样的域逻辑才有意义。

  2. 语句“使用域逻辑的回调…可能导致在链暂停执行中的回调”时为难以调试的意外错误。

  3. 是的,回调可能会导致影响其他对象的连锁反应。 在某种程度上,这是不可测试的,这是一个问题。

  4. 是的,您应该能够测试业务逻辑,而无需将对象保存到数据库。

  5. 如果一个对象的回调对你的敏感性而言过于膨胀,那么可以考虑其他设计,包括(a)观察者或(b)辅助类。 这些可以干净地处理多对象操作。

  6. “只使用[回调]来解决交叉问题,例如排队后台工作”的建议很有趣,但却被夸大了。 (我回顾了跨领域的问题 ,看看我是否可能会忽视某些事情。)

我还想分享一些我对博客文章的反应,我已经阅读了关于这个问题的讨论:

对“ActiveRecord的回调毁掉了我的生活”的反应

Mathias Meyer的2010年post, ActiveRecord的Callbacks毁了我的生活 ,提供了一个视角。 他写:

每当我开始在Rails应用程序中为模型添加validation和回调时[…]它只是感觉不对。 感觉就像我正在添加不应该存在的代码,这会使一切变得复杂得多,并且变成隐式代码。

我发现最后一个声明“明确变成隐含代码”是一种不公平的期望。 我们在这里谈论Rails ,对吧?! 如此大量的增值是关于Rails“神奇地”做事情,例如没有开发人员必须明确地做。 享受Rails的成果并批评隐含代码似乎并不奇怪吗?

仅根据对象的持久性状态运行的代码。

我同意这听起来很令人讨厌。

难以测试的代码,因为您需要保存对象以测试业务逻辑的各个部分。

是的,这使测试变得缓慢而困难。

因此,总而言之,我认为Mathias为火灾添加了一些有趣的燃料,尽管我并不觉得它们都引人注目。

对“疯狂,异端和令人敬畏:我编写Rails应用程序的方式”的反应

在James Golick的2010年post中, Crazy, Heretical 和Awesome:The Way I Write Rails Apps ,他写道:

此外,将所有业务逻辑耦合到持久性对象可能会产生奇怪的副作用。 在我们的应用程序中,当创建某些内容时,after_create回调会在日志中生成一个条目,用于生成活动源。 如果我想在没有记录的情况下创建对象,比如在控制台中,该怎么办? 我不能。 拯救和伐木永远结婚,永恒。

后来,他找到了它的根源:

解决方案实际上非常简单。 对问题的简化解释是我们违反了单一责任原则。 因此,我们将使用标准的面向对象技术来分离模型逻辑的关注点。

我真的很感激他通过告诉你何时适用以及什么时候没有:

事实是,在一个简单的应用程序中,肥胖的持久性对象可能永远不会受到伤害。 事情变得比CRUD操作复杂得多,这些东西开始堆积起来并成为痛点。

这里的这个问题( 忽略rspec中的validation失败 )是为什么不在回调中放置逻辑的一个很好的理由:可测试性。

您的代码可能会随着时间的推移而产生许多依赖关系, unless Rails.test?unless Rails.test?您开始添加unless Rails.test? 进入你的方法。

我建议只在before_validation回调中保留格式化逻辑,并将触及多个类的内容移动到Service对象中。

因此,在您的情况下,我会将normalize_card_number移动到before_validation,然后您可以validation卡号是否已标准化。

但是如果您需要在某处创建PaymentProfile,我会在另一个服务工作流对象中执行此操作:

 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 

然后,您可以轻松地测试某些条件,例如,如果它无效,是否未进行保存,或者支付网关是否引发exception。

在我看来,使用回调的最佳方案是,启动它的方法与回调本身中执行的操作无关。 例如,一个好的before_save :do_something不应该执行与保存相关的代码。 这更像是观察者应该如何运作。

人们倾向于仅使用回调来干掉他们的代码。 它并不坏,但可能会导致复杂且难以维护的代码,因为如果您没有注意到调用了回调,则读取save方法并不会告诉您它所做的一切。 我认为这对显式代码很重要(特别是在Ruby和Rails中,会发生如此多的魔术)。

保存相关的所有内容都应该在save方法中。 例如,如果回调是为了确保用户已经过身份validation,这与保存无关,那么这是一个很好的回调场景。

Avdi Grimm在他的书“ Object On Rails”中有一些很好的例子。

你会在这里和这里找到为什么他不选择回调选项以及如何通过重写相应的ActiveRecord方法来摆脱这个问题。

在你的情况下,你最终会得到类似的东西:

 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 

[评论后更新“这仍是回调”]

当我们谈到域逻辑的回调时,我理解ActiveRecord回调,如果你认为Mongoid引用其他东西,如果在某个地方我找不到它的“回调设计”,请纠正我。

我认为ActiveRecord回调是,对于大多数(整个?)部分而言,只不过我之前的例子你可以摆脱的语法糖。

首先,我同意这个回调方法隐藏了它们背后的逻辑:对于不熟悉ActiveRecord ,他必须学习它来理解代码,使用上面的版本,它很容易理解和可测试。

ActiveRecord回调他们的“常用用法”或者他们可以产生的“脱钩感”,这可能是最糟糕的。 回调版本起初可能看起来不错,但是当你添加更多回调时,理解你的代码会更加困难(加载它们的顺序,可能会停止执行流程等等)并测试它(您的域逻辑与ActiveRecord持久性逻辑相结合)。

当我阅读下面的例子时,我对这段代码感到很难过,这很有气味。 我相信如果您正在使用TDD / BDD,您可能最终不会使用此代码,如果您忘记了ActiveRecord ,我认为您只需编写card_number=方法。 我希望这个例子足够好,不能直接选择回调选项并首先考虑设计。

关于MongoId的引用我想知道为什么他们建议不使用域逻辑的回调,而是用它来排队后台工作。 我认为排队后台工作可能是域逻辑的一部分,有时可能更好地设计其他东西而不是回调(让我们说一个观察者)。

最后,从面向对象的编程设计的角度来看,对于如何使用Rail实现ActiveRecord有一些批评,这个答案包含有关它的良好信息,你会发现更容易。 您可能还想检查datamapper 设计模式 / ruby实现项目 ,这可能是ActiveRecord的替代(但有多好)并且没有他的弱点。

我不认为答案太复杂了。

如果您打算构建具有确定性行为的系统,那么处理与数据相关的事情(例如规范化)的回调就可以了,处理业务逻辑的回调(如发送确认电子邮件)也不行

OOP以紧急行为作为最佳实践1推广,根据我的经验,Rails似乎同意。 许多人, 包括引入MVC的人 ,认为这会给运行时行为具有确定性并且提前众所周知的应用程序带来不必要的痛苦。

如果您同意OO紧急行为的实践,那么与数据对象图形耦合行为的主动记录模式就不那么重要了。 如果(像我一样)你看到/感受到理解,调试和修改这些紧急系统的痛苦,你将希望尽一切可能使行为更具确定性。

现在,如何设计具有松散耦合和确定性行为的正确平衡的OO系统? 如果你知道答案,写一本书,我会买的! DCI , 域驱动设计 ,以及更普遍的GoF模式是一个开始:-)


  1. http://www.artima.com/articles/dci_vision.html ,“我们哪里出错?”。 不是主要来源,但与我对野外假设的一般理解和主观经验一致。