Effective C# 原則33:限制類型的訪問
Item 33: Limit Visibility of Your Types
並不是所有的人都須要知道所有的事。也不是所有的類型須要是公用的。對於每個類型,在滿足功能的情況下,應該儘可能的限制存取層級。而且這些存取層級往往比你想像的要少得多。在一個私人類型上,所有的使用者都可以通過一個公用的介面來訪問這個介面所定義的功能。
讓我們回到最根本的情況上來:強大的工具和懶惰的開發人員。VS.net對於他們來說是一個偉大的高產工具。我用VS.net或者C# Builder輕鬆的開發我所有的項目,因為它讓我更快的完成任務。其中一個加強的高產工具就是讓你只用點兩下按鈕,一個類就建立了,當然如果這正是我想要的話。VS.net為我們建立的類就是這樣的:
public class Class2
{
public Class2()
{
//
// TODO: Add constructor logic here
//
}
}
這是一個公用類,它在每個使用我的程式集的代碼塊上都是可見的。這樣的可見層級太高了,很多獨立存在的類都應該是內部(internal)的。你可以通過在已經存在的類裡嵌套一個受保護的或者私人的類來限制訪問。 越低的存取層級,對於今後的更新整個系統的可能性就越少。越少的地方可以訪問到類型,在更新時就越少的地方要修改。
只暴露須要暴露的內容,應該通過嘗試在類上實現公用介面來減少可見內容。你應該可以在.Net架構庫裡發現使用Enumerator模式的例子,System.ArrayList包含一個私人類,ArrayListEnumerator, 而就是它只實現了IEnumerator介面:
// Example, not complete source
public class ArrayList: IEnumerable
{
private class ArraylistEnumerator : IEnumerator
{
// Contains specific implementation of
// MoveNext( ), Reset( ), and Current.
}
public IEnumerator GetEnumerator()
{
return new ArrayListEnumerator( this );
}
// other ArrayList members.
}
對於我們這樣的使用者來說,不須要知道ArrayListEnumerator類,所有你須要知道的,就是當我們在ArrayList對象上調用GetEnumerator函數時,你所得到的是一個實現了IEnumerator介面的對象。而具體的實現則是一個明確的類。.Net架構的設計者在另一個集合類中使用了同樣的模式:雜湊表(Hashtable)包含一個私人的HashtableEnumerator, 隊列(Queue)包含一個QueueEnumerator, 等等。私人的枚舉類有更多的優勢。首先,ArrayList類可以完全取代實現IEnumerator的類型,而且你已經成為一個賢明的程式員了,不破壞任何內容。其實,列舉程式類不須要是CLS相容的,因為它並不是公用的(參見原則30)。而它的公用介面是相容的。你可以使用列舉程式而不用知道實現的類的任何細節問題。
建立內部的類是經常使用的用於限制類型可見範圍的概括方法。預設情況下,很多程式員都總是建立公用的類,從來不考慮其它方法。這是VS.net的事。我們應該取代這種不加思考的預設,我們應該仔細考慮你的類型會在哪些地方使用。它是所有使用者可見的?或者它主要只是在一個程式集內部使用?
通過使用介面來暴露功能,可以讓你更簡單的建立內部類,而不用限制它們在程式集外的使用(參見原則19)。類型應該是公用的呢?或者有更好的介面彙總來描述它的功能?內部類可以讓你用不同的版本來替換一個類,只要在它們實現了同樣的介面時。做為一個例子,考慮這個電話號碼驗證的問題:
public class PhoneValidator
{
public bool ValidateNumber( PhoneNumber ph )
{
// perform validation.
// Check for valid area code, exchange.
return true;
}
}
幾個月過後,這個類還是可以很好的工作。當你得到一個國際電話號碼的請求時,前面的這個PhoneValidator就失敗了。它只是針對US的電話號碼的。你仍然要對US電話號碼進行驗證,而現在,在安裝過程中還要對國際電話號碼進行驗證。與其粘貼額外的功能代碼到一個類中,還不如了斷減少兩個不同內容耦合的做法,直接建立一個介面來驗證電話號碼:
public interface IPhoneValidator
{
bool ValidateNumber( PhoneNumber ph );
}
下一步,修改已經存在的電話驗證,通過介面來實現,而且把它做為一個內部類:
internal class USPhoneValidator : IPhoneValidator
{
public bool ValidateNumber( PhoneNumber ph )
{
// perform validation.
// Check for valid area code, exchange.
return true;
}
}
最後,你可以為國際電話號碼的驗證建立一個類:
internal class InternationalPhoneValidator : IPhoneValidator
{
public bool ValidateNumber( PhoneNumber ph )
{
// perform validation.
// Check international code.
// Check specific phone number rules.
return true;
}
}
為了完成這個實現,你須要建立一個恰當的類,這個類基於電話號碼類型類,你可以使用類廠模式實現這個想法。在程式集外,只有介面是可見的。而實際的類,就是這個為世界不同地區使用的特殊類,只有在程式集內是可見的。你可以為不同的地區的驗證建立不同的驗證類,而不用再系統裡的其它程式集而煩擾了。
你還可以為PhoneValidator建立一個公用的抽象類別,它包含通用驗證的實現演算法。使用者應該可以通過程式集的基類訪問公用的功能。在這個例子中,我更喜歡用公用介面,因為即使是同樣的功能,這個相對少一些。其他人可能更喜歡抽象類別。不管用哪個方法實現,在程式集中儘可能少的公開類。
這些暴露在外的公用類和介面就是你的合約:你必須保留它們。越多混亂的介面暴露在外,將來你就越是多的直接受到限制。越少的公用類型暴露在外,將來就越是有更多的選擇來擴充或者修改任何的實現。
======================================
Item 33: Limit Visibility of Your Types
Not everybody needs to see everything. Not every type you create needs to be public. You should give each type the least visibility necessary to accomplish your purpose. That's often less visibility than you think. Internal or private classes can implement public interfaces. All clients can access the functionality defined in the public interfaces declared in a private type.
Let's get right to the root cause: powerful tools and lazy developers. VS .NET is a great productivity tool. I use it or C# Builder for all my development simply because I get more done faster. One of the productivity enhancements lets you create a new class with two button clicks. If only it created exactly what I wanted. The class that VS.NET creates looks like this:
public class Class2
{
public Class2()
{
//
// TODO: Add constructor logic here
//
}
}
It's a public class. It's visible to every piece of code that uses the assembly I'm creating. That's usually too much visibility. Many standalone classes that you create should be internal. You can further limit visibility by creating protected or private classes nested inside your original class. The less visibility there is, the less the entire system changes when you make updates later. The fewer places that can access a piece of code, the fewer places you must change when you modify it.
Expose only what needs to be exposed. Try implementing public interfaces with less visible classes. You'll find examples using the Enumerator pattern throughout the .NET Framework library. System.ArrayList contains a private class, ArrayListEnumerator, that implements the IEnumerator interface:
// Example, not complete source
public class ArrayList: IEnumerable
{
private class ArraylistEnumerator : IEnumerator
{
// Contains specific implementation of
// MoveNext( ), Reset( ), and Current.
}
public IEnumerator GetEnumerator()
{
return new ArrayListEnumerator( this );
}
// other ArrayList members.
}
Client code, written by you, never needs to know about the class ArrayListEnumerator. All you need to know is that you get an object that implements the IEnumerator interface when you call the GetEnumerator function on an ArrayList object. The specific type is an implementation detail. The .NET Framework designers followed this same pattern with the other collection classes: Hashtable contains a private HashtableEnumerator, Queue contains a QueueEnumerator, and so on. The enumerator class being private gives many advantages. First, the ArrayList class can completely replace the type implementing IEnumerator, and you'd be none the wiser. Nothing breaks. Also, the enumerator class need not be CLS compliant. It's not public (see Item 30.) Its public interface is compliant. You can use the enumerator without detailed knowledge about the class that implements it.
Creating internal classes is an often overlooked method of limiting the scope of types. By default, most programmers create public classes all the time, without any thought to the alternatives. It's that VS .NET wizard thing. Instead of unthinkingly accepting the default, you should give careful thought to where your new type will be used. Is it useful to all clients, or is it primarily used internally in this one assembly?
Exposing your functionality using interfaces enables you to more easily create internal classes without limiting their usefulness outside of the assembly (see Item 19). Does the type need to be public, or is an aggregation of interfaces a better way to describe its functionality? Internal classes allow you to replace the class with a different version, as long as it implements the same interfaces. As an example, consider a class that validates phone numbers:
public class PhoneValidator
{
public bool ValidateNumber( PhoneNumber ph )
{
// perform validation.
// Check for valid area code, exchange.
return true;
}
}
Months pass, and this class works fine. Then you get a request to handle international phone numbers. The previous PhoneValidator fails. It was codedto handle only U.S. phone numbers. You still need the U.S. Phone Validator, but now you need to use an international version in one installation. Rather than stick the extra functionality in this one class, you're better off reducing the coupling between the different items. You create an interface to validate any phone number:
public interface IPhoneValidator
{
bool ValidateNumber( PhoneNumber ph );
}
Next, change the existing phone validator to implement that interface, and make it an internal class:
internal class USPhoneValidator : IPhoneValidator
{
public bool ValidateNumber( PhoneNumber ph )
{
// perform validation.
// Check for valid area code, exchange.
return true;
}
}
Finally, you can create a class for international phone validators:
internal class InternationalPhoneValidator : IPhoneValidator
{
public bool ValidateNumber( PhoneNumber ph )
{
// perform validation.
// Check international code.
// Check specific phone number rules.
return true;
}
}
To finish this implementation, you need to create the proper class based on the type of the phone number. You can use the factory pattern for this purpose. Outside the assembly, only the interface is visible. The classes, which are specific for different regions in the world, are visible only inside the assembly. You can add different validation classes for different regions without disturbing any other assemblies in the system. By limiting the scope of the classes, you have limited the code you need to change to update and extend the entire system.
You could also create a public abstract base class for PhoneValidator, which could contain common implementation algorithms. The consumers could access the public functionality through the accessible base class. In this example, I prefer the implementation using public interfaces because there is little, if any, shared functionality. Other uses would be better served with public abstract base classes. Either way you implement it, fewer classes are publicly accessible.
Those classes and interfaces that you expose publicly to the outside world are your contract: You must live up to them. The more cluttered that interface is, the more constrained your future direction is. The fewer public types you expose, the more options you have to extend and modify any implementation in the future.