类与模块

类与模块

  • 在类定义中,使用一致的结构。
    [link]
class Person
  # 首先是 extend 与 include
  extend SomeModule
  include AnotherModule

  # 内部类
  CustomError = Class.new(StandardError)

  # 接着是常量
  SOME_CONSTANT = 20

  # 接下来是属性宏
  attr_reader :name

  # 跟着是其他宏(如果有的话)
  validates :name

  # 公开的类方法接在下一行
  def self.some_method
  end

  # 初始化方法在类方法和实例方法之间
  def initialize
  end

  # 跟着是公开的实例方法
  def some_method
  end

  # 受保护及私有的方法等放在接近结尾的地方
  protected

  def some_protected_method
  end

  private

  def some_private_method
  end
end
  • 在混入多个模块时,倾向使用多行语法。
    [link]
## 差
class Person
  include Foo, Bar
end

## 好
class Person
  include Foo
  include Bar
end
  • 如果嵌套类数目较多,进而导致外围类定义较长,则将它们从外围类中提取出来,分别放置在单独的以嵌套类命名的文件中,并将文件归类至以外围类命名的文件夹下。
    [link]
## 差

## foo.rb
class Foo
  class Bar
    # 定义 30 多个方法
  end

  class Car
    # 定义 20 多个方法
  end

  # 定义 30 多个方法
end

## 好

## foo.rb
class Foo
  # 定义 30 多个方法
end

## foo/bar.rb
class Foo
  class Bar
    # 定义 30 多个方法
  end
end

## foo/car.rb
class Foo
  class Car
    # 定义 20 多个方法
  end
end
  • 定义只有类方法的数据类型时,倾向使用模块而不是类。只有当需要实例化时才使用类。
    [link]
## 差
class SomeClass
  def self.some_method
    # 省略主体
  end

  def self.some_other_method
    # 省略主体
  end
end

## 好
module SomeModule
  module_function

  def some_method
    # 省略主体
  end

  def some_other_method
    # 省略主体
  end
end
  • 当你想将模块的实例方法变成类方法时,倾向使用 module_function 而不是 extend self。
    [link]
## 差
module Utilities
  extend self

  def parse_something(string)
    # 做一些事情
  end

  def other_utility_method(number, string)
    # 做一些事情
  end
end

## 好
module Utilities
  module_function

  def parse_something(string)
    # 做一些事情
  end

  def other_utility_method(number, string)
    # 做一些事情
  end
end
  • 当设计类的层次结构时,确保它们符合里式替换原则。
    [link]

  • 让你的类尽量满足 SOLID 原则) 。
    [link]

  • 总是替那些用以表示领域模型的类提供一个适当的 to_s 方法。
    [link]

class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def to_s
    "#{@first_name} #{@last_name}"
  end
end
  • 使用 attr 系列方法来定义琐碎的存取器或修改器。
    [link]
## 差
class Person
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end

  def first_name
    @first_name
  end

  def last_name
    @last_name
  end
end

## 好
class Person
  attr_reader :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end
  • 对于访问器方法,避免使用 get_ 作为名字前缀;对于更改器方法,避免使用 set_ 作为名字前缀。Ruby 语言中,通常使用 attr_name 作为访问器的方法名,使用 attr_name= 作为更改器的方法名。
    [link]
## 差
class Person
  def get_name
    "#{@first_name} #{@last_name}"
  end

  def set_name(name)
    @first_name, @last_name = name.split(' ')
  end
end

## 好
class Person
  def name
    "#{@first_name} #{@last_name}"
  end

  def name=(name)
    @first_name, @last_name = name.split(' ')
  end
end
  • 避免使用 attr。使用 attr_reader 与 attr_accessor 来替代。
    [link]
## 差 - 创建单个存取方法(此方法在 Ruby 1.9 之后被移除了)
attr :something, true
attr :one, :two, :three # 类似于 attr_reader

## 好
attr_accessor :something
attr_reader :one, :two, :three
  • 优先考虑使用 Struct.new。它替你定义了那些琐碎的访问器、构造器及比较操作符。
    [link]
## 好
class Person
  attr_accessor :first_name, :last_name

  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
end

