如果你曾经使用过Ruby on Rails,你可能已经接触过关注点的概念。每当你启动一个新的Rails项目时,你会得到一个目录app/controllers/concerns 和app/models/concerns 。但什么是关注点?为什么Rails社区的人有时会对它们恶语相向?
快速概述
一个Rails关注点是任何扩展了ActiveSupport::Concern 模块的模块。你可能会问--关注点与模块有什么不同?主要的区别在于,Rails关注点允许你做一些神奇的事情,就像这样:
# app/models/concerns/trashable.rb
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
你看这个词包括了。它是Rails的一点碳水化合物,洒在Ruby模块上。ActiveSupport::Concern 为你做的是,它允许你把你想评估的代码放在包含块内。例如,你想从你的模型中提取垃圾处理逻辑。included 允许你做我们所做的,以后像这样包含你的模型的关注点:
class Song < ApplicationRecord
include Trashable
has_many :authors
# ...
end
在这一点上相当方便和天真,对吗?模型失去了一点重量,垃圾处理现在可以在其他的模型中重复使用,而不仅仅是我们的宋氏模型。好吧,事情会变得很复杂。让我们深入了解一下。
一个混合器的经典例子
在我们进一步踏入关注的深处之前,让我们对它们再加一个解释。当你看到include SomeModule 或extend AnotherModule ,这些都被称为mixins。mixin是一组可以被添加到其他类中的代码。而且,我们都从Ruby文档中知道,一个模块是一个方法和常量的集合。所以我们在这里所做的是把带有方法和常量的模块纳入不同的类中,这样它们就可以使用它们。
这正是我们对Trashable 的关注所做的。我们把围绕着捣毁一个模型对象的普通逻辑提取到一个模块中。这个模块后来可以被包含在其他地方。所以,mixin是一种设计模式,不仅在Ruby和Rails中使用。但是,无论在哪里使用,人们要么喜欢它,认为它很好,要么讨厌它,认为它很容易失去控制。
为了更好地理解这一点,我们将通过几个使用它们的优点和缺点。希望通过这样做,我们可以对何时或是否使用关注点有一个了解。
我拥有这一切
当你决定将一些东西提取到一个关注点中时,比如Trashable 关注点,你现在可以访问所有包含Trashable 的功能。这带来了巨大的力量,但正如Richard Schneeman在他关于这个主题的博文中所说的那样--"有了巨大的力量,就有了制造复杂代码的巨大能力"。他的意思是使你可能依赖的代码复杂化,这些东西应该在你的关注中存在。
如果我们再看一下Trashable :
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
关心的逻辑依赖于这样一个事实:trashed 字段存在于关心所包含的地方。对吗?没什么大不了的,这毕竟是我们想要的。但是,我所看到的情况是,人们被诱惑着把模型中的其他东西拉到关注中。为了说明这种情况是如何发生的,让我们想象一下,Song 模型有另一个方法featured_authors :
class Song < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true)
end
# ...
end
class Album < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true)
end
# ...
end
为了更好地说明问题,我添加了一个Album 模型,其中也包括Trashable 。然后让我们说,我们想在歌曲和专辑被捣毁时通知特色作者。人们会受到诱惑,像这样把这个逻辑放在关注点里面:
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
notify(featured_authors)
end
def notify(authors)
# ...
end
end
就在这里,事情开始变得有点复杂了。由于我们的垃圾处理逻辑在我们的歌曲模型之外,我们可能会被诱惑把通知放在Trashable concern中。在这里,有一些 "错误 "发生了。featured_authors 是从Song 模型中提取的。好吧,让我们假设这通过了拉动请求审查和CI检查。
然后,在几个月后,一个新的需求被设定,开发人员需要改变我们为歌曲呈现featured_authors 的方式。例如,一个新的需求希望只显示来自欧洲的特色作者。当然,开发人员会找到定义特色作者的地方并编辑它们:
class Song < ApplicationRecord
include Trashable
has_many :authors
def featured_authors
authors.where(featured: true).where(region: 'Europe')
end
# ...
end
class Album < ApplicationRecord
include Trashable
has_many :authors
# ...
end
这在我们显示作者的地方很好用,但在我们部署到生产中后,世界上其他地方的人就不会再得到关于他们的歌曲的通知了。在使用关注点时,像这样的错误很容易犯。上面的例子是一个简单的人为的例子,但是 "在野外 "的例子可能是超级棘手的。
这里有风险的是,关注点(mixin)知道很多关于它被包含在模型中的情况。这就是所谓的循环依赖。Song 和Album 依赖于Trashable 进行垃圾处理,Trashable 依赖于它们两个进行featured_authors 的定义。同样可以说的是,一个trashed 字段需要存在于两个模型中,以便让Trashable 关心工作。
这就是为什么不关注的俱乐部可能会反对,而支持关注的俱乐部会支持。我想说的是,在我的代码库中,Trashable 的第一个版本是我要用的。让我们看看如何使第二个版本的通知变得更好。
你们从哪里来
回头看看我们的Trashable ,我们必须做点什么。使用关注点时发生的另一件事是,我们倾向于过度干燥。让我们试着这样做,为了演示的目的,在我们现有的模型上创建另一个关注点(在这个问题上请耐心等待):
module Authorable
has_many :authors
def featured_authors
authors.where(featured: true)
end
end
然后,我们的Song 和Album 将看起来像这样:
class Song < ApplicationRecord
include Trashable
include Authorable
# ...
end
class Album < ApplicationRecord
include Trashable
include Authorable
# ...
end
我们把一切都干了,但现在对来自欧洲的特色作者的要求没有得到满足。更糟糕的是,现在Trashable 和模型都依赖于Authorable 。这到底是怎么回事?这正是我前段时间在处理关注问题时的疑问。很难追踪到方法的来源。
我对这一切的解决方案是尽可能地让featured_authors 与模型接近。notify 方法根本就不应该是Trashable 关注的一部分。每个模型都应该自己处理这个问题,特别是如果他们倾向于通知不同的子群。让我们看看如何不那么痛苦地做:
# Concerns
module Trashable
extend ActiveSupport::Concern
included do
scope :existing, -> { where(trashed: false) }
scope :trashed, -> { where(trashed: true) }
end
def trash
update_attribute :trashed, true
end
end
module Authorable
has_many :authors
# Other useful methods that relate to authors across models.
# If there are none, ditch the concern.
end
# Models
class Song < ApplicationRecord
include Trashable
include Authorable
def featured_authors
authors.where(featured: true).where(region: 'Europe')
end
# ...
end
class Album < ApplicationRecord
include Trashable
include Authorable
def featured_authors
authors.where(featured: true)
end
# ...
end
像这样的关注是可以管理的,而且不会太复杂。我跳过了我前面描述的notify 功能,因为这可以是另一个话题。
最后的老板
对于Rails的创建者Basecamp来说,引用其他关注点的关注点似乎完全没有问题,正如DHH在不久前的推文中所说明的那样:
通过看代码截图,你要么张大嘴巴敬畏,要么惊恐。我觉得这里没有中间地带。如果我有机会编辑这段代码,我会把它设想成 "最终关注的Boss Fight"。但撇开笑话不谈,有趣的是,这里有一些评论说,哪个关注点取决于哪个。请看一下:
# ...
include Subscribable # Depends on Readable
include Eventable # Depends on Recordables
# ...
放置这样的注释可能是有帮助的,但它仍然是为做一些粗略的事情而设置的,特别是如果你是代码库的新手。作为一个新手,如果不知道代码中的所有 "问题",肯定会让你陷入担忧的漩涡中。
像这样的事情是DHH在讨论中的一个评论中分享的。里面的一条回应推文问道,与这个代码库一起工作的人应该如何与这样的问题互动。DHH回应说,他们没有太多的书面文档,他们很少雇佣,所以他们的团队对这些很熟悉。
但是把一个对代码库非常熟悉的有经验的团队作为使用他们的论据是很奇怪的,而且也不强。我想这更像是一种感觉,是否要使用它们。你是更喜欢模块提供的多重继承,还是更喜欢组合?你的决定。
总结
正如我们所看到的,关注点只不过是提供一些有用的语法糖的模块,以提取和干化你的代码。如果你已经有了更多有用的工具,也许你就不应该马上接触关注点了。像处理文件附件和我们在例子中展示的垃圾处理逻辑这样的行为,可能是提取到模块(关注点)的好人选。
希望你能看到在处理关注点和模块时可能出现的好与坏。请记住,没有代码是完美的。最后,如果你不去尝试并可能失败或成功,你怎么能了解什么是好的,什么是坏的?
没有任何解决方案是完美的,我希望你能在博文中了解到Rails关注的做事方式。像往常一样,使用你的判断力并注意利弊。