面向对象的编程模型常常会带给人一种误解,即_“一个对象的类型就是它的类”_——对象是类的实例,类是对象的类型。
其实这并不完全正确。在大多数典型的强类型语言(如:Java)中,为了良好的构造类与对象的对应关系,每一个对象的生成都要声明其构造类的名称,这种机制正是带给人上述误解的根源。
然而面向对象的编程并不一定非要限制对象的类型如何,更为重要的是对象所表现出的行为而不是它们的身份。我们把这种理念称之为_“鸭子类型”_,它在 Ruby 语言中体现的尤其淋漓尽致。
如果一个对象走起来像鸭子,叫起来也像鸭子,那么解释器就认为它是一只鸭子。
这里面关键的逻辑就在于:不是因为一个对象是鸭子
(类),我们才定义它如何走
与如何叫
(属性/方法),而是因为一个对象的走
和叫
(属性/方法)都和鸭子
(类)表现得一样,所以我们就认为它是一只鸭子。
因此,即使该对象真的不是从鸭子
类里实例化出来的,但它能够做和鸭子
一模一样的事情,那么我们在代码里就可以把它当做一只鸭子来看待。当然这是比较罕见的现象,因为设计良好的代码肯定在逻辑上也是禁得起推敲的,但这里的重点是:我们使用对象的目的是“做应该做的事情”,“对象究竟是什么”反而不那么重要。
这一点在写测试的时候非常易于体现,实际上我们写测试的时候就应该专注于去描述“代码应该_做_什么”,而不是“代码应该_是_什么”。
用一个较完整的示例来说明这一点吧。
class Customer
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
def append_fullname_to_file(file)
file << "#{@firstname} #{@last_name}"
end
end
很简单的一个类,我们要关注的是 append_fullname_to_file
方法的测试。由代码可知该方法的目的是构造 customer
的全名并将其追加到一个文件的末尾,如果我们头脑里考虑的是“代码是什么?”这个问题,那我们肯定会写出如下的测试来:
require 'minitest/autorun'
require_relative 'customer'
class TestCustomer < MiniTest::Test
def test_append_fullname_to_file
customer = Customer.new("Cici", "Hu")
file = File.open("tmpfile", "w") do |f|
customer.append_fullname_to_file(f)
end
file = File.open("tmpfile") do |f|
assert_equal("Cici Hu", f.gets)
end
ensure
File.delete("tmpfile") if File.exist?("tmpfile")
end
end
end
很自然,对吧?因为我们要把全名追加到一个文件中,那自然需要一个文件咯。但测试并非真实环境,所以我们不得不建立一个临时文件来模拟写入和读取,并且在测试的最后还得将这个临时文件删除掉。说真的,这个测试写得并不怎么样!
仔细想想——忘记“代码是什么”,考虑“代码做什么”——尽管在实际使用的时候我们的确操作的是一个文件,但对于测试来说我们只需要达成以下两个目标就好了:
- 全名需要放入一个“容器”;
- 可以从“容器”中读取全名——这是为了断言需要。
我们在真实代码中所考虑的“容器”是一个文件(类型),但测试的时候真的就必须是这个文件(类型)吗?
当然不!文件里保存的无非就是字符,在上例中我们使用 File.open
来读取文件的内容做断言,实际上读取出来的正是我们写入进去的全名(字符串)。既然如此,为什么一定要用一个文件呢?直接用字符串可不可以?
require 'minitest/autorun'
require_relative 'customer'
class TestCustomer < MiniTest::Test
def test_append_fullname_to_file
customer = Customer.new("Cici", "Hu")
file = ""
customer.append_fullname_to_file(file)
assert_equal("Cici Hu", file)
end
end
这样简单多了,对吗?在这个测试里你就可以把文件想象为一个字符串,因为对于 append_fullname_to_file
方法而言它需要的仅仅是一个“容器”而已,只不过我们在现实中选择了用独立的文件来做这个“容器”;然而从对象的行为角度来考虑,在此例中一个字符串和一个文件都可以很好地做到这一点。
所以,换成数组其实也是一样的:
require 'minitest/autorun'
require_relative 'customer'
class TestCustomer < MiniTest::Test
def test_append_fullname_to_file
customer = Customer.new("Cici", "Hu")
file = []
customer.append_fullname_to_file(file)
assert_equal(["Cici Hu"], file)
end
end
实际上,考虑到会在文件中保存多个用户的全名,数组反而会更方便测试。
这就是鸭子类型:关注代码的“行为”而不是代码的“身份”,这是面向对象编程的一条重要理念。