主题

  • 小心
  • 抵抗性
  • 大班/神班
  • 提取类
  • 长方法
  • 长参数列表

小心

以下简短文章系列适用于经验不足的Ruby开发人员和初学者。 我给人的印象是,代码的气味及其重构对新手来说可能是令人生畏的,并且会使其感到恐惧–尤其是如果他们没有幸运的职位,而他们的导师可以将神秘的编程概念变成闪亮的灯泡。

显然,我本人已经穿上了这些鞋子,我想起了进入代码的味道和重构的感觉是不必要的迷雾。

一方面,作者期望一定水平的熟练程度,因此可能不会觉得自己像超级新手可能需要尽快舒适地进入这个世界那样向读者提供相同数量的上下文。

结果,也许,另一方面,新手会形成一种印象,即他们应该等待更长的时间,直到他们更高级地了解气味和重构。 我不同意这种方法,并认为使该主题更易于理解将有助于他们在职业生涯早期设计更好的软件。 至少我希望这有助于为初学者提供扎实的起点。

那么,当人们提到代码气味时,我们到底在说什么呢? 在您的代码中总是有问题吗? 不必要! 您能完全避免它们吗? 我不这么认为! 您是说代码异味导致代码破损吗? 好吧,有时有时不是。 立即修复它们是我的优先事项吗? 我担心,答案还是一样的:有时候是的,有时候你当然应该先炸更大的鱼。 你疯了? 此时的问题还算公平!

在您继续从事整个有臭味的业务之前,请记住从所有这一切中拿走一件事:不要试图解决遇到的每种气味-这无疑是在浪费时间!

在我看来,代码气味很难包裹在一个贴有标签的盒子中。 各种各样的气味都有不同的解决方案。 另外,不同的编程语言和框架容易产生不同的气味,但是它们之间肯定有很多常见的“遗传”因素。 我试图描述代码气味的方法是将它们与医疗症状进行比较,以告知您可能有问题。 他们可以指出各种潜在的问题,并且如果得到诊断,可以提供多种解决方案。

值得庆幸的是,它们总体上并不像处理人体和心理那样复杂。 但是,这是一个公平的比较,因为其中一些症状需要立即得到治疗,而另一些则给您充足的时间来提出最适合“患者”总体健康的解决方案。 如果您具有有效的代码并且遇到了麻烦,那么您就必须做出艰难的决定,即是否值得花时间找到修复程序,以及这种重构是否可以改善应用程序的稳定性。

话虽如此,如果您偶然发现可以立即进行改进的代码,建议您将代码留得比以前好一些-甚至随着时间的流逝,积蓄一点点也是很好的建议。

抵抗性

如果添加新代码变得更加困难(例如,决定将新代码放置在何处或在整个代码库中引起很多连锁反应),则代码的质量将变得可疑。 这称为抵抗。

作为代码质量的准则,您可以始终通过引入更改的容易程度来对其进行度量。 如果这变得越来越困难,那么绝对是时候进行重构, 以后再认真对待red-green-REFACTOR的最后一部分了。

大班/神班

让我们从听起来很奇特的“神类”开始,因为我认为它们对于初学者来说特别容易掌握。 上帝阶级是一种叫做大阶级的特殊的气味。 在本节中,我将同时解决这两个问题。 如果您花了一点时间在Rails土地上,那么您可能经常看到它们,以至于它们对您来说看起来很正常。

您肯定还记得“胖模特,瘦控制器”的口头禅吗? 好吧,实际上,瘦对所有这些班级都有好处,但作为指导原则,我认为这对新手来说是不错的建议。

上帝阶级是吸引各种知识和行为的对象,就像黑洞一样。 您通常怀疑的对象通常包括用户模型以及您的应用试图解决的任何问题(希望如此!)-至少是首要的。 Todo应用程序可能会在Todos型号上大量使用, Products上的购物应用程序, Photos上的照片应用程序可能会大量增加—您会感到不解。

人们称他们为神课是因为他们了解太多。 它们与其他类的联系过多-主要是因为有人在懒惰地对它们建模。 但是,要控制神职人员是艰苦的工作。 它们使将更多的责任转嫁给他们变得非常容易,而且正如许多希腊英雄所证明的那样,划分和征服“神”需要一些技巧。

他们的问题在于,他们变得越来越难理解,尤其是对于新的团队成员而言,很难改变,并且随着他们积累的重力越来越多,重用它们的选择越来越少。 哦,是的,您是对的,您的测试也不必要地难以编写。 简而言之,开设大型班级,尤其是上帝班级并没有真正的好处。