## 更好
Person = Struct.new(:first_name, :last_name) do
end
  • 不要扩展 Struct.new 实例化后的对象。对它进行扩展不但引入了毫无意义的类层次,而且在此文件被多次引入时可能会产生奇怪的错误。
    [link]
## 差
class Person < Struct.new(:first_name, :last_name)
end

## 好
Person = Struct.new(:first_name, :last_name)
  • 优先考虑通过工厂方法的方式创建某些具有特定意义的实例对象。
    [link]
class Person
  def self.create(options_hash)
    # 省略主体
  end
end
## 差
class Animal
  # 抽象方法
  def speak
  end
end

## 继承父类
class Duck < Animal
  def speak
    puts 'Quack! Quack'
  end
end

## 继承父类
class Dog < Animal
  def speak
    puts 'Bau! Bau!'
  end
end

## 好
class Duck
  def speak
    puts 'Quack! Quack'
  end
end

class Dog
  def speak
    puts 'Bau! Bau!'
  end
end
  • 避免使用类变量(@@)。类变量在继承方面存在令人生厌的行为。
    [link]
class Parent
  @@class_var = 'parent'

  def self.print_class_var
    puts @@class_var
  end
end

class Child < Parent
  @@class_var = 'child'
end

Parent.print_class_var # => 此处打印的结果为 'child'

如你所见,在类的层次结构中所有类都会共享同一类变量。通常情况下,倾向使用类实例变量而不是类变量。

  • 根据方法的目的与用途设置适当的可见级别(private、protected)。不要什么都不做就把所有方法设置为 public(默认值)。毕竟我们写的是 Ruby 而不是 Python 。
    [link]

  • 把 public、protected、private 与其作用的方法缩排在同一层级。且在其上下各留一行以强调此可见级别作用于之后的所有方法。
    [link]

class SomeClass
  def public_method
    # ...
  end

  private

  def private_method
    # ...
  end

  def another_private_method
    # ...
  end
end
  • 使用 def self.method 定义类方法。这种做法使得在代码重构时,即使修改了类名也无需做多次修改。
    [link]
class TestClass
  # 差
  def TestClass.some_method
    # 省略主体
  end

  # 好
  def self.some_other_method
    # 省略主体
  end

  # 在需要定义多个类方法时,另一种便捷写法
  class << self
    def first_method
      # 省略主体
    end

    def second_method_etc
      # 省略主体
    end
  end
end
  • 在类的词法作用域中定义方法别名时,倾向使用 alias。因为定义期间 alias 与 self 指向的都是词法作用域,除非明确说明,否则该别名所引用的方法不会在运行期间被改变,或是在任何子类中被修改。
    [link]
class Westerner
  def first_name
    @names.first
  end

  alias given_name first_name
end

因为 alias 与 def 一样都是关键字,倾向使用裸字而不是符号或字符串。也就是说,使用 alias foo bar 而不是 alias :foo :bar。

另外需要了解 Ruby 是如何处理别名和继承的:别名所引用的原始方法是在定义期间被指定的,而不是运行期间。

class Fugitive < Westerner
  def first_name
    'Nobody'
  end
end

在这个例子中,Fugitive#given_name 仍然调用原先的 Westerner#first_name 方法,而不是 Fugitive#first_name。如果想要覆写 Fugitive#given_name,必须在子类中重新定义。

class Fugitive < Westerner
  def first_name
    'Nobody'
  end

  alias given_name first_name
end
  • 在运行期间定义模块方法、类方法、单件方法的别名时,总是使用 alias_method。在上述情况下,使用 alias 可能会导致预期之外的结果。
    [link]
module Mononymous
  def self.included(other)
    other.class_eval { alias_method :full_name, :given_name }
  end
end

class Sting < Westerner
  include Mononymous
end
  • 在模块方法,或是类方法内部调用自身其他方法时,通常省略模块名/类名/self。
    [link]
class TestClass
  # 差
  def self.call(param1, param2)
    TestClass.new(param1).call(param2)
  end

  # 差
  def self.call(param1, param2)
    self.new(param1).call(param2)
  end

  # 好
  def self.call(param1, param2)
    new(param1).call(param2)
  end

  # 省略其他方法
end