22

您认为将回调用于域逻辑的优缺点是什么?(我说的是 Rails 和/或 Ruby 项目的上下文。)

为了开始讨论,我想提一下Mongoid 页面关于回调的这句话:

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

我很想听听这一说法背后的论点或辩护。它是否仅适用于 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 的建议,这个逻辑会去哪里呢?

4

6 回答 6

32

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

before_save :ensure_values_are_calculated_correctly
before_save :down_case_titles
before_save :update_cache

立即清楚发生了什么。

我什至觉得这可以测试;我可以测试方法本身是否有效,并且可以分别测试每个回调。

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

回调

回调的优点:

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

回调的缺点:

  • 由于一切都在一个地方,因此很容易打破单一责任原则
  • 可以上重课
  • 如果一个回调失败会发生什么?它仍然遵循链条吗?提示:确保你的回调永远不会失败,否则将模型的状态设置为无效。

观察员

观察者的优势

  • 非常干净的代码,你可以为同一个类创建多个观察者,每个观察者做不同的事情
  • 观察者的执行不耦合

观察者的劣势

  • 起初,行为的触发方式可能很奇怪(看看观察者!)

结论

简而言之:

  • 对简单的模型相关的东西(计算值、默认值、验证)使用回调
  • 使用观察者进行更多的跨领域行为(例如发送邮件、传播状态……)

和往常一样:所有建议都必须持保留态度。但根据我的经验,观察者的扩展性非常好(而且鲜为人知)。

希望这可以帮助。

于 2012-06-19T23:48:02.937 回答
9

编辑:我在这里结合了一些人的建议的答案。

概括

基于一些阅读和思考,我得出了一些我认为的(暂定)陈述:

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

  2. “对域逻辑使用回调......可能会导致在链停止执行中的回调时难以调试的意外错误”这一说法是正确的。

  3. 是的,回调可能会导致影响其他对象的连锁反应。就无法测试的程度而言,这是一个问题。

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

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

  6. “仅将 [回调] 用于跨领域问题,例如排队后台作业”的建议很有趣,但被夸大了。(我回顾了横切关注点,看看我是否可能忽略了某些东西。)

我还想分享我对我读过的关于这个问题的博客文章的一些反应:

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

Mathias Meyer 2010 年的帖子ActiveRecord 的回调毁了我的生活,提供了一种观点。他写:

每当我开始向 Rails 应用程序中的模型添加验证和回调时 [...] 就感觉不对劲。感觉就像我添加了不应该存在的代码,这使一切变得更加复杂,并将显式代码转换为隐式代码。

我发现最后一个声明“将显式代码转换为隐式代码”是一个不公平的期望。我们在这里谈论的是Rails,对吧?!如此多的附加值是关于 Rails “神奇”地做事,例如,开发人员不必明确地做这件事。享受 Rails 的成果却批评隐式代码不是很奇怪吗?

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

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

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

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

所以,总而言之,我认为 Mathias 火上浇油,但我并不觉得所有这些都令人信服。

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

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

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

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

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

我非常感谢他通过告诉您何时适用和何时不适用来缓和他的建议:

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

于 2012-06-18T20:22:07.833 回答
2

这个问题就在这里(忽略 rspec 中的验证失败)是不将逻辑放在回调中的一个很好的理由:可测试性。

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

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

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

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

于 2012-06-14T18:30:43.180 回答
2

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(但要好多少)并且没有他的弱点。

于 2012-06-18T11:07:23.530 回答
2

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

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

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

于 2012-06-19T12:26:33.230 回答
1

我不认为答案太复杂。

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

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

如果您同意 OO 紧急行为的做法,那么将行为耦合到数据对象图的活动记录模式就没什么大不了的。如果(像我一样)你看到/感受到理解、调试和修改这些紧急系统的痛苦,你会想尽一切可能使行为更具确定性。

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


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