Ruby Ruby 代码清洁之道 (翻译)

rocLv · 2019年02月23日 · 2094 次阅读

Ruby 代码清洁之道

应用于 Ruby 语言的代码清洁之道。

clean-code-javascript项目启发。

介绍

软件工程的原则,来自于 Robert C. Martin 的书Clean Code, 应用于 Ruby。这不是样式指南。它是构建可读,可复用,可重构 工业级的 Ruby 软件工程指南。

不是每一个原则都必须严格的遵守,甚至有一些代码清洁原则没有达到广泛的共识。这些只是一些指导原则而已,但是他们是清洁之道作者的多年的经验总结。

我们的软件工程的这么手艺已经有 50 多年的历史了,不过我们依然可以从中学到很多。当软件架构和架构本身年纪一样大时,那时可能我们需要遵守更加严格的规则了。 不过,就当下而言,我们最好把这些原则当成你或者你的团队的 Ruby 代码清洁指南。

值得一提的是:知道这些原则不会立即让您成为一个更好的软件开发者,同样,一直努力去遵循这些代码清洁之道,并不意味着你就不会犯错。我们开始写的每行代码,这个过程就像瓷器从泥胚开始,最后变成瓷器一样。最后,当我们和搭档 Review 代码的时候,一起来把这些代码中的坏味道去掉。不要因为你的第一版需要提高的代码草稿而垂头丧气。相反,让我们打败臭代码。

变量

使用有意义的和可以发音的变量名

坏:*

yyyymmdstr = Time.now.strftime('%Y/%m/%d')

好:

current_date = Time.now.strftime('%Y/%m/%d')

⬆ 回到目录

为同样类型的变量使用同样的单词

为某个概念选好一个单词后不要再换来换去。

坏:

user_info
user_data
user_record

starts_at
start_at
start_time

好:

user

starts_at

⬆ 回到目录

使用可搜索的名称,使用常量

我们读的代码量会大大超过写的代码量。所以写出来可读和易搜索的代码是很重要的。所以好好的给变量起名字,最终会伤害到代码的读者。

同样,使用 “魔法数字”(常量),而不是硬编码变量。

坏:

# 86_400究竟是什么玩意?
status = Timeout::timeout(86_400) do
  # ...
end

好:

# 用常量来声明
SECONDS_IN_A_DAY = 86_400

status = Timeout::timeout(SECONDS_IN_A_DAY) do
  # ...
end

⬆ 回到目录

使用描述性的变量名

坏:

address = 'One Infinite Loop, Cupertino 95014'
city_zip_code_regex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/
save_city_zip_code(city_zip_code_regex.match(address)[1], city_zip_code_regex.match(address)[2])

好:

address = 'One Infinite Loop, Cupertino 95014'
city_zip_code_regex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/
_, city, zip_code = city_zip_code_regex.match(address).to_a
save_city_zip_code(city, zip_code)

⬆ 回到目录

避免 “心智图法”

明示比暗示要好。

坏:

locations = ['Austin', 'New York', 'San Francisco']
locations.each do |l|
  do_stuff
  do_some_other_stuff
  # ...
  # ...
  # ...
  # Wait, what is `l` for again?
  dispatch(l)
end

好:

locations = ['Austin', 'New York', 'San Francisco']
locations.each do |location|
  do_stuff
  do_some_other_stuff
  # ...
  # ...
  # ...
  dispatch(location)
end

⬆ 回到目录

不要添加不必要的上下文

假如你的 class/object 名告诉你些什么,就不要在你的变量名里重复一遍了。

坏:

car = {
  car_make: 'Honda',
  car_model: 'Accord',
  car_color: 'Blue'
}

def paint_car(car)
  car[:car_color] = 'Red'
end

好:

car = {
  make: 'Honda',
  model: 'Accord',
  color: 'Blue'
}

def paint_car(car)
  car[:color] = 'Red'
end

⬆ 回到目录

使用默认参数,而不是使用异或,或是条件赋值

默认参数比异或这种赋值方法会更清洁。 坏:

def create_micro_brewery(name)
  brewery_name = name || 'Hipster Brew Co.'
  # ...
end

好:

def create_micro_brewery(brewery_name = 'Hipster Brew Co.')
  # ...
end

⬆ 回到目录

**方法

方法参数(不多于 2 个)

限制方法参数是令人难以想象的重要,因为它会让你容易测试你的方法。多余三个参数的方法常常会让测试变得非常复杂。因为需要测试的组合太多了。

一个或两个参数是理想的情况,三个参数的情况应该尽量避免。超过三个参数的情况需要重构。通常,假如你的参数个数超过三个,那么意味着这个方法承担的责任太多。尽管有时候并不是这样的,多数的时候,多余的参数可以用一个高级的对象来包装一下。或者你可以通过实例变量来给方法传数据。

