Ruby / Rails代码气味基础01

文件

主题

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

小心

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

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

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

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

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

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

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

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

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

抵抗性

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

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

大班/神班

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

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

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

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

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

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

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

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

class CastingInviter
  EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/

  attr_reader :message, :invitees, :casting

  def initialize(attributes = {})
	  @message  = attributes[:message]  || ''
	  @invitees = attributes[:invitees] || ''
	  @sender   = attributes[:sender]
	  @casting  = attributes[:casting]
	end

	def valid?
	  valid_message? && valid_invitees?
	end

	def deliver
    if valid?
      invitee_list.each do |email|
        invitation = create_invitation(email)
        Mailer.invitation_notification(invitation, @message)
      end
	  else
	    failure_message  = "Your #{ @casting } message couldn’t be sent. Invitees emails or message are invalid"
	    invitation = create_invitation(@sender)
      Mailer.invitation_notification(invitation, failure_message )
	  end
	end

	private

	def invalid_invitees
	  @invalid_invitees ||= invitee_list.map do |item|
      unless item.match(EMAIL_REGEX)
	      item
	    end
	  end.compact
	end

	def invitee_list
	  @invitee_list ||= @invitees.gsub(/\s+/, '').split(/[\n,;]+/)
	end

	def valid_message?
	  @message.present?
	end

	def valid_invitees?
	  invalid_invitees.empty?
	end

  def create_invitation(email)
    Invitation.create(
      casting:       @casting,
      sender:        @sender,
      invitee_email: email,
      status:        'pending'
    )
  end
end

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

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

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

提取类

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

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

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

class CastingInviter

  attr_reader :message, :invitees, :casting

  def initialize(attributes = {})
    @message  = attributes[:message]  || ''
    @invitees = attributes[:invitees] || ''
    @casting  = attributes[:casting]
    @sender   = attributes[:sender]
  end
  
  def valid?
    casting_email_handler.valid?
  end
  
  def deliver
    casting_email_handler.deliver
  end
  
  private
  
  def 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]
  end

  def valid?
    valid_message? && valid_invitees?
  end

  def deliver
    if valid?
      invitee_list.each do |email|
        invitation = create_invitation(email)
        Mailer.invitation_notification(invitation, @message)
      end
    else
      failure_message  = "Your #{ @casting } message couldn’t be sent. Invitees emails or message are invalid"
      invitation = create_invitation(@sender)
      Mailer.invitation_notification(invitation, failure_message )
    end
  end

  private

  def invalid_invitees
    @invalid_invitees ||= invitee_list.map do |item|
      unless item.match(EMAIL_REGEX)
        item
      end
    end.compact
  end

  def invitee_list
    @invitee_list ||= @invitees.gsub(/\s+/, '').split(/[\n,;]+/)
  end
  
  def valid_invitees?
    invalid_invitees.empty?
  end
  
  def valid_message?
    @message.present?
  end

  def 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' do
  scenario 'successfully' do
    visit_root_path
    fill_in      'Email', with: 'M@mi6.com'
    click_button 'Submit'
    visit missions_path
    click_on     'Create Mission' 
    fill_in      'Mission Name', with: 'Project Moonraker'
    click_button 'Submit'

    within "li:contains('Project Moonraker')" do
      click_on 'Mission completed'
    end

    expect(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' do
  scenario 'successfully' do
    sign_in_as 'M@mi6.com'
    create_classified_mission_named 'Project Moonraker'
  
    mark_mission_as_complete        'Project Moonraker'
  
    agent_sees_completed_mission    'Project Moonraker'
  end
end
  
def create_classified_mission_named(mission_name)
	visit missions_path
	click_on     'Create Mission' 
	fill_in      'Mission Name', with: mission_name
	click_button 'Submit'
	end

def mark_mission_as_complete(mission_name)
	within "li:contains('#{mission_name}')" do
	  click_on 'Mission completed'
	end
end

def agent_sees_completed_mission(mission_name)
	expect(page).to have_css 'ul.missions li.mission-name.completed', text: mission_name
end

def sign_in_as(email)
	visit root_path
	fill_in      'Email', with: email
	click_button 'Submit'
end

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

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

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

长参数列表

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

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

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

class M
  def 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_kill
      print " The licence to kill has been granted."
    else
      print "The licence to kill has not been granted."
    end
  end
end

m = 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 Mission
  attr_reader :mission_name, :agent_name, :objective, :licence_to_kill

  def 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_kill
	end

	def assign
	  print "Mission #{mission_name} has been assigned to #{agent_name} with the objective to #{objective}."
	  if licence_to_kill
	    print " The licence to kill has been granted."
	  else
	    print " The licence to kill has not been granted."
	  end
	end
end

class M
  def assign_new_mission(mission)
	  mission.assign
	end
end

m = 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值