Rails遗留程序中最常犯的错误(下)

在模型的实例方法里,本来不需要的时候使用了“self.”

不好的


; html-script: false ]
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{self.first_name} #{self.last_name}"
  end

end

这段代码并不复杂但是里面并不需要使用“self.”。把“self.”去掉会使代码更简洁且不影响可用性。

好的

在模型里,只有在实例方法里需要赋值时,才会用到“self.”,否则通篇的“self.”只会徒增代码复杂度。


; html-script: false ]
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    "#{first_name} #{last_name}"
  end

end

使用条件表达式并且返回了条件

不好的


; html-script: false ]
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    if name
      name
    else
      "No name"
    end
  end

end

或者


; html-script: false ]
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    name ? name : "No name"
  end

end

这段代码的问题在于:在不需要的地方添加了控制语句。

好的

有种更简便的处理方式也能达到同样效果


; html-script: false ]
# app/models/user.rb
class User < ActiveRecord::Base

  def full_name
    name || "No name"
  end

end

简单来说这段代码会在name不为false或nil时将其返回,否则返回”No name”.

使用得当的话,||和&&这些操作符会对提升你的代码品质提供巨大助力。

在视图层使用行内样式

不好的


; html-script: false ]
<!-- app/views/projects/show.html.erb -->
...
<h3 style="font-size:20px;letter-spacing:normal;color:#95d60a;line-height:100%;margin:0;font-family:'Proxima Nova';">
  SECRET PROJECT
</h3>
...

这里我们只列出一个标签,所有的样式都写在了标签里。现在,请设想一下,如果所有的标签都接收行内样式。这会把你的HTML变得和其难度,除此之外,每当你需要引入另一个同样的h3元素时,将不得不把同样代码照搬一边,造成冗余。

好的


; html-script: false ]
// app/assets/stylesheets/application.css
.project-title {
    font-size: 20px;
    letter-spacing: normal;
    color: #95d60a; 
    line-height: 100%;
    margin: 0;
    font-family:'Proxima Nova';
}


; html-script: false ]
<!-- app/views/projects/show.html.erb -->
...
<h3 class="project-title">
  SECRET PROJECT
</h3>
...

现在我么可以复用样式了,并且HTML的可读性也有所提高。

注意:这只是个简单的范例,实际应用时你应该把CSS拆分成多个小文件,并通过application.css来加载这些文件。另外只有在email模板里,才会用到行内样式。

在视图层使用JavaScript

不好的


; html-script: false ]
<!-- app/views/questions/show.html.erb -->
...
<textarea rows="4" cols="50" class='wysihtml5'>
  Insert your question details here.
</textarea>
...
<script>
  $(document).ready(function(){
  $('textarea.wysihtml5').wysihtml5({
    "font-styles": true, //Font styling, e.g. h1, h2, etc. Default true.
    "emphasis": true, //Italics, bold, etc. Default true.
    "lists": true, //(Un)ordered lists, e.g. Bullets, Numbers. Default true.
    "html": false, //Button which allows you to edit the generated HTML. Default false.
    "link": true, //Button to insert a link. Default true.
    "image": true, //Button to insert an image. Default true.
    "color": true //Button to change color of font. Default true.
  });
});
<script>

这里的逻辑和特定页面耦合在一起,导致代码不可复用。

好的

Rails里面有专门用于组织和存放javascript代码的地方:“app/assets/javascripts/”。


; html-script: false ]
// app/assets/javascripts/application.js
...
$(document).ready(function(){
  $('textarea.wysihtml5').wysihtml5({
    "font-styles": true, //Font styling, e.g. h1, h2, etc. Default true.
    "emphasis": true, //Italics, bold, etc. Default true.
    "lists": true, //(Un)ordered lists, e.g. Bullets, Numbers. Default true.
    "html": false, //Button which allows you to edit the generated HTML. Default false.
    "link": true, //Button to insert a link. Default true.
    "image": true, //Button to insert an image. Default true.
    "color": true //Button to change color of font. Default true.
  });
});
...


; html-script: false ]
<!-- app/views/questions/show.html.erb -->
...
<textarea rows="4" cols="50" class='wysihtml5'>
  Insert your question details here.
</textarea>
...

现在我们可以在view层任何地方用这段代码了。只需要页面上有一个带有wysihtml5这个class的textarea,刚才的那段js就会被执行。