因为在 Ruby 中创建对象很方便,不像其他语言一样,假如你需要许多参数的话,你可以使用对象。在 Ruby 中流行的做法是使用 Hash 作为参数。 为了让方法想要的属性显而易见,你可以使用关键词参数的语法(在 Ruby 2.1 中引入)。这有几个优点:

  1. 当有人查找方法的签名时,使用了那个属性一目了然。

  2. 假如必要的关键词参数没有传,Ruby 会 raise 一个有用的ArgumentError,它会告诉我们我们必须传入哪个参数。

坏:

def create_menu(title, body)
  # ...
end

好:

def create_menu(title:, body:)
  # ...
end

create_menu(title: 'Foo', body: 'Bar')

⬆ 回到目录

方法应该仅做一件事

这个到目前为止软件工程中最重要的一条原则。当一个方法的责任超过一个时,它们会变得难以组合,难以测试,难以推断它的具体作用。 当你可以做到一个方法只做一件事时,你重构它们的时候就会比较容易,而且你的代码也会变得更加清洁。假如你通过这个指南就记住了这一条, 那你也已经远远超过许多程序员了。

坏:

def email_clients(clients)
  clients.each do |client|
    client_record = database.lookup(client)
    email(client) if client_record.active?
  end
end

email_clients(clients)

好:

def email_clients(clients)
  clients.each { |client| email(client) }
end

def active_clients(clients)
  clients.select { |client| active_client?(client) }
end

def active_client?(client)
  client_record = database.lookup(client)
  client_record.active?
end

email_clients(active_clients(clients))

⬆ 回到目录

方法名应该说明它们做了什么

差的方法名容易让 Review 代码的人产生误解。当给方法命名的时候,要尽量体现方法的意图。

坏:

def add_to_date(date, month)
  # ...
end

date = DateTime.now

# It's hard to tell from the method name what is added
add_to_date(date, 1)

好:

def add_month_to_date(date, month)
  # ...
end

date = DateTime.now
add_month_to_date(date, 1)

⬆ 回到目录

方法应该仅仅是一层抽象

当你的方法有多余一层的抽象时,通常说明这个方法太复杂了。拆分方法会提高可复用性,也会让测试变得很容易。更进一步说,方法应该降低抽象的层级:一个很抽象的方法应该调用稍微不抽象的方法,以此类推。

坏:

def interpret(code)
  regexes = [
    # ...
  ]

  statements = code.split(' ')
  tokens = []
  regexes.each do |regex|
    statements.each do |statement|
      # ...
    end
  end

  ast = []
  tokens.each do |token|
    # lex...
  end

  result = []
  ast.each do |node|
    # result.push(...)
  end

  result
end

好:

def interpret(code)
  tokens = tokenize(code)
  ast = lex(tokens)
  parse(ast)
end

def tokenize(code)
  regexes = [
    # ...
  ]

  statements = code.split(' ')
  tokens = []
  regexes.each do |regex|
    statements.each do |statement|
      # tokens.push(...)
    end
  end

  tokens
end

def lex(tokens)
  ast = []
  tokens.each do |token|
    # ast.push(...)
  end

  ast
end

def parse(ast)
  result = []
  ast.each do |node|
    # result.push(...)
  end

  result
end

⬆ 回到目录

DRY

尽你最大的努力去实现 DRY 的目标。重复的代码意味着假如你需要修改一些逻辑的时候,你必须同时修改它们。

设想你开一家餐馆,你记录下你每天购买的蔬菜:所有的西红柿,洋葱,蒜,辣椒等等。假如你同时有几个单子,然后当你上了一盘西红柿以后,你必须更新所有的记录。 假如你仅有一个单子,那么你仅需要更新一个地方!

通常,你有重复的代码的情况是因为它们或多或少会有点不一样,其他大部分都一样,但是那些不同点让你不得不写一些大部分代码重复的方法。 移除重复的代码意味着创建抽象,通过抽象出一个方法/模块/类来处理不同的情况。

创建正确的抽象有点麻烦,这也是为什么你应该遵循 SOLID 原则。我们会在后面的的部分来单独讨论 SOLID 原则。坏的抽象比重复代码更臭,所以一定要更加小心! 继然说到这里了,假如你可以创建好的抽象,为什么不呢!不要重复自己,否则当你想要改变一个地方的时候,你不得不同时修改几个地方。

坏:

def show_developer_list(developers)
  developers.each do |developer|
    data = {
      expected_salary: developer.expected_salary,
      experience: developer.experience,
      github_link: developer.github_link
    }

    render(data)
  end
