带有实例变量的模块可能被认为是有害的

背景

Rails鼓励人们在任何地方使用模块和实例变量。例如,在控制器、助手和视图中使用实例变量。他们还鼓励使用ActiveSupport这样::关注,这进一步强化了将所有内容保存在一个巨大的单个对象中的想法,人们可以访问这个巨大对象中的所有内容。

的问题

当然,这是很方便开发的,因为我们把所有东西都放在触手可及的地方。然而,当选择的对象正在增长时,这有许多缺点,它稍后会因为同样的原因而失去控制。

在相同的环境中有太多的东西,我们不知道这些东西是否紧密耦合,是否相互依赖。很难判断复杂性何时增长到一定程度,这也使得跟踪代码变得极其困难。例如,一个类可以使用3个不同的实例变量,并且可以从3个不同的模块对它们进行初始化和操作。当这些变量开始给我们带来麻烦时很难追踪。我们不知道哪个模块会突然改变其中一个变量。任何东西都可以接触任何东西。

类似的担忧

人们说多重继承不好。混合多个模块和分散在各处的多个实例变量也会遇到同样的问题。这同样适用于ActiveSupport这样::关注。看到的:考虑用专用类和组合代替关注点

还有一个类似的想法:使用装饰器和接口隔离来解决模型过度增长的问题

请注意,包括并不能解决整个问题。它们定义了依赖关系,但它们仍然允许每个模块通过最终巨型对象中的实例变量进行隐式通信,这就是问题所在。

解决方案

我们应该把这个巨大的对象分割成多个对象,它们通过API(即公共方法)相互通信。简而言之,组合优于继承。这样,每个较小的对象都有各自的有限状态,即实例变量。如果一个实例变量出了问题,我们会很清楚它是来自那个小对象,因为没有其他人可以接触它。

有了明确定义的API,这将使事情耦合度更低,更容易调试和跟踪,并且更易于扩展以供其他对象使用,因为它们以清晰的方式通信,而不是隐式依赖。

可接受的使用

然而,在模块中使用实例变量并不总是坏事,只要它包含在同一个模块中;也就是说,没有其他模块或对象接触它们,那么它将是一个可接受的使用。

我们特别允许使用单个实例变量的情况| | =设置值。这看起来像:

模块deff@f| | =真正的结束结束

不幸的是,将更复杂的规则编码到cop中并不容易,因此我们依赖于人们的最佳判断。如果我们能找到另一个好的模式,我们可以很容易地添加到警察,我们应该这样做。

如何重写并避免禁用这个cop

即使我们能让警察瘫痪,我们也应该避免这样做。一些代码可以很容易地以简单的形式重写。考虑这个可接受的方法:

模块Gitlab模块Emojidefemoji_unicode_version(名字@emoji_unicode_versions_by_name| | =JSON解析(文件(Rails加入(“设备”“emojis”“emoji-unicode-version-map.json”)))@emoji_unicode_versions_by_name名字结束结束结束

这个方法完全没问题,因为它已经是独立的了。不应使用其他方法@emoji_unicode_versions_by_name我们很好。但这还是冒犯了警察,因为这不是| | =警察还不够聪明,不会判断这是好事。

另一方面,我们可以把这个方法分成两个部分:

模块Gitlab模块Emojidefemoji_unicode_version(名字emoji_unicode_versions_by_name名字结束私人defemoji_unicode_versions_by_name@emoji_unicode_versions_by_name| | =JSON解析(文件(Rails加入(“设备”“emojis”“emoji-unicode-version-map.json”)))结束结束结束

现在警察不会抱怨了。这里有一个不好的例子,我们可以重写一下:

模块SpamCheckServicedeffilter_spam_check_params@request参数个数删除(:请求@api参数个数删除(: api@recaptcha_verified参数个数删除(: recaptcha_verified@spam_log_id参数个数删除(: spam_log_id结束defspam_check(spammable用户spam_serviceSpamService(spammable@requestspam_servicewhen_recaptcha_verified(@recaptcha_verified@api用户spam_logsfind_by(id:@spam_log_id&更新!(recaptcha_verified:真正的结束结束结束

这里有几个隐式依赖项。首先,参数个数应在使用前定义。第二,filter_spam_check_params应该提前打电话spam_check。这些都是隐式的,包含程序可以在不知情的情况下使用这些实例变量。

这应该重写为:

SpamCheckServicedef初始化(请求:,api:,recaptcha_verified:,spam_log_id:)@request请求@apiapi@recaptcha_verifiedrecaptcha_verified@spam_log_idspam_log_id结束defspam_check(spammable用户spam_serviceSpamService(spammable@requestspam_servicewhen_recaptcha_verified(@recaptcha_verified@api用户spam_logsfind_by(id:@spam_log_id&更新!(recaptcha_verified:真正的结束结束结束

然后这样使用:

UpdateSnippetService<BaseServicedef执行#……垃圾邮件SpamCheckService(参数个数片!(:请求: api: recaptcha_verified: spam_log_id))垃圾邮件检查(片段current_user#……结束结束

这样,所有的实例变量都被隔离在SpamCheckService而不是包含模块的任何东西,以及那些也被包含的模块,这样更容易跟踪任何问题,并减少出现名称冲突的机会。

如何使这个警察失效

将禁用注释放在同一行代码的后面:

模块defviolating_method@f+@g禁用Gitlab/ modulewithinstancvariables结束结束

如果有多行,你也可以为一个节启用和禁用:

模块禁用Gitlab/ modulewithinstancvariablesdefviolating_method@f0@g1@h2结束# rubocop:启用Gitlab/ modulewithinstancvariables结束

注意,您需要在某个时候启用它,否则下面的所有内容都不会被选中。

我们现在可能需要忽略的事情

由于Rails帮助程序和邮件程序的工作方式,我们可能无法避免在那里使用实例变量。对于这些情况,我们可以暂时忽略它们。至少我们不会与其他随机对象共享这些模块,所以它们在某种程度上仍然是孤立的。

视图中的实例变量

它们很糟糕,因为我们不能轻易地分辨谁在使用实例变量(从控制器的角度来看)以及我们在哪里设置它们(从局部的角度来看),这使得跟踪数据依赖变得极其困难。

我们试着用这样的东西来代替:

渲染“项目/提交/提交”提交:提交裁判:裁判项目:项目

在部分中:

-裁判local_assigns获取(:裁判-提交local_assigns获取(:提交-项目local_assigns获取(:项目

通过这种方式,可以更清楚地看到这些值来自何处,并且可以在使用实例变量时进行错别字检查。将来,我们还应该禁止在局部中使用实例变量。

Baidu
map