嘎嘎叫的小狗 - 快乐的鸭子类型
鸭子类型是我目前在Ruby语言里最喜欢的一个“功能特征”,主要是因为它能让我们轻松的写出漂亮的代码——毕竟,你无需再担心类型:你可以把精力全部集中到你想发送的消息上,以及你需要打交道的对象能发挥的功能上。
我第一次接触Ruby时就知道它是一种“鸭子类型语言”,但我的静态编译型语言的背景知识妨碍了我真正理解鸭子类型的真正含义。理论很简单:如果你 设计一个方法,它需要一个‘鸭子’参数,那么你呼叫一声“嘎嘎”,任何以“嘎嘎”回应你的对象都可以传入这个方法——这个对象究竟是什么类型并不重要。很 显然,你可以得出这样的结论,如果你写出一个Dog类,它实现了一个叫“嘎嘎”的方法(很奇怪的狗),那么,你可以把这个狗传入上面的那个方法,一点问题 都没有。非常酷吧,
鸭子类型的强大功能震撼了我,我认识到,它在各种对象间打通了一条重要的沟通途径,强化了API的能力,减少了代码中的干扰。为了说明这些,让我来展示一些Ruby标准库中的几个例子。
File.open
File.open(“path/to/file”)最常见的读取文件的方法:你传入path,这个方法会返回一个能读取文件的对象。你是否注意到,我加粗强调了“path”这个词。这是特意的——这个‘open’函数实际可以接受任何可以扮演路径角色的东西,并不仅仅指路径字符串。这区别有些微妙,但你会发现我们可以把代码这样写:
class VimConfig # ... behavior ... # def to_path "~/.vimrc" end end config = VimConfig.new config_file = File.open config
很帅,不是吗?Ruby的File API在使用它的参数前会进行转化,转化的一种途径是通过‘to_path’方法。如果你感到奇怪,下面是实现它的C语言代码(‘rb_f_open’ 调用 ‘FilePathValue’,后者最终调用 ‘rb_get_path_check_to_string’)):
static VALUE rb_f_open(int argc, VALUE *argv) { ID to_open = 0; int redirect = FALSE; if (argc >= 1) { CONST_ID(to_open, "to_open"); if (rb_respond_to(argv[0], to_open)) { redirect = TRUE; } else { VALUE tmp = argv[0]; FilePathValue(tmp); if (NIL_P(tmp)) { redirect = TRUE; } else { VALUE cmd = check_pipe_command(tmp); if (!NIL_P(cmd)) { argv[0] = cmd; return rb_io_s_popen(argc, argv, rb_cIO); } } } } if (redirect) { VALUE io = rb_funcall2(argv[0], to_open, argc-1, argv+1); if (rb_block_given_p()) { return rb_ensure(rb_yield, io, io_close, io); } return io; } return rb_io_s_open(argc, argv, rb_cFile); } VALUE rb_get_path_check_to_string(VALUE obj, int level) { VALUE tmp; ID to_path; if (insecure_obj_p(obj, level)) { rb_insecure_operation(); } if (RB_TYPE_P(obj, T_STRING)) { return obj; } CONST_ID(to_path, "to_path"); //to_path call! tmp = rb_check_funcall(obj, to_path, 0, 0); if (tmp == Qundef) { tmp = obj; } StringValue(tmp); return tmp; }
数组索引
数组索引(a_array[index])是另外一个很好的例子:它会向索引调用‘to_int’方法,所以,任何能响应to_int方法的对象都可以当作索引。这让我们可以这样写:
class PodiumPosition # .. behavior .. # def to_int @race_position end end position = PodiumPosition.new(1) prizes = [ "orange", "apple", "corn" ] puts "Congrats, you won #{prizes[position]}"
IO.select
我是通过IO.select API才第一次发现了Ruby的强大。这个API会调用系统select(2)函数,接收文件描述符参数,并挂起当前的线程,直到有文件可以进行读写操作。这个Ruby函数定义如下:
select(read_array [, write_array [, error_array [, timeout]]]) → array or nil
因此,你可以传入一个数据流数组,而“select”函数会一直等到流文件准备好可读或可写。问题是,很多数据流是存储在具有各种行为特征的特定对 象里的(例如一个执行网络操作的Connection类),这些对象里的IO接口通常经过了二次封装,外界无法直接访问。根本不可能通过重构内核代码来适 应‘select’ API。打破它的封装吗?很显然不行!这时‘to_io’方法就成了救星!
class Connection # .. rest of the class .. # def accept_connection(io) @io = io # new connection code end def to_io @io end end class Reactor # array_of_connections_to_read is an array of instances of the above Connection class # array_of_connections_to_write is an array of instances of the above Connection class def tick to_read, to_write = IO.select(array_of_connections_to_read, array_of_connections_to_write) end end
你可以看到,Ruby的标准库里到处都是鸭子类型.
重构
最明显,也是最值得一提的鸭子类型的好处是,它让重构变得更容易:“用多型替换条件判断” 和 “Replace Type Code with Strategy/State”的重构原则,当你不需要考虑类型、只关心行为时,这些都变得极其简单和容易实现。
鸭子类型的黑暗面
没有编译器为你探路是很危险的。专业的Ruby程序员(1)永远不会忘记有责任测试它们的代码的各种行为,并且(2)一定写出整洁的代码,并及时重构。Ruby代码必须要认真写,否者调试起来就会是一场噩梦。
同时,动态语言一般最合适的是开发小型或中型软件。我的经验告诉我,当系统变得复杂时,最好把它拆分成小的应用,如果是用动态语言开发的,那这种做法更加重要——一个reddit的网友说需要在一个10万行的程序了修改一个函数的名称,我只能说,这很难实现。修改公开的接口,这很难很难。有时最好把它标注为‘废弃’就行了。
结论
动态语言能漂亮的解决你的问题,但需要有很好的设计,Ruby的标准库里鸭子类型为我们提供了方便的途径。它是一个很好的例子,向我们展示了一个Ruby程序员该如何的编程:按对象的行为——而不是按对象的类型——来接收参数。
我希望这篇文章给那些仍然不明白像Ruby这样的语言的强大之处的人带来新的认识。我推荐阅读下面几本书来进一步的学习:
- Practical Object-Oriented Design in Ruby, from Sandi Metz
- Confident Ruby, from Avdi Grimm
- Object on Rails, 同样来自 Avdi Grimm
:)
[英文原文:Quacking The Dog - Duck typing for happiness ]