有几种常见的症状/体征,表明您的班级需要一些英雄主义/手术:

  • 您需要滚动!
  • 大量的私人方法?
  • 您的课程上是否有七个或更多方法?
  • 简而言之,很难说出您的班级实际上是做什么的!
  • 当您的代码演变时,您的班级有很多理由要更改吗?

另外,如果您在课堂上着眼睛,想想“嗯? 哇!” 您可能也会遇到一些麻烦。 如果听起来一切都很熟悉,那么您很有可能会发现自己是个好标本。

class CastingInviterEMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/attr_reader :message, :invitees, :castingdef initialize(attributes = {})@message  = attributes[:message]  || ''@invitees = attributes[:invitees] || ''@sender   = attributes[:sender]@casting  = attributes[:casting]enddef valid?valid_message? && valid_invitees?enddef deliverif valid?invitee_list.each do |email|invitation = create_invitation(email)Mailer.invitation_notification(invitation, @message)endelsefailure_message  = "Your #{ @casting } message couldn’t be sent. Invitees emails or message are invalid"invitation = create_invitation(@sender)Mailer.invitation_notification(invitation, failure_message )endendprivatedef invalid_invitees@invalid_invitees ||= invitee_list.map do |item|unless item.match(EMAIL_REGEX)itemendend.compactenddef invitee_list@invitee_list ||= @invitees.gsub(/\s+/, '').split(/[\n,;]+/)enddef valid_message?@message.present?enddef valid_invitees?invalid_invitees.empty?enddef create_invitation(email)Invitation.create(casting:       @casting,sender:        @sender,invitee_email: email,status:        'pending')end
end

丑家伙,是吗? 你能看到这里捆绑了多少东西吗? 当然我在上面放了一点樱桃,但是您迟早会遇到这样的代码。 让我们考虑一下CastingInviter类必须承担的责任。

  • 传送电子邮件
  • 检查有效的消息和电子邮件地址
  • 摆脱空白
  • 在逗号和分号上拆分电子邮件地址

如果这一切对刚刚想传递通过铸造调用类倾倒deliver ? 当然不是! 如果您的邀请方式发生变化,您可能会遇到shot弹枪手术 。 CastingInviter不需要了解大多数这些详细信息。 这是某些专门处理电子邮件相关内容的类的责任。 将来,您还将在这里找到许多更改代码的原因。

提取类

那么我们应该如何处理呢? 通常,提取类是一种方便的重构模式,可以将其呈现为解决诸如大而复杂的类之类的问题的合理解决方案,尤其是当所讨论的类处理多个职责时。

私有方法通常是不错的选择,而且也很容易标记。 有时,您需要从这样一个坏男孩身上提取甚至更多的课程,只是不要一step而就。 一旦找到足够多的连贯肉,似乎属于它自己的专用对象,就可以将该功能提取到新类中。

您将创建一个新类,并逐步将功能逐个移动。 分别移动每个方法,如果有理由请重命名它们。 然后在原始类中引用新类,并委派所需的功能。 好东西具有测试覆盖范围(希望如此!),它使您可以检查过程中的每个步骤是否仍然正常工作。 旨在也能够重用您提取的类。 看到它是如何完成的比较容易,所以让我们阅读一些代码:

class CastingInviterattr_reader :message, :invitees, :castingdef initialize(attributes = {})@message  = attributes[:message]  || ''@invitees = attributes[:invitees] || ''@casting  = attributes[:casting]@sender   = attributes[:sender]enddef valid?casting_email_handler.valid?enddef delivercasting_email_handler.deliverendprivatedef casting_email_handler@casting_email_handler ||= CastingEmailHandler.new(message:  message, invitees: invitees, casting:  casting, sender:   @sender)end
end
class CastingEmailHandler EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/def initialize(attr = {})@message  = attr[:message]  || ''@invitees = attr[:invitees] || ''@casting  = attr[:casting]@sender   = attr[:sender]enddef valid?valid_message? && valid_invitees?enddef deliverif valid?invitee_list.each do |email|invitation = create_invitation(email)Mailer.invitation_notification(invitation, @message)endelsefailure_message  = "Your #{ @casting } message couldn’t be sent. Invitees emails or message are invalid"invitation = create_invitation(@sender)Mailer.invitation_notification(invitation, failure_message )endendprivatedef invalid_invitees@invalid_invitees ||= invitee_list.map do |item|unless item.match(EMAIL_REGEX)itemendend.compactenddef invitee_list@invitee_list ||= @invitees.gsub(/\s+/, '').split(/[\n,;]+/)enddef valid_invitees?invalid_invitees.empty?enddef valid_message?@message.present?enddef create_invitation(email)Invitation.create(casting:       @casting,sender:        @sender,invitee_email: email,status:        'pending')end
end

