本節書摘來自華章出版社《effective ruby:改善ruby程式的48條建議》一書中的第2章,第2.6節,作者 [美]彼得 j.瓊斯(peter j. jones),更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視
假設你正在做一個訂購個性化筆記本(那種過時的紙質筆記本)的應用程式。客戶能夠在衆多裝訂方式中選擇,如使用金屬釘針裝訂或使用傳統的膠水裝訂。你決定建立一個類來表示裝訂類型,并将其他參數一同放在裡面。然而很遺憾,事情并非計劃的那樣。下面的類定義有什麼問題呢?

乍一看,似乎什麼都是對的。這是沒有文法錯誤的,但如果你在irb中運作這個類,你會看到還真有點問題。當你執行這段代碼來建立新的對象時,你會發現事實和預期不同。然而如果這段代碼不是在定義一個類那它又做了些什麼呢?這是個值得思考的問題。
如你所知,ruby中的類是可變的——你能在任何時間增加和替換方法。建立類的文法和修改類是相同的。在本例中,你想建立的類的名字恰好已經被建立過了。binding是ruby核心類庫中的一個類。上面的代碼不是定義一個新類,而是打開了本就存在的binding類并修改了它。與你想的顯然不同。
任何卓越的軟體早晚都會在運作中遇到問題。尤其是對庫的作者來說。如果多個庫中存在相同名字的類會發生什麼呢?你如何同時使用兩個這樣的庫呢?還好,幾乎所有現代的通用語言都有解決這個問題的方式,包括ruby。ruby通過命名空間的作用域将名字進行分隔。
命名空間是一種保證常量唯一性的方式。其最基本的功能是建立作用域進而定義互不沖突的常量。由于類和子產品的名字都是常量,是以命名空間能用來很好地隔離它們。
所有的核心ruby類都存在于被稱為“全局”的命名空間内。我們之後再說它的含義,現在隻要知道這些名字代表的每個類都無需使用限定符即可使用就可以了。換句話說,如果你啟動irb并鍵入array,意味着“類array是在全局命名空間裡的”。當你不加命名空間地定義類時,這個類就被放在了全局命名空間裡。同時存在與已有名字發生沖突的風險。
建立一個新類并将其置于自定義命名空間是非常簡單的,隻要将類定義嵌入在一個子產品中即可。
把類定義嵌入子產品中使binding類和核心類差別開來,就避免了同名造成的困擾。引用新類你需要引用子產品名并使用“類路徑分隔符”,也就是兩個冒号:
使用子產品建立命名空間不僅限于保護類,該技術也可以用來把其他常量和子產品方法放入命名空間。你還能将子產品嵌入其他子產品中來建立任意深的命名空間。這在大型應用程式和庫中作為代碼隔離和子產品化的一種方式。
使用命名空間時,常用的實踐是在項目的檔案系統上使用和命名空間一樣的目錄結構。比如,上述類的定義應該能在notebooks檔案夾下的bindings.rb檔案中找到。換句話說,名字notebooks::bindings映射着檔案notebooks/bindings.rb。
有時在子產品中的嵌入類非常煩瑣并會導緻不必要的縮進。存在一個替代文法可以在命名空間裡建立類。但它僅在命名空間已經存在時有效,即如果用來建立命名空間的子產品已經在前面定義過,那麼你可以在類定義中直接使用子產品名和類路徑分隔符。
這種文法的類定義的典型應用場景是,你先在入口源檔案中定義了命名空間子產品,随後加載所有剩下的源檔案。但是要小心,在沒有預先定義命名空間時就嘗試使用這種文法會導緻一個nameerror異常。ruby如果找不到引用的常量就會引發這個異常。将常量嵌套在另一個常量中會為你在程式中如何限定常量帶來一點複雜性,不過如果明白了ruby如何搜尋它們,就不會有這樣的疑慮了。
ruby使用兩種技術尋找常量。第一種是,檢查目前詞法作用域和所有閉包詞法作用域。(我們将在稍後探索詞法作用域。)如果無法找到這個常量,ruby将循着繼承體系繼續尋找。這就是為什麼你可以在子類中使用父類定義的常量。如我們将要看到的,這也是為什麼我們可以使用所謂的“全局”常量。
談到命名空間,我們最關心的就是詞法作用域,表示實際定義或引用常量的位置。看代碼顯然比讀這些文字容易些,那麼請看:
子產品定義中建立了一個詞法作用域。由于常量key和類encrypt都定義在同一個詞法作用域中,是以initialize方法能夠在不加限定符(使用常量全路徑)的情況下就使用常量key。明白詞法作用域不同于子產品建立的命名空間這一點非常重要。它與常量定義和使用的實體位置有關。如果我們稍做改變,你就可以看出其差別:
這一次命名空間和詞法作用域都使用子產品來定義,不過它們都在建立完常量key之後立即關閉了。類定義在正确的命名空間裡,隻是它并沒有和key共享同一個詞法作用域,是以不能不加限定符就使用它。因為ruby在詞法作用域或其繼承體系中無法找到常量key,是以這段代碼會引發nameerror異常。修複這個問題很簡單,隻要引用常量時加上限定符即可:
這看起來有點古怪,不過它就是這麼運作的。還有更奇怪的呢,既然常量已經加上了限定符,它卻是在繼承體系中被找到的而非詞法作用域。常量superdumbcrypto可以被看作全局常量,但當它出現時,ruby還沒有全局命名空間。這時所有頂級常量都被存在object類中。由于ruby中幾乎所有的東西都繼承自object,是以可以通過繼承體系找到所有頂級常量。這就解釋了為什麼ruby會在兩個地方尋找它們:目前的詞法作用域以及繼承體系。
當使用命名空間時還有最後一處可能遇到的坑,如下代碼所示:
上面的代碼定義了cluster::array類,它需要用到頂級類array。根據常量的檢索規則,目前上下文中沒有限定符的常量array表示類cluster::array,而這并不是我們期望的。解決方法是:為常量array加上限定符。由于這是頂級常量,我們知道它們被存儲在類object中,是以限定符常量的名字将是object::array。這看起來有點怪,是以ruby允許我們簡寫為::array。下面的代碼就是我們想要的:
命名空間雖然增加了些許複雜性(對于不加限定符的常量)但這個特性是值得的。任何卓越的ruby項目都必然會使用它,尤其是被打成gem包的庫。我們預期這類庫會把它們的常量放在和庫名相比對的命名空間裡。通過建立和使用命名空間,能夠和其他程式友好相處。
要點回顧
通過在子產品中嵌入代碼來建立命名空間。
讓你的命名空間結構和目錄結構相同。
如果使用時可能出現歧義,可使用“::”來限定頂級常量(比如,::array)。