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

时间:2012-06-14 18:18:25

标签: ruby-on-rails mongoid

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

为了开始讨论,我想提一下Mongoid page on callbacks

中的这句话
  

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

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

似乎The Ruby on Rails Guide to ActiveRecord Validations and Callbacks可能不同意,至少在涉及关系数据库时。举个例子:

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

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

6 个答案:

答案 0 :(得分:28)

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

之类的东西
before_save :ensure_values_are_calculated_correctly
before_save :down_case_titles
before_save :update_cache

立即清楚发生了什么。

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

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

回调

回调的优势:

  • 一切都在一个地方,这样可以轻松实现
  • 非常易读的代码

回调的缺点:

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

观察

观察员的优势

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

观察员的缺点

  • 起初,行为被触发可能会很奇怪(在观察者看来!)

结论

简而言之:

  • 对简单的,与模型相关的东西(计算值,默认值,验证)使用回调
  • 使用观察员进行更多交叉行为(例如发送邮件,传播州,......)

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

希望这有帮助。

答案 1 :(得分:9)

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

摘要

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

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

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

  3. 是的,回调可能会导致影响其他对象的连锁反应。如果这是不可测试的,这是一个问题。

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

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

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

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

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

    Mathias Meyer的2010年帖子ActiveRecord's Callbacks Ruined My Life提供了一个视角。他写道:

      

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

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

      

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

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

      

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

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

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

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

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

      

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

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

      

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

    我真的很感激他通过告诉你什么时候适用它以及什么时候不适用来缓和他的建议:

      

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

答案 2 :(得分:2)

这里的这个问题(Ignore the validation failures in rspec)是为什么不在你的回调中加入逻辑的一个很好的理由:可测试性。

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

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

因此,在您的情况下,我会将normalize_card_number移动到before_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

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

答案 3 :(得分:2)

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

你会发现herehere为什么他不选择回调选项以及如何通过覆盖相应的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

[评论后更新&#34;这仍然是回调&#34;]

当我们谈到域逻辑的回调时,我理解ActiveRecord回调,如果你认为Mongoid引用其他内容,如果有一个&#34;回调设计&#34;那么请纠正我。在某个地方,我没有找到它。

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

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

ActiveRecord回调他的&#34;常用用法&#34;或者&#34;脱钩的感觉&#34;他们可以生产。回调版本起初可能看起来不错,但是当您添加更多回调时,将更难理解您的代码(加载它们的顺序,哪些可能会停止执行流程等等)并对其进行测试(您的域逻辑与ActiveRecord持久性逻辑相结合。

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

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

最后,从面向对象的编程设计的角度来看,对于如何使用Rail实现ActiveRecord有一些批评,这个answer包含有关它的良好信息,你会发现更容易。您可能还想检查数据映射器design pattern / ruby implementation project,这可能是ActiveRecord的替代(但更好)并且没有他的弱点。

答案 4 :(得分:2)

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

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

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

答案 5 :(得分:1)

我认为答案太复杂了。

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

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

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

现在,如何设计具有松散耦合和确定性行为的正确平衡的OO系统?如果你知道答案,写一本书,我会买的! including the guy who introduced MVCDCI,更常见的是Domain-driven design是一个开头: - )


  1. GoF patterns,“我们哪里出错?”。不是主要来源,但与我对野外假设的一般理解和主观经验一致。