end

def show_manager_list(managers)
  managers.each do |manager|
    data = {
      expected_salary: manager.expected_salary,
      experience: manager.experience,
      portfolio: manager.mba_projects
    }

    render(data)
  end
end

好:

def show_employee_list(employees)
  employees.each do |employee|
    data = {
      expected_salary: employee.expected_salary,
      experience: employee.experience
    }

    case employee.type
    when 'manager'
      data[:portfolio] = employee.mba_projects
    when 'developer'
      data[:github_link] = employee.github_link
    end

    render(data)
  end
end

⬆ 回到目录

不要使用 flag 作为方法的参数

Flag 意味着这个方法做了不只一件事。方法应该只做一件事。依据逻辑变量拆分你的方法。

坏:

def create_file(name, temp)
  if temp
    fs.create("./temp/#{name}")
  else
    fs.create(name)
  end
end

好:

def create_file(name)
  fs.create(name)
end

def create_temp_file(name)
  create_file("./temp/#{name}")
end

⬆ 回到目录

避免负效应(第一部分)

一个不仅仅会接受参数,返回值得方法会产生负效应。负效应可能入写入文件,修改全局变量,或者不小心把你的钱转给陌生人。

现在,偶尔,在一个程序里需要一些负效应。例如之前的例子,你可能需要写入文件。你想要做的是在你做这些的地方要中心化它们。不要用几个方法,或者类都去写入一个特殊的文件。 让一个服务来做这件事。一个,而且仅一个。

关键在于避免普通的陷阱,例如没有任何结构,在对象间共享状态,使用可变的数据类型,不中心化处理负效应的产生。假如你这样做,你会比绝大多数其他程序员更快乐。

坏:

# Global variable referenced by following method.
# If we had another method that used this name, now it'd be an array and it could break it.
$name = 'Ryan McDermott'

def split_into_first_and_last_name
  $name = $name.split(' ')
end

split_into_first_and_last_name()

puts $name # ['Ryan', 'McDermott']

好:

def split_into_first_and_last_name(name)
  name.split(' ')
end

name = 'Ryan McDermott'
first_and_last_name = split_into_first_and_last_name(name)

puts name # 'Ryan McDermott'
puts first_and_last_name # ['Ryan', 'McDermott']

⬆ 回到目录

避免负效应(部分 2)

在 Ruby 里,所有的都是对象,所有的都是通过值来传递,但是这些值是对象的参考。在对象和数组的例子中,假如你的方法在购物车数组里改变了,例如,通过添加一件商品, 另外一个处理cart数组的方法可能被这个影响。这也可能很好,也可能很坏。让我们设想一个坏的情况:

