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.