第 9 章:常用的设计模式
9.1 聚合组件
考虑为常用的特性域提供聚合组件。
要用聚合组件来对高层的概念(物理对象)进行建模,而不是对系统级的任务进行建模。
要让聚合组件的名字与众所周知的系统实体相对应,比如 MessageQueue、Process 或 EventLog,这样就能使类型更加引人注目。
要在设计聚合组件时使初始化尽可能地简单,这样用户只需进行简单的初始化就可以使用组件。如果某一项初始化是必需的,那么由于没有对组件进行初始化而引发的异常应该明确地告诉用户应该怎么做。
不要要求聚合组件的用户在一个场景中显式地实例化多个对象。
要保证让聚合组件支持 Create-Set-Call 使用模式,这样用户就可以先实例化组件,然后设置它的属性,最后调用一些简单的方法,以实现大多数场景。
要为所有的聚合组件提供默认构造函数或非常简单的构造函数。
要为聚合组件提供可读写的属性来与构造函数中的所有参数相对应。
要在聚合组件中使用事件,不要使用基于委托的 API。
考虑用事件来代替需要被覆盖的虚成员。
不要要求聚合组件的用户在常用场景中使用继承、覆盖方法及实现接口。
不要要求聚合组件的用户在常用场景中除了编写代码之外,还要做其他的做工。例如,不应该让用户用配置文件来配置组件,也不应该让用户生成资源文件,等等。
考虑让聚合组件能够自动切换状态。
不要涉及有多种状态的因子类型。
考虑将聚合组件集成到 VS 的设计器中。
考虑把聚合组件和因子类型分开,各自放在不同的程序集中。
考虑把聚合组件内部的因子类型暴露给外界访问。
9.2 Async 模式
要实现基于事件的 Async 模式 - 如果类型是一个支持可视化设计器的组件(也就是说类型实现了 IComponent)。
要实现经典的 Async 模式 - 如果必须支持等待句柄。
考虑在实现高层 API 时使用基于事件的 Async 模式。例如,聚合组件就应该实现该模式。
考虑在实现底层 API 时使用经典的 Async 模式,在这种情况下更强大的功能、更少的内存消耗、更好的灵活性、更少的磁盘占用要比可用性更重要。
避免在同一个类型中甚至是一组相关的类型中同时实现两种 Async 模式。
要在为异步操作定义 API 时遵循下面的约定。给定名为 Operation 的同步方法,应该提供名为 BeginOperation 和 EndOperation 的方法,它们的方法签名如下面所示(注意,输出参数不是必需的)。
要确保 Begin 方法的返回类型实现了 IAsyncResult 接口。
要确保同步方法的按值传递和按引用传递的参数在 Begin 方法中都是按值传递的。同步方法的输出参数不应该出现在 Begin 方法的签名中。
要确保 End 方法的返回类型与同步方法的返回类型相同。
要确保同步方法的任何输出参数和按引用传递的参数都作为 End 方法的输出参数。同步方法中安置传递的参数不应该出现在 End 方法的签名中。
不要继续执行异步操作 - 如果 Begin 方法抛出了异常。
要一次通过下面的机制来通知调用方异步操作已经完成。
将 IAsyncResult.IsCompleted 设为 true。
激活 IAsyncResult.AsyncWaitHandle 返回的等待句柄。
调用异步回调函数。
要通过从 End 方法中抛出异常来表示无法成功地完成异步操作。
要在 End 方法被调用时同步完成所有尚未完成的操作。。
考虑抛出 InvalidOperationException 异常 - 如果用户用同一个 IAsyncResult 两次调用 End 方法,或 IAsyncResult 是从另一个不相关的 Begin 方法返回的。
要把 IAsyncResult.CompletedSynchronously 设为 true - 当且仅当异步回调函数将在调用 Begin 方法的线程中运行的时候。
要确保在正确的线程中调用事件处理程序。与经典 Async 模式相比,这是使用基于事件的 Async 模式的主要好处之一。
要确保无论是操作已经完成,还是操作出错,还是操作被取消,都是种会调用事件处理程序。不应该让应用程序无休止地等待一间永远不会发生的事件
要确保在异步操作失败后,访问时间参数类的属性会引发异常。换句话说,如果有错误导致操作无法完成,那么就不应该允许用户访问操作的结果。
不要为返回值为空的方法定义新的事件处理程序或事件参数类型。要使用 AsyncCompletedEventArgs,AsyncCompletedEventHandler 或 EventHandler<AsyncCompletedEventArg>。
要确保如果在一个一步操作中实现了 PaogressChanged 事件,那么在操作的完成事件被触发之后,不应该再出现此类事件。
要确保如果使用了标准的 ProgressChangedEventArgs,那么 ProgressPercentage 始终能用来表示进度的百分比(不一定要完全精确,但表示的一定要百分比)。如果使用的不是标准进度,那么从 ProgressChangedEventArgs 派生一个子类会更合适,这种情况下应该保持 ProgressPercentage 为 0 ;
要在有增量结果需要报告的时候出发 ProgressChanged 事件。
要对 ProgressChangedEventArgs 进行扩展来保存增量结果数据,并用扩展后的时间参数类来定义 ProgressChanged 事件。
要把增量结果报告与进度报告分开。
要为每个异步操作定义单独的 <MethodName>ProgreessChanged 事件和相应的事件参数类,来处理该操作的增量结果数据。
9.3 依赖属性
要提供依赖属性 - 如果需要用他们来支持各种 WPF 特性,比如样式、触发器、数据绑定、动画、动态资源以及继承。
要在设计依赖属性的时候继承自 DependencyObject 或它的子类型。该类型实现的属性存储区非常高效,它还自动支持 WPF 的数据绑定。
要为每个依赖属性提供常规的 CLR 属性和存放 System.Windows.DependencyProperty 实例的公有静态只读字段。
要通过调用 DependencyObject.GetValue 和 DependencyObject.SetValue 的方式来实现依赖属性。
要用依赖属性的名字加上“Property”后缀来命名依赖属性的静态字段。
不要显式地在代码中设置依赖属性的默认值,应该在元数据中设置默认值。
不要在属性的访问器中添加额外的代码,而应该使用标准代码来访问静态字段。
不要使用依赖属性来保存保密数据。任何代码都能访问依赖属性,即使它们是私有的。
不要把依赖属性的验证逻辑放在访问器中,而应该把验证毁掉函数传给 DependencyProperty.Register 方法。
不要在依赖属性的访问器中实现属性改变的通知,而应该向 PropertyMetadata 注册改变通知的回调函数,后者是依赖属性本身提供的一项特性,为了支持改变通知,必须使用该特性。
不要在依赖属性的访问器中实现属性强制赋值逻辑,而应该向 PropertyMetadata 注册强制赋值的回调函数。后者是依赖属性本身提供的一项特性,为了支持强制赋值,必须使用该特性。
9.4 Disopse 模式
要为含有可处置类型实例的类型实现基本 Dispose 模式。
要为类型实现基本 Dispose 模式并提供终结方法 - 如果类型持有需求由开发人员显式释放的类型,而且后者本身没有终结方法。
考虑为类实现基本 Dispose 模式 - 如果类本身并不持有非托管资源或可处置对象,但是它的子类型却可能会持有非托管资源或可处置对象。
要按下面的方法来实现 IDisposable 接口,即先调用 Dispose(true),然后再调用 GC.SuppressFinalize(this)。
不要将无参数的 Dispose 方法定义为虚方法。
不要为 Dispose 方法声明除了 Dispose() 和 Dispose(bool) 之外的任何其它重载方法。
要允许多次调用 Dispose(bool) 方法。他可以在第一次调用之后就什么也不做。
避免从 Dispose(bool) 方法中抛出异常,除非是紧急情况,所处的进程已经遭到破坏(比如泄漏、共享状态不一致,等等)。
要从成员中抛出 ObjectDisposedException 异常 - 如果该成员在对象终结之后就无法继续使用。
考虑在 Dispose() 方法之外在提供一个 Close() 方法 - 如果 close 是该领域中的一个标准术语。
避免定义可终结类型。
不要定义可终结的值类型。
要将类型定义为可终结类型 - 如果该类型要负责释放非托管资源,且非托管资源本身不具备终结方法。
要为所有的可终结类型实现基本 Dispose 模式。
不要在终结方法中访问任何可终结对象,这样做存在很大的风险,因为被访问的对象可能已经被终结了。
要将 Finalize 方法定义为受保护的。
不要在终结方法中放过任何异常,除非是致命的系统错误。
考虑创建一个用于紧急情况的可终结对象 - 如果终结方法在应用程序域被强制卸载或线程异常退出的情况下都务必要执行。
9.5 Factory 模式
要优先使用构造函数,而不是优先使用工厂,因为与特殊的对象构造机制相比,构造函数一般来说更容易使用、更一致,也更方便。
考虑使用工厂 - 如果构造函数提供的对象创建机制不能满足要求。
要使用工厂 - 如果开发人员可能不清楚待创建的对象的确切类型,比如对基类或接口编程就属于这种情况。
考虑使用工厂方法 - 如果这是让操作不言自明的唯一方法。
要在转换风格的操作中使用 factory。
要尽量将工厂操作方法实现为方法,而不是实现为属性。
要通过方法的返回值而不是方法的输出参数来返回新创建的对象实例。
考虑把 Create 和要创建的类型名连在一起,一次来命名工厂方法。
考虑把要创建的类型名和 Factory 连在一起,一次来命名工厂类型。例如,可以考虑把创建 Control 对象的工厂类型命名为 ControlFactory。
9.6 对 LINQ 的支持
要实现 IEnumerabl<T>,其目的是为了得到基本的 LINQ 支持。
考虑实现 ICollection<T>,其目的是为了提高查询的性能。
考虑实现 IQueryable<T> - 如果必须要访问传给 IQueryable 的成员的查询表达式。
不要草率地实现 IQueryable<T>,要理解这样做可能会对性能产生什么影响。
要在 IQueryable<T> 的方法中抛出 NotSupportedException - 如果你的数据源上不支持该操作。
要在新类型中将 Query 模式实现为实例方法 - 如果在 LINQ 以外的场合,这些方法在类型中仍然有存在的意义。否则,应该将它们实现为扩展方法。
要让实现了 Query 模式在类型实现了 IEnumerable<T>。
考虑在设计 LINQ 操作符时,让它们返回领域特有的可枚举类型。虽然从本质上来说,Select 查询方法可以返回任何类型,但是大家通常都希望查询的结果是可枚举类型。
避免只实现 Query 模式的一部分 - 如果不希望退回到基本的 IEnuerable<T> 实现。
要为有序序列定义单独的类型,从而将它和对应的无序序列分开。这样的类型应该定义 ThenBy 方法。
要推迟执行实际的查询操作。对 Query 模式的大多数成员来说,我希望它们只是创建一个新的对象,并在枚举的时候才产生集合重负荷查询条件的元素。
要将用于查询的扩展方法放在主命名空间中的一个名为“Linq” 的子命名空间中。例如,为 System.Data 特性定义的扩展方法被放在 System.Data.Linq 命名空间。
要在参数中使用 Expression<Func<...>>,而不是 Func<...> - 如果需要查询查询表达式。
9.7 Optional Feature 模式
考虑将 Optional Feature 模式用于抽象中的可选特性。
要提供一个简单的布尔属性来让用户检测对象是否支持可选特性。
要在积累中将可选特性定义为虚方法,并在该方法中抛出 NotSupportedException 异常。
9.8 Simulated Convariance 模式
考虑使用 Simulated Convariance 模式 - 如果需要有一种统一的类型来表示泛型类型的所有实例。
要确保以等价的方式来实现根基类型成员和对应的泛型类型成员。
考虑使用抽象基类来表示根基类型,而不是使用接口来表示根基类型。
考虑用非泛型类型作为根基类型 - 如果这样的类型已经存在。
9.9 Template Method 模式
避免将公有成员定义为虚成员。
考虑使用 Template Method 模式来更好地控制扩展性。
考虑以非秀成员的名字加“Core”后缀为名字,来命名为该费虚成员提供扩展点的受保护的虚成员。
9.10 超时
要优先让用户通过参数来制定超时长度。
要优先使用 TimeSpan 来表示超时长度。
要在超时后抛出 System.TimeoutException 异常。
不要通过返回错误码的方式来告诉用户发生了超时。
9.11 可供 XAML 使用的类型
考虑提供默认构造函数 - 如果想让类型能用于 XAML。
要提供标记扩展 - 如果想让 XAML 读取程序能够创建不可变的类型。。
避免定义新的类型转换器,除非这样的转换是自然而直观的。一般来说,应该将类型转换器的使用范围限制在 .NET 框架中已经使用了类型转换器的地方。
考虑将 ContentPropertyAttribute 用于最常用的属性,从而得到更方便的 XAML 语法。