# 原则21:限制你的类型的可见性
**By D.S.Qiu**
**尊重他人的劳动,支持原创,转载请注明出处:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
不是每个人都需要知道每件事。你创建的类型也不是都需要 public 。只要能达到你的目的,你就应该让你的每个类都是最小的可见性。那总是会比你认为的更小的可见性。 internal 和 private 类可以实现 public 接口。所有客户可以访问 private 类的 public 函数接口。
创建 public 类型太简单不过了。而且,它经常只是一个权宜之计。很多单独的类型你都创建为 internal 。你也通过在类里面创建 protected 或者 private 类更进一步限制可见性。当你更新时,可见性越小,整个系统改变就越小。当你修改代码时,访问的代码越少,改变的地方就越少。
只需要暴露才暴露。实现 public 接口尽量访问更少的类。你会发现 Enumerator 模式的使用贯穿 .NET 框架库。System.Collections.Generic.List<T> 包含一个 private 类 Enumerator<T> ,它实现 IEnumerator<T> 接口:
```
// For illustration, not complete source
public class List<T> : IEnumerable<T>
{
private class Enumerator<T> : IEnumerator<T>
{
// Contains specific implementation of
// MoveNext(), Reset(), and Current.
public Enumerator(List<T> storage)
{
// elided
}
}
public IEnumerator<T> GetEnumerator()
{
return new Enumerator<T>(this);
}
// other List members.
}
```
当你写客户端代码时,不需要知道 Enumerator<T> 类。你只需要知道调用 List<T> 对象是实现 IEnumerator<T> 接口的可以调用 GetEnumerator 函数。具体哪的类型就是实现上的细节。 .NET 框架设计者对其他集合类也遵循同样的模式: Dictionary<T> 包含一个 private DictionaryEnumerator<T> ,Queue<T> 包含一个 QueueEnumerator<T> 等等。这个迭代类是 private 会有很多好处。首先,List<T> 可以代替任何实现 IEnumerator<T> 的类,并且一点都不会不明智。没有破坏任何东西。而且 Enumerator 类不需要和 公共语言规范(CLS)的保持一致。 public 接口才需要。你使用 Enumerator 类不需要知道实现类的详细细节。
创建 internal 类是一个经常被忽略限制类作用域的方法。默认情况下,大多数程序员总是创建 public 类,而没有想过其他替代方案。那是 VS.NET 向导的事情。不要不假思索地接受默认设置,你应该仔细考虑将要使用的新的类型。它是不是对客户很有用的,或是它主要用在这个程序集的内部使用?
通过接口暴露你的功能,使得你更容易创建内部类而不限制其在程序集外部的使用(查看原则26)。这类型应该是公共的,还是一个聚集的接口的一个更好的方式来描述它的功能?interal 类允许你使用不同的版本的类,只要它实现了相同的接口。来看下面的例子,考虑一类验证电话
号码的:
```
public class PhoneValidator
{
public bool ValidateNumber(PhoneNumber ph)
{
// perform validation.
// Check for valid area code, exchange.
return true;
}
}
```
几个月过去了,这个类还能很好的工作。后来你接到需要处理国际电话号码的需求。上面的号码验证就会失败。它只能处理 U.S. 的电话号码。你仍需要验证 U.S. 电话,但是你需要在同一个软件中使用国际版本。你最好能降低不同项目之间的耦合,而不是在同一个类增加额外的函数。你可以定义一个验证号码的接口:
```
public interface IPhoneValidator
{
bool ValidateNumber(PhoneNumber ph);
}
```
下一步,让已经存在的号码验证器类实现这个接口,并且声明为 internal 类:
```
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;
}
}
```
为了完成这个功能,你需要创建恰当的基于号码类型的类。你可以使用工厂模式达到这个目的。在程序集外,只有接口是可见的。这个类,具体于世界不同区域,只有在程序集内部可见。你可以为不同区域添加不同的验证类而不干扰系统里任何其他程序集。通过限制类的作用域,你就已经很好地限制因更新的代码和扩展整个系统的改变。
你也可以创建一个号码验证器的 public abstract 基类,包含了通用算法的实现。使用者可以通过可访问的基类访问 public 的功能。在这个例子中,我更喜欢使用 public 接口实现因为只有很少,共享的功能。其他情况使用 public abstract 基类会更好。无论你实现哪种方式,越少类是 public 可访问的。
此外,越少 public 类就越少的 public 区域这将有助于覆盖率单元测试。如果有更少的 public 类,就更少需要创建测试用例的 public 可访问的方法。同时,如果通过接口暴露更多的公共 API ,你有自动创建一个系统,可以替代那些用于单元测试使用的备份。
你暴露给外部的那些类和接口是你的约定:你必须依靠它们。接口越混乱,你将来受到的约束就越多。暴露越少 public 类,你将来会有更多扩展和修改任何实现的选择。
小结:
这个原则很简单却很重要,在软件系统架构中相当重要,永远记住暴露越少的 public 接口,后续你的痛苦就会越少。接口定义规范,基类实现公有功能!
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/2083197](/blog/2083197)
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数