注意:这只是个简单的范例,实际应用时需要考虑是否需要把你的JavaScript拆分成若干小的文件,并通过application.js来加载这些文件。另外,如果你使用的是CoffeeScript而非JavaScript,请坚持不要把CoffeeScript与普通JavaScript在一起混写。

调用方法时把另一个方法的调用作为参数

不好的


; html-script: false ]
# app/services/find_or_create_topic.rb
class FindOrCreateTopic
  ...
  def self.perform(user, name)
    find(user, sluggify(name)) || create(user, name)
  end
  ...
end

这段代码里调用了find方法并传入了2个参数,首参数为user,第二个参数则是直接调用了sluggify这个方法并把name作为参数传给sluggify。你也许会有疑问,这么写有什么问题吗?我明明完全能够看懂这段代码呀。是的,代码也许不难理解,但是每次到这里你都需要自己做一点脑筋转换,而这正是我一直极力想要避免的。

好的

避免需要脑筋转换的一个比较有效的办法就是:使用有意义的变量名。


; html-script: false ]
# app/services/find_or_create_topic.rb
class FindOrCreateTopic
  ...
  def self.perform(user, name)
    slug = sluggify(name)
    find(user, slug) || create(user, name)
  end
  ...
end

这样做可以避免脑筋转换。换用含义明确的变量名之后,每当你再调用find方法,就会知道find接受一个user和一个slug做参数。

没有使用类来隔离Rake Tasks

不好的


; html-script: false ]
# lib/tasks/recalculate_badges_for_users.rake
namespace :users do

  desc "Recalculates Badges for Users"
  task recalculate_badges: :environment do
    user_count = User.count

    User.find_each do |user|
      puts "#{index}/#{user_count} Recalculating Badges for: #{user.email}"

      if user.questions_with_negative_votes >= 1 || user.answers_with_negative_votes >= 1
        user.grant_badge('Critic')
      end

      user.answers.find_each do |answer|
        question   = answer.question
        next unless answer && question

        days       = answer.created_at - question.created_at
        days_count = (days / 1.day).round

        if (days_count >= 60) && (question.vote_count >= 5)
          grant_badge('Necromancer') && return
        end
      end
    end
  end

end

这个rake task有问题多多。最主要的问题是,这个rake太长了而且不好测试。代码写的初一看也很难理解。你只好相信这个task的确是为用户重新计算奖章系统的,因为它上面描述就这么写的。

好的

解决这个问题最好的办法就是,把业务逻辑挪到一个特定的类里面。所以,让我们新建个类来搞定它吧。


; html-script: false ]
# app/services/recalculate_badge.rb
class RecalculateBadge
  attr_reader :user

  def initialize(user)
    @user = user
  end

  def grant_citric
    if grant_citric?
      user.grant_badge('Critic')
    end
  end

  def grant_necromancer
    user.answers.find_each do |answer|
      question = answer.question
      next unless answer && question

      if grant_necromancer?(answer, question)
        grant_badge('Necromancer') && return
      end
    end
  end

  protected

    def grant_citric?
      (user.questions_with_negative_votes >= 1) ||
      (user.answers_with_negative_votes >= 1)
    end

    def days_count(answer, question)
      days = answer.created_at - question.created_at
      (days / 1.day).round
    end

    def grant_necromancer?(answer, question)
      (days_count(answer,question) >= 60) &&
      (question.vote_count >= 5)
    end

end


; html-script: false ]
# lib/tasks/recalculate_badges_for_users.rake
namespace :users do

  desc "Recalculates Badges for Users"
  task recalculate_badges: :environment do
    user_count = User.count

    User.find_each do |user|
      puts "#{index}/#{user_count} Recalculating Badges for: #{user.email}"
      task = RecalculateBadge.new(user)
      task.grant_citric
      task.grant_necromancer
    end
  end

end

如你所见,现在这个rake task可读性要好的多,而且还可以单独测试每一种批准徽章的方法。除此以外我么也可以在有需要时复用这个类。当然这段代码只是点到即止,诸位可以再做进一步优化。

没有遵循Sandi Metz的规则

  1. 每个类代码不可以超过100行
  2. 每个方法代码不可以超过5行
  3. 方法参数不可以超过4个,hash项也包括在内
  4. 控制器之可以初始化一个对象。而且视图层只可以使用一个实例变量,并且只可以在这个对象上调用方法(@object.collaborator.value这种是不可以的)。

更多关于Sandi Metz的规则请移步至thoughtbot,参阅Sandi Metz’ Rules For Developers这篇博文。