嘎嘎叫的小狗 - 快樂的鴨子類型
鴨子類型是我目前在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</pre>
很帥,不是嗎?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;
}</pre>
數組索引
數組索引(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]}"</pre>
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</pre>
你可以看到,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 </ul>
:)
[英文原文:Quacking The Dog - Duck typing for happiness ]