用户点击了购买按钮,这个按钮会调用一个purchase方法,这个方法会发生一个网络请求,然后把cart的值传到服务器。 因为网络环境较差,purchase方法不得不重新发起请求。现在,假设在网络请求开始的同时用户又不小心在一个他原本不想要的产品上点击了一下 “加入购物车” 按钮, 假如这两个同时发生,那么purchase方法就会把不小心加进购物车的产品一起结算了,因为它调用的数组正好是add_item_to_cart`方法通过添加不想要的产品而修改过的数组的参考。 译者注:(通常数组等在作为参数的时候,为了节约内存,编译器一般只会把数组的指针,即数组的地址传递给调用它的方法。因为 Ruby 中没有指针这样的概念,这里的 reference 意义和指针是一样的。)

好的方案就是add_item_to_cart总是克隆cart, 然后编辑它,然后返回克隆。这确保了没有别的方法会有购物车数组的参考,因此也不用担心它会被修改。

有两点需要说明的是:

  1. 可能有一些情况,你确实希望修改输入的对象,但是当你适应这种编程实践的时候,你会发现这样的情况非常少见。大部分的情况可以在丝毫不影响的情况下进行重构。
  2. 克隆大的对象在性能方面会有很大的影响。不过,幸运的是,因为有hamster。通过它,克隆大型对象在性能方面的牺牲可以忽略不计。

坏:

def add_item_to_cart(cart, item)
  cart.push(item: item, time: Time.now)
end

好:

def add_item_to_cart(cart, item)
  cart + [{ item: item, time: Time.now }]
end

⬆ 回到目录

与命令式编程相比,更喜欢函数式编程

Ruby 不是函数式语言,就像 Haskell 一样,但是它也可以做到类似函数式编程。函数式语言更加清洁,也更容易测试。如果你可以,就用函数式的方式去编程。

坏:

programmer_output = [
  {
    name: 'Uncle Bobby',
    lines_of_code: 500
  }, {
    name: 'Suzie Q',
    lines_of_code: 1500
  }, {
    name: 'Jimmy Gosling',
    lines_of_code: 150
  }, {
    name: 'Grace Hopper',
    lines_of_code: 1000
  }
]

total_output = 0

programmer_output.each do |output|
  total_output += output[:lines_of_code]
end

好:

programmer_output = [
  {
    name: 'Uncle Bobby',
    lines_of_code: 500
  }, {
    name: 'Suzie Q',
    lines_of_code: 1500
  }, {
    name: 'Jimmy Gosling',
    lines_of_code: 150
  }, {
    name: 'Grace Hopper',
    lines_of_code: 1000
  }
]

INITIAL_VALUE = 0

total_output = programmer_output.sum(INITIAL_VALUE) { |output| output[:lines_of_code] }

⬆ 回到目录

封装条件

坏:

if params[:message].present? && params[:recipient].present?
  # ...
end

好:

def send_message?(params)
  params[:message].present? && params[:recipient].present?
end

if send_message?(params)
  # ...
end

⬆ 回到目录

避免否定条件

坏:

if !genres.blank?
  # ...
end

好:

unless genres.blank?
  # ...
end

# or

if genres.present?
  # ...
end

⬆ 回到目录

避免条件

第一次听到这句话的人的第一反应就是这好像是不可能完成的任务. "没有if我什么也干不了"。 答案是在许多情况下,你都可以用多态来完成这些任务。第二个疑问通常是 “为什么我要这么做?”。答案是之前我们学到的代码清洁的概念:方法应该只做一件事。 如果你的类或方法中有if语句,你就在告诉你的用户,你的方法做了不止一件事情。记住,只做一件事情。

坏:

class Airplane
  # ...
  def cruising_altitude
    case @type
    when '777'
      max_altitude - passenger_count
    when 'Air Force One'
      max_altitude
    when 'Cessna'
      max_altitude - fuel_expenditure
    end
  end
end

好:

class Airplane
  # ...
end

class Boeing777 < Airplane
  # ...
  def cruising_altitude
    max_altitude - passenger_count
  end
end

class AirForceOne < Airplane
  # ...
  def cruising_altitude
    max_altitude
  end
end

class Cessna < Airplane
  # ...
  def cruising_altitude
    max_altitude - fuel_expenditure
  end
end

⬆ 回到目录

避免类型检查(第一部分)

Ruby 是动态类型,也就是说你的方法可以传入任意类型的参数。有时这种自由可能会伤害你,因此有种情况可能会诱惑你在你的方法里做类型检查。 有很多方法可以避免这样做。首先需要考虑的是一致的 API。

坏:

def travel_to_texas(vehicle)
  if vehicle.is_a?(Bicycle)
    vehicle.pedal(@current_location, Location.new('texas'))
  elsif vehicle.is_a?(Car)
    vehicle.drive(@current_location, Location.new('texas'))
  end
end

好:

def travel_to_texas(vehicle)
  vehicle.move(@current_location, Location.new('texas'))
end

⬆ 回到目录

避免类型检查(第二部分)

假如你正使用基础的数据类型,如字符串或者整数,所以你没法使用多态这种解决方案,这时你应该考试使用contracts.ruby。 对于需要手动进行类型检查的 Ruby 代码的主要问题是获得 “类型安全” 的同时,牺牲了可读性。保持你的 Ruby 代码清洁,写好的测试代码,这样代码检查就容易了。

坏:

def combine(val1, val2)
  if (val1.is_a?(Numeric) && val2.is_a?(Numeric)) ||
     (val1.is_a?(String) && val2.is_a?(String))
    return val1 + val2
  end

  raise 'Must be of type String or Numeric'
end

好:

def combine(val1, val2)
  val1 + val2
end

⬆ 回到目录

移除死代码

死代码和重复的代码一样臭。没有理由在你的代码里保存它们。假如它不会被调用,去掉它!假如你需要的时候,你还是可以从代码仓库中找回它们。

坏:

def old_request_module(url)
  # ...
end

def new_request_module(url)
  # ...
end

req = new_request_module(request_url)
inventory_tracker('apples', req, 'www.inventory-awesome.io')

好:

def new_request_module(url)
  # ...
end

req = new_request_module(request_url)
inventory_tracker('apples', req, 'www.inventory-awesome.io')

⬆ 回到目录

对象和数据结构

使用 getters 和 setters

使用 getters 和 setters 去获取一个对象的属性,可能比简单的查找对象的属性要好。为什么?你可能会问。嗯,这里有个没组织好的一些理由:

  • 当你不仅仅要获取一个对象的属性的时候,你不必查找并且修改你代码库里的每个 accessor。
  • 当你有set时,添加一些属性验证会很容易。
  • 封装内部的表达。
  • 当有 getter 和 setter 时,添加记录和错误处理就会比较容易。
  • 比如说你从服务器获取时,你可以延迟加载你的对象的属性。

坏:

def make_bank_account
  # ...

  {
    balance: 0
    # ...
  }
end

account = make_bank_account
account[:balance] = 100
account[:balance] # => 100

好:

class BankAccount
  def initialize
    # this one is private
    @balance = 0
  end

  # a "getter" via a public instance method
  def balance
    # do some logging
    @balance
  end

  # a "setter" via a public instance method
  def balance=(amount)
    # do some logging
    # do some validation
    @balance = amount
  end
end

account = BankAccount.new
account.balance = 100
account.balance # => 100

假如你的 getter 和 setter 变得非常琐碎,你应该使用attr_accessor来定义他们。这个方法在实现数据类的对象时尤其方便,如 ActiveRecord 的对象,响应远程 API 调用的包装。 好:

class Toy
  attr_accessor :price
end

toy = Toy.new
toy.price = 50
toy.price # => 50

然而,你必须意识到在有些情况,使用attr_accessor也是臭代码,你可以看看这里 ⬆ 回到目录

避免流利的接口

流利的接口是面向对象的 API,通过使用方法链来提高程序的可读性。

当有一些情景,需要频繁的创建对象,这种模式可以让代码看起来更简洁(如 ActiveRecord 的查询),其他更多的情况,这样编码会有一些代价。

  1. 打破封装
  2. 打破装饰器
  3. 在测试的时候,难以mock
  4. 让提交代码时的 diff 变得难以阅读。

其他更多的信息,你可以阅读一下Marco Pivetta写的这篇博客

坏:

class Car
  def initialize(make, model, color)
    @make = make
    @model = model
    @color = color
    # NOTE: Returning self for chaining
    self
  end

  def set_make(make)
    @make = make
    # NOTE: Returning self for chaining
    self
  end

  def set_model(model)
    @model = model
    # NOTE: Returning self for chaining
    self
  end

  def set_color(color)
    @color = color
    # NOTE: Returning self for chaining
    self
  end

  def save
    # save object...
    # NOTE: Returning self for chaining
    self
  end
end

car = Car.new('Ford','F-150','red')
  .set_color('pink')
  .save

好:

class Car
  attr_accessor :make, :model, :color

  def initialize(make, model, color)
    @make = make
    @model = model
    @color = color
  end

  def save
    # Save object...
  end
end

car = Car.new('Ford', 'F-150', 'red')
car.color = 'pink'
car.save

⬆ 回到目录

偏好使用 Mixin 多过继承

正如在著名的设计模式这本书里提到的,你应该尽量使用 Mixin,而不是继承。 有许多好的理由使用继承,也有很多好的理由使用 Mixin。主要在于你直觉选择了继承,可以尝试使用 Mixin 来模型化你的问题,结果可能会更好。 在某些情况下,确实是这样的。

你可能犹豫,“那什么时候我应该用继承呢?” 这依赖于你的问题,但是有一个公认的列表,如果属于下面列出的情况,那么使用继承更合理。

  1. 你的继承代表了 “是一个” 的关系,而不是 “有一个” 的关系 (Human->Animal vs. User->UserDetails)。
  2. 你可以复用基类的代码(人可以向动物一样移动)。
  3. 你希望可以通过改变基类影响全部的子孙类。(当动物活动的时候,改变所有动物运动时所消耗的卡路里)

坏:

class Employee
  def initialize(name, email)
    @name = name
    @email = email
  end

  # ...
end

# 坏 because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData < Employee
  def initialize(ssn, salary)
    super()
    @ssn = ssn
    @salary = salary
  end

  # ...
end

好:

class EmployeeTaxData
  def initialize(ssn, salary)
    @ssn = ssn
    @salary = salary
  end

  # ...
end

class Employee
  def initialize(name, email)
    @name = name
    @email = email
  end

  def set_tax_data(ssn, salary)
    @tax_data = EmployeeTaxData.new(ssn, salary)
  end
  # ...
end

⬆ 回到目录

SOLID

单一责任原则

如同在清洁代码之道中所说,“修改类的原因不能超过一个”。给一个类添加许多功能,就好像要把它塞满一样,这种情形好像坐飞机的时候只能带一个行李箱。 这会导致类从概念上缺少凝聚力,所以就会有很多理由去修改这个类。最小化你修改类的需求很重要。因为如果一个类中包含太多的功能会让你在修改哪怕其中的一小块代码,你也难于 理解它会对其他代码造成什么样的影响。

坏:

class UserSettings
  def initialize(user)
    @user = user
  end

  def change_settings(settings)
    return unless valid_credentials?
    # ...
  end

  def valid_credentials?
    # ...
  end
end

好:

class UserAuth
  def initialize(user)
    @user = user
  end

  def valid_credentials?
    # ...
  end
end

class UserSettings
  def initialize(user)
    @user = user
    @auth = UserAuth.new(user)
  end

  def change_settings(settings)
    return unless @auth.valid_credentials?
    # ...
  end
end

⬆ 回到目录

开/闭原则(OCP)

Bertrand Meyer 所说, “软件中的实体(类、模块、方法等)应该对扩展是开放的,但是对修改是关闭的。” 什么意思呢?这个原则基本是在说你应该允许其他人在不修改现存代码的情况下就添加新的功能。

坏:

class Adapter
  attr_reader :name
end

class AjaxAdapter < Adapter
  def initialize
    super()
    @name = 'ajaxAdapter'
  end
end

class NodeAdapter < Adapter
  def initialize
    super()
    @name = 'nodeAdapter'
  end
end

class HttpRequester
  def initialize(adapter)
    @adapter = adapter
  end

  def fetch(url)
    case @adapter.name
    when 'ajaxAdapter'
      make_ajax_call(url)
    when 'nodeAdapter'
      make_http_call(url)
    end
  end

  def make_ajax_call(url)
    # ...
  end

  def make_http_call(url)
    # ...
  end
end

好:

class Adapter
  attr_reader :name
end

class AjaxAdapter < Adapter
  def initialize
    super()
    @name = 'ajaxAdapter'
  end

  def request(url)
    # ...
  end
end

class NodeAdapter < Adapter
  def initialize
    super()
    @name = 'nodeAdapter'
  end

  def request(url)
    # ...
  end
end

class HttpRequester
  def initialize(adapter)
    @adapter = adapter
  end

  def fetch(url)
    @adapter.request(url)
  end
end

⬆ 回到目录

里氏替换原则(LSP)

这个概念本身很简单,不过它的名字却有点唬人。它正式的定义是 “假如 S 是 T 的子类型,那么类型为 T 的对象应该都可以被类型为 S 的对象替换,也不会改变程序的属性(正确性、任务执行等)。”(例如,类型 S 的对象可能替换类型 T 的对象)这个定义听起来更吓人。

对这个原则最佳的解释是假如你有一个父类和子类,那么父类即便用子类替换了,也不会引发错误。这可能依然会让人迷糊,所以我们看一下经典的 Square-Rectangle 例子。 从数学的角度来说,正方形是矩形的一种,但是假如你通过继承来模型化它,你很快就会遇到麻烦。

坏:

class Rectangle
  def initialize
    @width = 0
    @height = 0
  end

  def color=(color)
    # ...
  end

  def render(area)
    # ...
  end

  def width=(width)
    @width = width
  end

  def height=(height)
    @height = height
  end

  def area
    @width * @height
  end
end

class Square < Rectangle
  def width=(width)
    @width = width
    @height = width
  end

  def height=(height)
    @width = height
    @height = height
  end
end

def render_large_rectangles(rectangles)
  rectangles.each do |rectangle|
    rectangle.width = 4
    rectangle.height = 5
    area = rectangle.area # BAD: Returns 25 for Square. Should be 20.
    rectangle.render(area)
  end
end

rectangles = [Rectangle.new, Rectangle.new, Square.new]
render_large_rectangles(rectangles)

好:

class Shape
  def color=(color)
    # ...
  end

  def render(area)
    # ...
  end
end

class Rectangle < Shape
  def initialize(width, height)
    super()
    @width = width
    @height = height
  end

  def area
    @width * @height
  end
end

class Square < Shape
  def initialize(length)
    super()
    @length = length
  end

  def area
    @length * @length
  end
end

def render_large_shapes(shapes)
  shapes.each do |shape|
    area = shape.area
    shape.render(area)
  end
end

shapes = [Rectangle.new(4, 5), Rectangle.new(4, 5), Square.new(5)]
render_large_shapes(shapes)

⬆ 回到目录

接口隔离原则 (ISP)

Ruby 没有接口,所以对 Ruby 来说这个原则不像其他语言要求那么严格。然而,它是重要的,甚至和 Ruby 的类型系统缺失相关。

ISP 是说 “客户不应该被强迫去依赖他们不使用的接口。” 因为鸭子类型,所以在 Ruby 中接口是隐式的合同。

当客户依赖包含接口的类,但客户不会使用这个接口,但是其他的客户使用了,那么这个客户就会在其他客户需要修改这个接口时受到影响。

下面的例子是来源于here.

坏:

class Car
  # used by Driver
  def open
    # ...
  end

  # used by Driver
  def start_engine
    # ...
  end

  # used by Mechanic
  def change_engine
    # ...
  end
end

class Driver
  def drive
    @car.open
    @car.start_engine
  end
end

class Mechanic
  def do_stuff
    @car.change_engine
  end
end

好:

# used by Driver only
class Car
  def open
    # ...
  end

  def start_engine
    # ...
  end
end

# used by Mechanic only
class CarInternals
  def change_engine
    # ...
  end
end

class Driver
  def drive
    @car.open
    @car.start_engine
  end
end

class Mechanic
  def do_stuff
    @car_internals.change_engine
  end
end

⬆ 回到目录

依赖反转原则(DIP)

这个原则说明了两个必要的事情:

  1. 高阶的模块不应该依赖于低阶的模块。它们都应该依赖于抽象。
  2. 抽象不应该依赖于细节。细节应该依赖于抽象。

简单来说,DIP 可以防止高阶的模块了解低阶模块的细节,这样可以模块之间的耦合。耦合是很差的开发模式,它可以让代码难以重构。

如之前所说,Ruby 没有接口,所以被依赖的抽象是隐式的合同。也就是对象或者类暴露给别的对象和类的方法。在下面的例子里,隐式的合同是为InventoryTracker类的任意 Request 模块, 都有一个request_items方法。

坏:

class InventoryRequester
  def initialize
    @req_methods = ['HTTP']
  end

  def request_item(item)
    # ...
  end
end

class InventoryTracker
  def initialize(items)
    @items = items

    # BAD: We have created a dependency on a specific request implementation.
    @requester = InventoryRequester.new
  end

  def request_items
    @items.each do |item|
      @requester.request_item(item)
    end
  end
end

inventory_tracker = InventoryTracker.new(['apples', 'bananas'])
inventory_tracker.request_items

好:

class InventoryTracker
  def initialize(items, requester)
    @items = items
    @requester = requester
  end

  def request_items
    @items.each do |item|
      @requester.request_item(item)
    end
  end
end

class InventoryRequesterV1
  def initialize
    @req_methods = ['HTTP']
  end

  def request_item(item)
    # ...
  end
end

class InventoryRequesterV2
  def initialize
    @req_methods = ['WS']
  end

  def request_item(item)
    # ...
  end
end

# By constructing our dependencies externally and injecting them, we can easily
# substitute our request module for a fancy new one that uses WebSockets.
inventory_tracker = InventoryTracker.new(['apples', 'bananas'], InventoryRequesterV2.new)
inventory_tracker.request_items

⬆ 回到目录

测试

测试比运行系统更重要。假如你没有测试,或者测试的比例不够,那么每次你部署的时候,你就很难确定系统会不会出错。 测试的覆盖度是否足够,取决于你的团队,但是 100% 的测试覆盖率(所有的声明和分支)是你获得非常高的信心和开发时内心平和的关键。这意味着你需要一个额外的伟大的测试框架, 你也需要使用好的测试覆盖工具

没有任何理由不写测试。为你的每个功能/模块写测试。假如你更喜欢的方法是测试驱动开发(TDD),那也挺好,但是主要的一点是,你也要在部署系统的时候确保达到你的测试覆盖目标。

每个测试只有一个 expectation

坏:

require 'rspec'

describe 'Calculator' do
  let(:calculator) { Calculator.new }

  it 'performs addition, subtraction, multiplication and division' do
    expect(calculator.calculate('1 + 2')).to eq(3)
    expect(calculator.calculate('4 - 2')).to eq(2)
    expect(calculator.calculate('2 * 3')).to eq(6)
    expect(calculator.calculate('6 / 2')).to eq(3)
  end
end

好:

require 'rspec'

describe 'Calculator' do
  let(:calculator) { Calculator.new }

  it 'performs addition' do
    expect(calculator.calculate('1 + 2')).to eq(3)
  end

  it 'performs subtraction' do
    expect(calculator.calculate('4 - 2')).to eq(2)
  end

  it 'performs multiplication' do
    expect(calculator.calculate('2 * 3')).to eq(6)
  end

  it 'performs division' do
    expect(calculator.calculate('6 / 2')).to eq(3)
  end
end

⬆ 回到目录

错误处理

抛出错误是对的!它们意味着运行时已经可以在程序出错的时候识别出错误,通过在当前的栈停止方法让你知道程序出错,结束进程,然后在日志里记录它们。

不要忽视捕捉错误

在捕捉错误的时候无所作为不会给你修复它的机会,当然你也意识不到出错。用日志记录错误也不太好,因为错误的信息会常常淹没于海量的日志当中。 假如你把代码用begin/rescue包住,这也意味着你知道可能会有错误出现,然后你也应该有一个相应的处理方案。

坏:

require 'logger'

logger = Logger.new(STDOUT)

begin
  method_that_might_throw()
rescue StandardError => err
  logger.info(err)
end

好:

require 'logger'

logger = Logger.new(STDOUT)
# Change the logger level to ERROR to output only logs with ERROR level and above
logger.level = Logger::ERROR

begin
  method_that_might_throw()
rescue StandardError => err
  # Option 1: Only log errors
  logger.error(err)
  # Option 2: Notify end-user via an interface
  notify_user_of_error(err)
  # Option 3: Report error to a third-party service like Honeybadger
  report_error_to_service(err)
  # OR do all three!
end

提供 Exception 的上下文

当你抛出错误的时候,使用描述性的错误类名和错误信息。然后你就可以知道为什么错误会发生,你也可以依据它提供的信息来修改错误。

坏:

def initialize(user)
  fail unless user
  ...
end

好:

def initialize(user)
  fail ArgumentError, 'Missing user' unless user
  ...
end

⬆ 回到目录

Formatting

代码格式

代码格式是非常主观的。就像我们这里的其他规则,没有那条规则是你必须遵循的。要点就是不要在格式化上争论。 有许许多多的像 RuboCop 这样的工具来自动化处理他。随便找一个!对于工程师来说,在代码格式上进行争论是浪费时间。

对于那些自动格式化代码工具无法处理的情况,先看看社区的代码规范指南。

使用一致的大写规则

Ruby 是动态类型,所以字母大写会告诉你很多关于变量,方法的信息。这些规则是主观的,所以你的团队可以选择任一种他们喜欢的规则。重点是保持一致性。

坏:

DAYS_IN_WEEK = 7
daysInMonth = 30

songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude']
Artists = ['ACDC', 'Led Zeppelin', 'The Beatles']

def eraseDatabase; end

def restore_database; end

class ANIMAL; end
class Alpaca; end

好:

DAYS_IN_WEEK = 7
DAYS_IN_MONTH = 30

SONGS = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'].freeze
ARTISTS = ['ACDC', 'Led Zeppelin', 'The Beatles'].freeze

def erase_database; end

def restore_database; end

class Animal; end
class Alpaca; end

⬆ 回到目录

方法的调用者和被调用者应该尽量靠近

假如一个方法调用另一个,尽量让他们在源文件中的位置靠近一点。理想状况下,让调用方法的方法,正好位于被调用方法的上部。 我们希望自上而下的读代码,就像读报纸一样。因为这样可以让你的代码读起来的时候也有这种感觉。

坏:

class PerformanceReview
  def initialize(employee)
    @employee = employee
  end

  def lookup_peers
    db.lookup(@employee, :peers)
  end

  def lookup_manager
    db.lookup(@employee, :manager)
  end

  def peer_reviews
    peers = lookup_peers
    # ...
  end

  def perf_review
    peer_reviews
    manager_review
    self_review
  end

  def manager_review
    manager = lookup_manager
    # ...
  end

  def self_review
    # ...
  end
end

review = PerformanceReview.new(employee)
review.perf_review

好:

class PerformanceReview
  def initialize(employee)
    @employee = employee
  end

  def perf_review
    peer_reviews
    manager_review
    self_review
  end

  def peer_reviews
    peers = lookup_peers
    # ...
  end

  def lookup_peers
    db.lookup(@employee, :peers)
  end

  def manager_review
    manager = lookup_manager
    # ...
  end

  def lookup_manager
    db.lookup(@employee, :manager)
  end

  def self_review
    # ...
  end
end

review = PerformanceReview.new(employee)
review.perf_review

⬆ 回到目录

评论

不要把注释过的代码留在代码库

版本控制可以解决这个问题。所以大胆的把不需要的代码删除了。

坏:

do_stuff
# do_other_stuff
# do_some_more_stuff
# do_so_much_stuff

好:

do_stuff

⬆ 回到目录

不要在代码里保存日志性的评论

记住,使用版本控制!死代码是没有必要保留的,注释过的代码,尤其是日志性的注释都是没有必要保留的。使用git log就可以看到他们的历史!

坏:

# 2016-12-20: Removed monads, didn't understand them (RM)
# 2016-10-01: Improved using special monads (JP)
# 2016-02-03: Removed type-checking (LI)
# 2015-03-14: Added combine with type-checking (JR)
def combine(a, b)
  a + b
end

好:

def combine(a, b)
  a + b
end

⬆ 回到目录

Translations

This is also available in other languages:

⬆ 回到目录*

1楼 已删除
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册