在此解决方案中,您不仅会看到这种关注点分离如何影响您的代码质量,而且阅读效果也会更好,并且更容易消化。

在这里,我们将方法委托给一个新类,该类专门处理通过电子邮件传递这些邀请。 您可以在一个专用的地方检查邮件和被邀请者是否有效以及如何传递它们。 CastingInviter不需要了解这些详细信息,因此我们将这些职责委托给新的CastingEmailHandler类。

现在,有关如何发送和检查这些强制邀请电子邮件的有效性的知识全部包含在我们的新提取类中。 现在有更多代码吗? 你打赌! 分开关注是否值得? 很确定! 我们可以超越这个范围并重构CastingEmailHandler吗? 绝对! 把自己打昏!

如果您想知道valid? CastingEmailHandlerCastingInviter上的方法,此方法用于RSpec创建自定义匹配器。 这使我可以编写如下内容:

expect(casting_inviter).to be_valid

我认为非常方便。

有更多处理大型类/上帝对象的技术,在本系列教程中,您将学到几种重构此类对象的方法。

处理这些案例并没有固定的处方,它总是取决于情况,如果您需要携带大笔武器,或者如果较小的增量式重构技术最能满足您的需求,那么这是一个个案判断。 我知道,有时候有些沮丧。 不过,遵循“ 单一责任原则” (SRP)将会走很长一段路,并且是可以遵循的一个很好的建议。

长方法

具有一些大方法是开发人员遇到的最常见的事情之一。 通常,您希望一眼就知道一种方法应该做什么。 它也应该仅具有一层嵌套或一层抽象。 简而言之,避免编写复杂的方法。

我知道这听起来很困难,而且经常如此。 经常出现的解决方案是将方法的一部分提取到一个或多个新函数中。 这种重构技术称为提取方法 -这是最简单但非常有效的方法之一。 作为一个很好的副作用,如果适当地命名方法,则代码将更易读。

让我们看一下功能规范,在该规范中您将非常需要此技术。 我记得在编写此类功能规范时被介绍给extract方法 ,以及灯泡点亮时的感觉如何。 由于这样的功能规格易于理解,因此很适合演示。 另外,编写规范时,您将一再遇到类似的情况。

规格/功能/some_feature_spec.rb

require 'rails_helper'feature 'M marks mission as complete' doscenario 'successfully' dovisit_root_pathfill_in      'Email', with: 'M@mi6.com'click_button 'Submit'visit missions_pathclick_on     'Create Mission' fill_in      'Mission Name', with: 'Project Moonraker'click_button 'Submit'within "li:contains('Project Moonraker')" doclick_on 'Mission completed'endexpect(page).to have_css 'ul.missions li.mission-name.completed', text: 'Project Moonraker'end
end

如您所见,这种情况下发生了很多事情。 您转到索引页面,登录并创建设置任务,然后通过将任务标记为完成进行练习,最后验证行为。 没有火箭科学,但也不干净,而且绝对没有可重用性。 我们可以做得更好:

规格/功能/some_feature_spec.rb

require 'rails_helper'feature 'M marks mission as complete' doscenario 'successfully' dosign_in_as 'M@mi6.com'create_classified_mission_named 'Project Moonraker'mark_mission_as_complete        'Project Moonraker'agent_sees_completed_mission    'Project Moonraker'end
enddef create_classified_mission_named(mission_name)visit missions_pathclick_on     'Create Mission' fill_in      'Mission Name', with: mission_nameclick_button 'Submit'enddef mark_mission_as_complete(mission_name)within "li:contains('#{mission_name}')" doclick_on 'Mission completed'end
enddef agent_sees_completed_mission(mission_name)expect(page).to have_css 'ul.missions li.mission-name.completed', text: mission_name
enddef sign_in_as(email)visit root_pathfill_in      'Email', with: emailclick_button 'Submit'
end

在这里,我们提取了四种可以在其他测试中轻松重用的方法。 我希望很明显,我们用一块石头击中了三只鸟。 该功能更加简洁,阅读效果更好,并且由提取的组件组成,没有重复。

假设您编写了各种类似的场景而没有提取这些方法,并且想更改一些实现。 现在,您希望花时间重构测试,并在一个中心位置应用所做的更改。

当然,还有一种更好的方法来处理这样的功能规范,例如Page Objects,但这不是我们今天的工作范围。 我想这就是您需要了解的有关提取方法的全部信息。 您可以在代码中的任何地方应用这种重构模式,当然不仅限于规范。 就使用频率而言,我猜测这将是提高代码质量的第一技术。 玩得开心!

