class String
  def hi
    puts "Hi, #{self}"
  end
end

"Focus".hi
#=> Hi, Focus

这是一个基本的 Ruby 打开类. 像这样打开 String 后添加 hi 方法, 会影响之后的所有 String 对象.

如果我们想减小这种影响, 可以使用 refinement 技术: refineusing.

我们定义一个 MyPatch 的命名空间(一个module):

module MyPatch
  refine String do
    def hi
      puts "Hi, #{self}"
    end
  end
end

在需要使用 hi 方法的命名空间内, 通过 using 来使其生效:

class Pet
  using MyPatch
  attr_accessor :name

  def initialize(name:)
    @name = name
  end

  def greet
    name.hi
  end

  def self.hello
    "abc".hi
  end
end

发现 hi 仅能在 Pet 内使用, 在其他的作用域 String 是没有 hi 方法的:

Pet.hello
# => Hi, abc
doudou = Pet.new(name: 'DouDou')
doudou.greet
# => Hi, DouDou
doudou.name.hi
# => undefined method `hi' for "DouDou":String (NoMethodError)

从语法上可以看到区别, class 是一个保留字, 用来把你带入到这个类的上下文中.

class 的副作用是当该类不存在时, 新建这个类; 当该类存在时, 打开这个类, 不新建类.

refine 后面还有 do, 是 Module 的一个私有实例方法:

Module.private_instance_methods.grep(/^refine/)
#=> [:refine]

using MyPatch 的影响范围被约束在 Pet 内, 可以用于 Pet 的类方法 和 Pet 的实例方法.

使用 refineusing, 可以有效减少打开类带来的副作用.

题外话

module MyPatch
  class String
    def hi
      puts "Hi, #{self}"
    end
  end
end

class Pet
  include MyPatch

  def self.hello
    'abc'.hi
  end
end

Pet.hello

如果我们这样写会怎么样呢?

undefined method hi’ for “abc”:String (NoMethodError)`

WHY?

module MyPatch
  class String
    def hi
      puts "Hi, #{self}"
    end
  end
end

class Pet
  include MyPatch

  def self.hello
    puts 'abc'.instance_of? String # false
    puts 'abc'.instance_of? ::String # true
  end
end

Pet.hello

正确的写法:

module MyPatch
  class ::String
    def hi
      puts "Hi, #{self}"
    end
  end
end

应该指明打开的是顶层的 String, 也就是 ::String, 否则, class 看到 MyPatch::String 不存在, 就新建了一个类.

使用 refine 时, 最好只添加方法, 不要覆盖方法. 因为其他方法还是会调用被覆盖的老方法, 而不是调用refine的新方法.