Package vs. Namespace
我們知道,重用性(reusebility)是軟件工程中一個非常重要的目標。重用,不僅僅指自己所寫的軟件(代碼、組件等等)可以被重復利用;更廣義的重用是指不同的人,不同的團隊,不同的公司之間可以互相利用別人的成果。另外,對於大型軟件,往往是由多個團隊共同開發的,這些團隊有可能分布於不同的城市、地區、甚至國家。由於這些原因,名字管理成為一個非常重要的因素。
由於C語言本身不提供名字管理的機制(C語言的static命名解決的是可見性問題,這些名字不會輸出給外部,但我們要討論的名字空間和這個問題並不完全一樣),為了解決名字沖突的問題,大家一般會選取加前綴的方法;而前綴規則往往是: ${project name}_${name};更加安全的命名規則將前綴分了更多的級別:${project name}_${module name}_${name}。
這種方案在現實生活中有大量類似的例子。比如:中國的很多城市都有濱河路,如果你談話的對象都明白你所指的城市,你只需要說濱河路,大家都會明白你的所指。但如果情況不是這樣,你就需要加上前綴,說明這到底是樂山的濱河路,還是成都的濱河路。你在郵寄信件的時候,這一點體現的最為直接。
所以,如果存在一個全局的管理,C語言的這種方案應該是非常有效的。但它的缺陷是,這可能會造成很長的名字,而每次在引用一個名字的時候你都必須給出全名。
這是一件非常麻煩的事情。你不妨想象一下,明明大家都知道你所談的是樂山的濱河路,但你不得不在每次談到它的時候,都要說“中國四川省樂山市濱河路”,你會多么的痛苦。
為了解決這類問題,並給出一個行之有效的管理方案。隨后的編程語言,無論是C++,Java還是C#都提供了自己的名字管理的機制。這些方案在本質上有其統一的思想,但操作方式存在着一定的差異。
在前面C語言的方案里,本身體現了分級管理的方式。分級管理是一種非常自然而有效的手段。比如,互聯網的域名。它通過分級的命名保證了一個名字的全局唯一性,其排列方式是從小范圍到大范圍(這既是因為西方的書寫習慣,也是為了方便。其實從這一點上,我們可以發現,如果一個人的預讀習慣是從左到右的話,從小到大的排布方式則非常便於節省時間,比如 “濱河路,樂山市,四川省,中國”。在我們從左邊的信息已經知道我們的所指時,則可以跳過或忽略后面的信息。而從大到小的排布方式則可以避免錯誤,因為我們首先了解了限定條件,最后讀到濱河路的時候,我們已經確定我們的所指了) 。我們可以在前面加上名字,指定更小的范圍。比如:wsd.wmsg.sps.motorola.com 說明這是motorola公司的SPS部門的wmsg部門的wsd組。
Java使用這種方式來命名包(Package),只不過把書寫方式反過來。這種方式可以非常有效的保證命名的統一。比如,一個名為mlca的項目的mmi模塊包可以命名為:com.motorola.sps.wmsg.wsd.mlca.mmi,而其engine模塊包可以命名為:com.motorola.sps.wmsg.wsd.mlca.engine。
這樣,當不同的團隊,公司之間的代碼放在一起進行使用時,在一個名字不沖突的情況下,我們只需要簡單的使用它。當引起沖突的時候,我們指定其全名就行了。比如,上述的兩個包中都有一個名為Message的class,如果我們的另外一個package中的某個class要同時使用這兩個包,在引用Message類的時候,我們需要指明它來自於哪個包。如下:
import com.motorola.sps.wmsg.wsd.mlca.mmi;
import com.motorola.sps.wmsg.wsd.mlca.engine;
// 我們需要指明class Message來自於哪個包.
public class Foo extends com.motorola.sps.wmsg.mlca.mlca.mmi.Message {
...
}
而C++和C#則提供了namespace的概念來支持這種方式。你可以在全局的空間內指定自己的namespace,然后還可以在某個namespace內制定更小范圍的namespace。雖然C++和C#本身沒有推薦任何namespace的命名方式(其實反域名的方式也是Java推薦的,並非強制),但我們也可以使用上述方式。比如下面的C# code:
namespace com.motorola.sps.wmsg.wsd.mlca.mmi
{
// MMI Stuff
...
}
namespace com.motorola.sps.wmsg.wsd.mlca.engine
{
// Engine Stuff
...
}
當我們同時使用這兩個模塊時,如果出現名字沖突,也許要通過指定namespace來指明。比如:
class Foo: com.motorola.sps.wmsg.wsd.mlca.mmi.Message
{
...
}
Java的package本身沒有子包的概念,所有package都是並列的關系,沒有誰包含誰的問題。比如:org.dominoo.action和org.dominoo.action.asl之間絕對沒有包與子包的關系。它們是各自獨立的包,各自擁有自己的class/interface的集合。在org.dominoo.action.asl的某個java文件里,如果想引用org.dominoo.action里的某個class/interface,則必須import org.dominoo.action。
C++/C#的namespace方案則不然,一個namespace可以有自己的sub-namespace,我們不妨將namespace也稱為package,那么C++/C#的package之間就可能存在包與子包的關系. 比如:
namespace org.dominoo
{
}
在這個例子中,action和constraint都是org.dominoo的子包,而它們又各自擁有自己的子包asl和ocl。
所以,對於一個全局的名字空間,C語言無法直接進行名字空間分離,而Java則可以從全局的名字空間里分離出獨立的名字空間,但C++/C#則可以進一步將各個名字空間進行進一步分離。如下圖:
|------------------|
| global namespace |
|
|
|
|------------------|
|-------------------|
| global namespace |
|
| |---| |---| |---| |
| | A | | B | | C | |
| |---| |---| |---| |
|-------------------|
|----------------------------|
| global namespace
|
| |-------------| |--------| |
| | A
| | |---| |---| | | |---| | |
| | | C | | D | | | | E | | |
| | |---| |---| | | |---| | |
| |-------------| |--------| |
|----------------------------|
所以,Java的Package方案只對全局的名字空間進行了一次划分,本質上只是為語言提供了一個命名前綴方案,只是通過命名前綴的分級管理來保證名字的唯一性。它唯一的作用就是為了避免名字沖突。
而C++/C#則提供了對任何一個空間進行再次划分的能力。在Java中org.dominoo和org.dominoo.asl之間是完全沒有包含關系的各自獨立的包,但在C++/C#中,dominoo.asl則和明顯是dominoo的一個子包。
事實上,如果僅僅為了避免命名沖突,像C++/C#這樣復雜的方案並無必要,Java的方案就足夠了。但C++/C#這種方案可以帶來其它的便利:
1、軟件開發的本質就是自上而下依次分解的,每一層都有自己的定義,並且這種定義可以作為下一層所有子系統的公共服務,多層次的樹狀結構符合這種邏輯。C++/C#方案用最自然的方式滿足了這種划分關系。事實上,這種方案和文件管理的思路是一樣的。
2、一個程序一旦using哪個namespace,就可以通過它向下訪問它的子包,而無需指出全路經。比如,在上面的圖中,如果一個程序寫了using namespace A,則它在訪問C包中的class Foo時,只需要寫C::Foo,而不需要寫全路徑::A::C::Foo。在Java中,由於A,C是並列的關系,為了訪問C中的內容,必須明確指出import C。然后在訪問Foo而產生名字沖突的情況下,必須指出全路徑。
3、當程序身處某個包的時候,在不產生名字沖突的情況下,可以直接訪問外部包中的定義。由於Java包的層次只有一層,所以Java只能直接訪問global namespace中的定義,任何其它包中的定義,必須通過import才能夠訪問。
毫無疑問,C++/C#的方案更加強大靈活,但也更復雜。而復雜的東西往往讓使用者更容易犯錯誤。孰優孰劣,你自己判斷吧。