长参数列表

让我们以如何精简参数的示例结束本文。 当您必须向您的方法提供一个或两个以上的参数时,它变得非常繁琐。 而是放一个对象不是很好吗? 如果您引入参数object,那正是您可以做的。

所有这些参数不仅给编写和保持顺序带来麻烦,而且还可能导致代码重复-我们当然希望尽可能避免这种情况。 我特别喜欢这种重构技术的原因是它如何影响内部的其他方法。 您通常可以摆脱食物链中大量的参数垃圾。

让我们来看这个简单的例子。 M可以分配一个新任务,并需要一个任务名称,一个代理和一个目标。 M还能切换特工的双0状态,这意味着他们可以杀死对方。

class Mdef assign_new_mission(mission_name, agent_name, objective, licence_to_kill: nil)print "Mission #{mission_name} has been assigned to #{agent_name} with the objective to #{objective}."if licence_to_killprint " The licence to kill has been granted."elseprint "The licence to kill has not been granted."endend
endm = M.new
m.assign_new_mission('Octopussy', 'James Bond', 'find the nuclear device', licence_to_kill: true)
# => Mission Octopussy has been assigned to James Bond with the objective to find the nuclear device. The licence to kill has been granted.

当您查看此问题并询问任务“参数”变得越来越复杂时会发生什么时,您已经开始有所了解。 只有在传递具有所需全部信息的单个对象时,您才能解决这一难题。 通常,如果参数对象由于某种原因发生更改,这还可以帮助您避免更改方法。

class Missionattr_reader :mission_name, :agent_name, :objective, :licence_to_killdef initialize(mission_name: mission_name, agent_name: agent_name, objective: objective, licence_to_kill: licence_to_kill)@mission_name    = mission_name@agent_name      = agent_name@objective       = objective@licence_to_kill = licence_to_killenddef assignprint "Mission #{mission_name} has been assigned to #{agent_name} with the objective to #{objective}."if licence_to_killprint " The licence to kill has been granted."elseprint " The licence to kill has not been granted."endend
endclass Mdef assign_new_mission(mission)mission.assignend
endm = M.new
mission = Mission.new(mission_name: 'Octopussy', agent_name: 'James Bond', objective: 'find the nuclear device', licence_to_kill: true)
m.assign_new_mission(mission)
# => Mission Octopussy has been assigned to James Bond with the objective to find the nuclear device. The licence to kill has been granted.

因此,我们创建了一个新对象Mission ,该对象仅专注于为M提供分配新任务所需的信息,并为#assign_new_mission提供单个参数对象。 无需自己传递这些讨厌的参数。 相反,您告诉对象在方法本身内部揭示所需的信息。 此外,我们还将某些行为(即如何打印的信息)提取到新的Mission对象中。

M为什么需要知道如何打印任务分配? 新的#assign还由于减轻了重量而得益于提取,因为我们不需要传入参数对象,因此无需编写诸如mission.mission_namemission.agent_name类的东西。 现在,我们只使用attr_reader ,它比不提取时干净得多。 你挖了吗?

还方便的是, Mission可能会收集各种其他方法或状态,这些状态或状态很好地封装在一个地方,可供您访问。

使用这种技术,您将获得更简洁,易于阅读的方法,并避免在各处重复相同的参数组。 相当划算! 摆脱相同的参数组也是DRY代码的重要策略。

尝试寻找提取的不仅仅是数据。 如果您也可以将行为放置在新类中,那么您将拥有更有用的对象,否则它们也将很快开始闻起来。

当然,大多数情况下,您会遇到更复杂的版本-当然,在这种重构过程中,您的测试当然也需要同时进行调整-但是,如果您有一个简单的示例,您将准备好采取行动。

我现在要看新的邦德。 听说不是很好,但是…

更新:锯光谱。 我的结论是:与Skyfall(即MEH imho)相比,Spectre是wawawiwa!

翻译自: https://code.tutsplus.com/articles/rubyrails-code-smell-basics-01--cms-25261

