Effective C# 原则33:限制类型的访问

JerryXia 发表于 , 阅读 (834)

并不是所有的人都须要知道所有的事。也不是所有的类型须要是公共的。对于每个类型,在满足功能的情况下,应该尽可能的限制访问级别。而且这些访问级别往往比你想像的要少得多。在一个私有类型上,所有的用户都可以通过一个公共的接口来访问这个接口所定义的功能。

让我们回到最根本的情况上来:强大的工具和懒惰的开发人员。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创建一个公共的抽象类,它包含通用验证的实现算法。用户应该可以通过程序集的基类访问公共的功能。在这个例子中,我更喜欢用公共接口,因为即使是同样的功能,这个相对少一些。其他人可能更喜欢抽象类。不管用哪个方法实现,在程序集中尽可能少的公开类。

这些暴露在外的公共类和接口就是你的合约:你必须保留它们。越多混乱的接口暴露在外,将来你就越是多的直接受到限制。越少的公共类型暴露在外,将来就越是有更多的选择来扩展或者修改任何的实现。

添加新评论