Ruby / Rails代码气味基础01相关推荐

  1. Ruby / Rails代码气味基础03

    这篇对新手友好的文章涵盖了您在职业生涯初期应该熟悉的另一轮气味和重构. 我们介绍了案例陈述,多态性,空对象和数据类. 主题 案例陈述 多态性 空对象 资料类别 案例陈述 这也可以被称为"清单 ...

  2. 视频教程-Ruby on Rails打造企业级RESTful API项目实战我的云音乐-Ruby/Rails

    Ruby on Rails打造企业级RESTful API项目实战我的云音乐 任苹蜻,爱学啊创始人 & CEO,曾就职于某二车手公司担任Android工程师后离职创办爱学啊,我们的宗旨是:人生 ...

  3. 前端基础:html基础(css基础和JavaScript基础)/01/B/S网络结构,html文件结构,html标签格式,lt;bodygt;中的一些常用标记

    前端基础:html基础(css基础和JavaScript基础)/01/B/S网络结构,html文件结构,html标签格式,<body>中的一些常用标记 html:超文本标记语言(非编程语言 ...

  4. 【重难点】【Java基础 01】一致性哈希算法、sleep() 和wait() 的区别、强软弱虚引用

    [重难点][Java基础 01]一致性哈希算法.sleep() 和wait() 的区别.强软弱虚引用 文章目录 [重难点][Java基础 01]一致性哈希算法.sleep() 和wait() 的区别. ...

  5. python一般的基础代码-Python中的变量,一切代码的基础,你掌握了吗

    学习编程,首先要学习的,肯定就是变量了,因为变量是一切代码的基础,变量里面会介绍到python的很多知识点,都是python的基础知识,对于刚入门的同学非常重要. 一.什么是变量 变量存储在内存中的值 ...

  6. Ruby/Rails学习教程-Hello Ruby

    项目需要,我的Ruby之旅也开始了.学习之初必定是各种菜各种困惑,但是作为Developer,学习新知识是我们的基本技能.于是决定写点什么,来自我记录自己的成长轨迹. 从最初的Hello World ...

  7. 滤波器基础01——滤波器的种类与特性

    滤波器基础系列博客,传送门: 滤波器基础01--滤波器的种类与特性 滤波器基础02--滤波器的传递函数与性能参数 滤波器基础03--Sallen-Key滤波器.多反馈滤波器与Bainter陷波器 滤波 ...

  8. 视频教程-桫哥-GOlang基础-01基本程序设计-Go语言

    桫哥-GOlang基础-01基本程序设计 多年互联网从业经验: 有丰富的的企业网站.手游.APP开发经验: 曾担任上海益盟软件技术股份有限公司项目经理及产品经理: 参与项目有益盟私募工厂.睿妙影音家庭 ...

  9. 【Taichi】代码框架基础、数据与计算核

    目录:[Taichi]代码框架基础.数据与计算核 Taichi 代码框架基础 引入包与初始化 作用域 Taichi 支持的数据类型 基元数据类型 修改默认数据类型 数据类型转换 复合数据类型 场数据类 ...

  10. jQuery基础-01

    jQuery基础-01 jQuery 1. 初识jQuery 1.1 使用JavaScript的方式去实现 1.2 使用jQuery的方式去实现 2. 什么是jQuery? 3. 使用的步骤 4. 版 ...

最新文章

  1. 安装Oracle11g先决条件检查失败
  2. java 必备_Java基础必备
  3. 利用grep命令查找文件内容
  4. win32 汇编基础概念整理
  5. 电脑技巧:如何彻底关闭电脑右下角闪烁弹窗广告?
  6. 任正非采访的数据分析解读
  7. source 1.5 中不支持 diamond 运算符
  8. 技术面试问项目难题如何解决的_同轴线如何当网线使用?解决改造项目中难题...
  9. 统计数字字符和空格 (15 分)
  10. WLAN定义和基础架构
  11. windows自动导出oracle数据库,Oracle数据库的自动导出备份脚本(windows环境)
  12. 基于stm32智能车的设计(ucosiii)---北京之行
  13. C#之FileInfo的简单操作
  14. java-php-net-python-4大型卖场仓储部进出仓库管理系统计算机毕业设计程序
  15. C# WMP 视频播放
  16. oracle语句调试,Oracle中使用fnd_log_messages调试的步骤
  17. c语言编程矩阵鞍点函数,c语言 任意输入一个3×3的矩阵,用函数实现求上三角矩阵并输出。...
  18. Linux中一句话反弹Shell细说
  19. dbms_aq和dbms_aqadm有关问题
  20. DAY27.XIA.面向對象

热门文章

  1. PCB原理图制作一些简单个人总结
  2. Python爬虫——糗百
  3. hp服务器增加raid卡,HP服务器增加硬盘实施方案
  4. xshell功能大全(上)
  5. KNN算法以及R语言的实现
  6. JSPL中用fn:contains()判断list中是否包含某个值
  7. 机械臂D-H坐标系的建立
  8. 企业员工管理系统 一:项目介绍
  9. 磁盘被写保护怎么解除?取消写保护状态,只需这几步
  10. python海龟怎样写字又快又好看_python海龟画图