框架设计原则和规范(完)

祝大家圣诞节快乐!有事没事别出门,外面太!挤!了!

此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,今天推送最后一章。

1. 什么是好的框架

2. 框架设计原则

3. 命名规范

4. 类型设计规范

5. 成员设计规范

6. 扩展性设计

7. 异常

8. 使用规范

9. 设计模式

一、设计模式

1. 聚合组件

Aggregate Component:

把多个底层类型集中到一个高层类型中,以此来支持常用场景。例如E-mail组件、System.Net.WebClient、System.Messaging.MessageQueue、System.IO.SeralPort、System.Diagnostics.EventLog

1) 面向组件的设计

component-oriented design:

通过类型来暴露API,而类型由函数、属性、方法及事件组成。

其使用模式为:Create-Set-Call

注意不要让对象处于不可用的状态,或者对方法的调用有先后顺序依赖

A. 应该有默认构造函数

B. 构造函数的所有参数应该与属性相对应,并用来对属性进行初始化

C. 大多数属性应该有getter和setter

D.所有属性都有合理的默认值

E. 如果参数在主要场景的方法调用之间不会改变,那么方法就不应该带这样的参数。这样的选项应该通过属性来指定。

F. 方法不以委托为参数。所有回调函数都通过事件来实现。

2) 因子类型

组成聚合类型的子类型,称为因子类型(factoredtype)

A. 没有状态

B. 有非常清晰的生命周期

C. 可用通过聚合组件的属性或方法访问

D.用于高级场景或与系统的不同部分集成

3) 聚合组件规范

A.考虑为常用的特性域提供聚合组件

B. 要用聚合组件来对高层的概念(物理对象)进行建模,而不是对系统级的任务进行建模

比如应该对文件、目录、驱动器建模,而不应该对流(stream)、格式化器(formatter)、比较器(comparer)进行建模

C.要让聚合组件的名字与众所周知的系统实体相对应

如MessageQueue,Process,EventLog

D. 要在设计聚合组件时使初始化尽量地简单

E. 不要要求聚合组件的用户在一个场景中显式的实例化多个对象

API的用户数量与简单场景中的new语句数目成反比

F.要保证让聚合组件支持Create-Set-Call使用模式

用户可以先实例化组件,然后设置属性,调用方法,以实现大多数场景

G. 要为所有聚合组件提供默认构造函数或非常简单的构造函数

H.要为聚合组件提供可读写的属性来与构造函数中的所有参数相对应

I. 要在聚合组件中使用事件,不要使用基于委托的API

J. 考虑用事件来代替需要被覆盖的虚成员

K. 不要要求聚合组件的用户在常用场景中使用继承、覆盖方法及实现接口。

应该主要依靠属性以及属性的组合来改变自己的行为

L. 不要要求用户在常用场景中除了写代码,还要搞配置文件、资源文件等其他工作

M. 考虑让聚合组件能够自动切换状态

MessageQueue既可以收也可以发消息,用户感觉不到模式切换

N. 不要设计有多种状态的因子类型

O.考虑将聚合组件集成到Visual Stuio的设计器中。

只要实现IComponent接口即可

P.考虑把聚合组件和因子类型分开,各自放到不通的程序集中。防止循环依赖。

Q.考虑把聚合组件内部的因子类型暴露给外界访问

2. Async模式

异步API建模:

一个是“经典的”,一个是“基于事件的”

经典模式使用回调函数,在任意线程中执行。更加灵活强大,性能也更高。

基于事件模式使用事件,更容易学习。可以和VisualStudio集成。

1) 选择合适的Async模式

A.如果类型是一个支持可视化设计器的组件,使用“基于事件的Async模式”

B. 如果必须支持等待句柄,使用“经典的Async模式""

C. 考虑在高层API使用“基于事件的Async模式”

D.考虑在底层API时使用“经典的Async模式”

E. 避免在同一个类型或者是一组相关的类型中同时实现两种Async模式

2) 经典Async模式

public class APMTestRun1 
{ 
 publicstatic void AsyncRun() 
 {
          Utility.Log(""APMAsyncRun:start"");
          stringurl = ""http://sports.163.com/nba/""; 
 HttpWebRequestwebRequest =HttpWebRequest.Create(url) as HttpWebRequest;  // webRequest是一个 IAsyncResult
          webRequest.BeginGetResponse(Callback,webRequest);//开始异步操作,设置回调Callback,此Callback函数会在另外一个线程中运行,在任务完成的时候调用
          Utility.Log(""AsyncRun:download_start"");
 }
 // 回调函数,参数ar用来执行EndGetResponse()
 privatestatic void Callback(IAsyncResult ar) 
 {
          varsource = ar.AsyncState as HttpWebRequest; 

varresponse = source.EndGetResponse(ar); //一直阻塞,直到结束异步操作。实际上在这里一般都可以立刻返回,主要是为了清理掉异步状态防止内存漏洞

          using(var stream = response.GetResponseStream()) 
          {
                   using(var reader = new StreamReader(stream)) 
                   {
                            stringcontent = reader.ReadToEnd(); 
                            Utility.Log(""AsyncRun:result_size=""+ Utility.GetStrLen(content)); 
                   }
          }
 }
} 

A. 模式的主要元素

a) Begin方法,用来开始一个异步操作
b) End方法,用来完成一个异步操作
c) 返回自Begin方法的IAsyncResult对象,它本质上表示一个异步操作。
d) 由用户提供的异步回调函数,用户把它传给Begin方法,当异步操作完成时会被调用。
e) 有用户提供的State对象,用户可以先把它传给Begin方法,随即传给异步回调函数。通常用这个状态来把数据从调用方法传给异步回调函数。

B. 实现规范

a) 异步操作定义API时要遵循的约定
i. 给定名为Operation的同步方法,应该提供名为BeginOperation和EndOperation的方法

方法签名标准:

// 同步方法
public <return>Operation(<parameters>, <out params>)
// 异步方法
public IAsyncResultBeginOperation(<parameters>, AsyncCallback callback, object state)
public <return>EndOperation(IAsyncResult asyncResult, <out params>)

ii. 要确保begin方法的返回类型实现了IAsyncResult接口

iii. 要确保同步方法的按值传递和按引用传递的参数在Begin方法中都是按值传递
iv. 要确保End方法的返回类型和同步方法的返回类型相同
v. 如果Begin方法抛出异常,不要继续执行异步操作
vi. 要依次通过下面的机制来通知调用方异步操作已经完成
将IAsyncResult.IsCompleted设为true
激活IAsyncResult.AsyncWaitHandler返回的的等待句柄
调用异步回调函数
vii. 要通过从End方法中抛出异常来表示无法成功的完成异步操作
viii. 要在End方法被调用时同步完成所有尚未完成的操作
ix. 如果用户用同一个IAsyncResult两次调用一个End方法,或IAsyncResult是从另外一个不想管的Begin方法返回的,考虑抛出InvalidOperationException异常
x. 当且仅当异步回调函数将在调用Begin方法的线程中运行的时候,要把IAsyncResult.CompletedSynchoronously设为true

C. Async模式的基本实现样例

//一个菲波拉契数列的生成器,以异步方式调用
public class FiboCalculator {
     delegatevoid Callback(int count,ICollection<decimal> series); //关于异步线程的原型声明,按业务需求设置
     privateCallback callback = new Callback(GetFibo); //生成异步处理线程对象,注意GetFibo和Callback是两个类型,GetFibo表示一个业务功能
     //开始生成一个序列的过程,然后返回
     //返回值是由线程启动方法BeginInvoke所产生的,用来表示一次异步过程
    publicIAsyncResult BeginGetFibo{
               intcount,
               ICollection<decimal>series,
 AsyncCallback callback,
               object state)
     {
               returnthis.callback.BeginInvoke(count, series, callback, state); //BeginInvoke()的参数列表前半段为异步业务函数的参数,后2个为线程处理所需的参数:callback->任务结束后的回调;state->传递给callback函数的参数
     }
     //阻塞,直到生成序列的过程完成。
     // 用户可以在主线程中调用此方法阻塞直到返回,也可以放在异步回调方法里面,用来清理异步调用的内存漏洞。
     publicvoid EndGetFibo(IAsyncResult asyncResult) {
               this.callback.EndInvoke(asyncResult);//EndInvoke()的作用为阻塞直到callback线程退出,参数应该为BeginInvoke()的返回值
     }
     //生成一个第一个参数count的数量的菲波拉契数列数列,此方法可能会造成一段时间的阻塞,因为可能需要很多层递归
     publicstatic void GetFibo(
               intcount, ICollection<decimal> series)
     {
               for(int i = 0; i < count; i++) {
                        decimald = GetFiboCore(i);
                        lock(series) {
                                 series.Add(d);
                        }
               }
     }
     //返回第N个菲波拉契数
     staticdecimal getFiboCore(int n) {
               if(n < 0) throw new ArgumentException(""n must be >0"");
               if(n == 0 || n == 1) return 1;
               returnGetFiboCore(n-1) + getFiboCore(n-2);
     }
}

3) 基于事件的Async模式

A.异步方法的定义

a) 要确保如果组件定义的异步方法没有userState参数,那么在前一次调用完成之前试图再调用该方法都将引发InvalidOperationException

userState参数用来区别对于同一个异步方法的多次调用:

public void MethodAsync(stringarg1, string arg2, object userState);

这个userState会传递到事件处理函数里,用来让事件处理函数分辨是哪一次异步请求所产生的事件。

b) 要确保在正确的线程中调用事件处理程序。
c) 要确保无论是操作已经完成,还是操作出错,还是操作被取消,都始终会调用事件处理程序。不应该让应用程序无休止的等待一件永远不会发生的事件。
d) 要确保在异步操作失败后,访问事件参数类的属性会引发异常。——如果有错误导致操作无法完成,那么就不应该允许用户访问操作的结果。
e) 不要为返回值为空的方法定义新的事件处理程序或事件参数类型,要用:
i. AsyncCompletedEventArgs
ii. AsyncCompletedeventHandler
iii. EventHandler<AsyncCompletedEventArg>

4) 对于输出参数和引用参数的支持

A.异步方法签名中去掉所有的输出参数

B. 把输出参数作为EventArgs类的只读属性暴露给用户

C. 属性的名字和类型应该和对应的参数相同

5) 对取消操作的支持

A.要确保在将操作取消时,将事件参数类的Cancelled属性设为true,并确保在用户试图访问结果时引发InvalidOperationException,来告诉用户操作已经取消

B. 如果无法取消某个特定的操作,要忽略对取消操作的调用而不是抛出异常。

6) 对进度报告的支持

增加一个额外的ProgessChanged事件,这个事件由异步操作引发。

传给此事件的处理程序的事件参数:ProgressChangedEventArgs参数中有一个表示进度的属性,该属性为0-100。

A. 要确保如果在一个异步操作中实现了ProgressChanged事件,那么在操作的完成事件被触发之后,不应该再出现此类事件。

B. 要确保如果使用了标准的ProgessChangedEventArgs,那么ProgressPercentage始终能用来表示进度的百分比。

7) 对增量结果的支持

在少数情况下,异步操作可以在操作完成之前不定期的返回增量结果(incrementalresult)。

A. 要在有增量节诶过需要报告的时候触发ProgressChanged事件

B. 要对ProgressChangedEventArgs进行扩展来保存增量结果数据,并用扩展后的事件参数类来定义ProgessChanged事件

C. 多个异步操作返回不通类型的数据

a) 要把增量结果报告与进度报告分开
b) 要为每个异步操作定义单独的<MethodName>ProgressChanged事件和响应的事件参数类,来处理该操作的增量结果数据。

3. 依赖属性

Dependency Properties

依赖属性的值不保存在类型的字段中,而是放在一个属性存储区中。比如对象和对象容器的关系;Panel容器中的一个Button对象。

拿Button来讲,它的继承树是Button->ButtonBase->ContentControl->Control->FrameworkElement->UIElement->Visual->DependencyObject->…

每次继承,父类的私有字段都被继承下来。当然,这个继承是有意思的,不过以Button来说,大多数属性并没有被修改,仍然保持着父类定义时的默认值。通常情况,在整个Button对象的生命周期里,也只有少部分属性被修改,大多数属性一直保持着初始值。每个字段,都需要占用4K等不等的内存,这里,就出现了期望可以优化的地方:

因继承而带来的对象膨胀。每次继承,父类的字段都被继承,这样,继承树的低端对象不可避免的膨胀。

大多数字段并没有被修改,一直保持着构造时的默认值,可否把这些字段从对象中剥离开来,减少对象的体积。

1) 如果需要支持各种WPE特性,比如样式、触发器、数据保定、动画、动态资源以及继承,要提供依赖属性

2) 依赖属性的设计

A.继承DependencyObject或其子类型实现依赖属性

B. 要为每个依赖属性提供常规的CLR属性和存放System.Windows.DependencyProperty实例的公有静态只读字段

C. 使用DependencyObject.GetValue和SetValue的方式来实现依赖属性

D.要用依赖属性的名字加上“Property“后缀来命名依赖属性的静态字段

E. 不要显式的在代码中设置依赖属性的默认值,应该在元数据中设置默认值

F. 不要在属性的访问器中添加额外的代码,而应该使用标准代码来访问静态字段

G.不要依赖书香来保存保密数据。任何代码都能访问依赖属性,即使他们是私有的。

3) 附加依赖属性的设计

A.依赖属性的验证

a) 不要把依赖属性的验证逻辑放在访问器中,而应该把验证回调函数传给DependencyProperty.Register方法

B. 依赖属性的改变通知

a) 不要在依赖属性的访问器中实现属性改变的通知,而应该向PropertyMetadata注册改变通知的回调函数

C. 依赖属性的强制赋值

a) 不要再依赖属性的访问器中实现属性强制赋值逻辑,而应该向PropertyMetadata注册强制赋值的回到函数

4. Dispose模式

参见: IDisposable

手动释放非托管资源的方法:

调用.Dispose()方法

使用using(DisposableObjectobj = new DisposableObject()){ ... }

在资源类的析构函数写释放代码,但是无法确定什么时候被释放

1) 要为含有可处置实例的类型实现基本Dispose模式。

2) 如果类型持有需要开发人员显式释放的类型,而且其本事没有终结方法,要为其实现基本Dispose模式并提供终结方法

3) 如果类本身并不持有非托管资源或可处置对象,但是它的子类型却可能会持有,那么考虑为此基类实现基本Dispose模式

4) 基本Dispose模式

参见: 要为所有的可终结类型实现“基本Dispose模式”

基本Dispose模式的一个简单实现:

public classDisposableResourceHolder : IDisposable {
 privateSafeHandle resource; // 掌握一个资源
 publicDisposableResourceHolder() {
          this.resource= ... // 分配资源
 }
 publicvoid Dispose() {
          Dispose(true);
          GC.SuppressFinalize(this);
 }
 //此方法被IDisposable.Dispose方法所调用时disposing 为 true,所以应该检查资源是否还可用
 //此方法被终结器(垃圾回收机制)调用时 disposing 为false。
 protectedvirtual void Dispose(bool disposing) {
          if(disposing) {
                   if(resource != null) resource.Dispose();
          }
 }
}

A. 要声明protected virtual void Dispose(bool disposing)方法,来把所有与非托管资源有关的清理工作集中在一起。

B. 要先调用Dispose(true),然后再调用GC.SuppressFinalize(this)

C. 不要把无参数的Dispose方法定义为虚方法

D.不要为Dispose方法声明除了Dispose()和Dispose(bool)之外的任何其他重载放啊分。

E. 要允许多次调用Dispose(bool)方法。可以让它在第一次调用后就什么都不错。

F. 避免从Dispose(bool)方法中抛出异常,除非是紧急情况,所处进程已经早到破坏。

G.如果方法在对象终结之后(被调用了Dispose方法后)就无法继续使用,要从成员中抛出ObjectDisposedException异常

H.如果Close是该领域中的一个标准术语,考虑在Dispose()方法之外再提供一个Close()方法

5) 可终结类型

如果类型覆盖了终结方法(析构函数),并在Dispose(bool)中加入支持终结的代码,以此来扩展基本Dispose模式,那么这些类型就是可终结类型。

这种是把非托管资源封装成托管资源的做法。性能不高

A. 避免定义可终结类

B. 不要定义可终结的值类型

C. 如果一个类型要负责释放非托管资源,且非托管资源本身不具备终结方法,要将该类型定位为可终结类型

D.要为所有的可终结类型实现“基本Dispose模式”

参见: 基本Dispose模式

E.不要在终结方法中访问任何可终结对象,因为被访问的对象可能已经被终结了

F. 要将Finalize方法(析构函数)定义为受保护的

G.不要在终结方法中放过任何异常,除非是致命的系统错误。

如果从终结方法抛出异常,那么CLR会关闭整个进程。

H. 考虑创建一个用于紧急情况的可终结对象——如果终结方法在应用程序域被强制卸载或线程异常退出的情况下都务必执行。

5. Factory模式

1) 要优先使用构造函数,而不是优先使用工厂,构造函数更容易使用,更一致,更方便

2) 如果构造函数提供的对象创建机制不能满足要求,才考虑使用工厂

3) 如果开发人员可能不清楚待创建对象的确切类型,比如对基类或接口编程就属于这种情况,要使用工长

4) 如果这是让操作不言自明的唯一办法,要考虑使用工厂方法

5) 要在转换风格的操作中使用factory

所谓转换风格:

int i =int.Parse(""35"");

DateTime d =DateTime.Parse(""10/10/1999"");

6) 要尽量将工厂操作实现为方法,而不是实现为属性

7) 要通过方法的返回值而不是方法的输出参数来返回新创建的对象实例

8) 考虑把Create和要创建的类型名连在一起,来命名工厂方法

9) 考虑把创建的类型名和Factory连在一起,以此来命名工厂类型。

6. 对LINQ的支持

语言集成查询:Language-IntegratedQuery

1) LINQ概要

2) 支持LINQ的几种方法

3) 通过IEnumerable来支持LINQ

4) 通过IQueryable来支持LINQ

7. Optional Feature模式

抽象的一部分实现支持某种特性,而其他实现则不支持该特性。如stream的实现可能会支持读、写、定位或其他组合。

// 可选功能在继承的时候按所提供的范围来覆盖
    public abstract class Stream {
     publicabstract void Close();
     publicabstract int Position { get; }
//可选的写入功能
     publicvirtual bool CanWrite { get { return false; } }
     publicvirtual void Write(byte[] bytes) {
              thrownew NotSupportedException(...);
     }
     //可选的搜索功能
     publicvirtual bool CanSeek { get {return false;} }
     publicvirtual void Seek(int position) {
              thrownew NotSupportedException(...);
     }
     //其他可选功能
}

1) 考虑将Optional Feature模式用于抽象中的可选特性

2) 要提供一个简单的布尔属性来让用户检测对象是否支持可选特性

3) 要在基类中将可选特性定义为虚方法,并在方法中抛出NotSupportedException异常

8. Simulated Covariance模式

泛型生成的类因为没有一个公共的基类,在某些情况下很不好操作。比如List<T>就不是List<object>的子类,在需要管理多个List的时候会很麻烦。

因此,我们需要用SimulatedCovariance模式:

  • 声明一个IFoo<T>的接口模板,在此接口中以T作为类型声明各种所需的公共的方法。
  • 然后让具体对于泛型类实现的时候,用Bar<T>: IFoo<object>来继承
  • 这样所有的Bar<T>类型都有一个公共的基类:IFoo<object>,因此也可以调用此基类的公共方法。

1) 如果需要有一种同意的类型来表示泛型类型的所有实例,考虑使用SimulatedCovariance模式

2) 要确保以等价的方式来实现根基类型成员和对应的泛型类型成员

3) 考虑使用抽象基类来表达根基类型,而不是使用接口来表示根基类型

4) 如果这样的类型已经存在,考虑用非泛型类作为根基类型

9. Template Method模式

最常见的形式由一个过着多过非虚(通常是公有)成员组成,这些成员通过调用一个或者多个受保护的虚成员来实现。

目标是对扩展性加以控制。

1) 避免将公有成员定义为虚成员

2) 考虑使用Template Method模式来更好的控制扩展性

3) 考虑以非虚成员的名字加""Core“后缀,来命名该非虚成员提供扩展点的受保护虚成员

public void SetBounds(...) {
 ...
 SetBoundsCore(...);
}
protected virtual voidSetBoundsCore(...) {...}

10. 超时

需要支持超时的API的设计规范

1) 要优先让用户通过参数来指定超时长度

用方法的参数比属性好,这使操作与超市长度之间的关系更加明确。

2) 优先使用TimeSpan来表示超时长度

用整数来表示超时长度有几个缺点:

  • 超时长度的度量单位不明显
  • 不太容易把时间单位转换为常用的毫秒

如果要用整数,需要满足:

  • 参数或属性的名字能够描述相应的时间单位,如XxxMillisenconds
  • 常用的值非常小,用户不需要用计算器来得出最终的值,如单位是毫秒,常用的数量应该是小于1秒

3) 要在超时后抛出System.TimeoutException异常

4) 不要通过返回错误码的方式来告诉用户发生了超时

11. 可供XAML使用的类型

XAML是WPF用来表示对象图的一种XML格式,一般用于画UI

感谢大家的阅读,如觉得此文对你有那么一丁点的作用,麻烦动动手指转发或分享至朋友圈。如有不同意见,欢迎后台留言探讨。

原文发布于微信公众号 - 韩大(handa1740168)

原文发表时间:2015-12-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java一日一条

用Java实现一个通用并发对象池

这篇文章里我们主要讨论下如何在Java里实现一个对象池。最近几年,Java虚拟机的性能在各方面都得到了极大的提升,因此对大多数对象而言,已经没有必要通过对象池来...

382
来自专栏Java架构

想要面试BATJ,先做完这160道Java面试题~

2、访问修饰符public,private,protected,以及不写(默认)时的区别?

552
来自专栏技术博客

菜菜从零学习WCF十(序列化)

 本次课程的主要内容包括以下四格部分:DataContractSerializer、序列化、反序列化、XmlSerializer

633
来自专栏Jed的技术阶梯

Kafka 中使用 Avro 序列化框架(二):使用 Twitter 的 Bijection 类库实现 avro 的序列化与反序列化

使用传统的 avro API 自定义序列化类和反序列化类比较麻烦,需要根据 schema 生成实体类,需要调用 avro 的 API 实现 对象到 byte[]...

574
来自专栏编码小白

ofbiz实体引擎(三) GenericDelegator实例化的具体过程

/** * @author 郑小康 * 1.设置delegatorFullName 基本delegatorName+"#"+tenantId...

2805
来自专栏nnngu

经典Java面试题收集

1、面向对象的特征有哪些方面? 答:面向对象的特征主要有以下几个方面: 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象...

4356
来自专栏技术博客

C#简单的面试题目(四)

46.请编程遍历页面上所有TextBox控件并给它赋值为string.Empty?

732
来自专栏IT可乐

mybatis源码解读(二)——构建Configuration对象

1372
来自专栏微信公众号:Java团长

什么是JVM?

说明:做java开发的几乎都知道jvm这个名词,但是由于jvm对实际的简单开发的来说关联的还是不多,一般工作个一两年(当然不包括爱学习的及专门做性能优化的什么的...

881
来自专栏草根专栏

C# 7.0简而言之 -- 01. C#和.NET Framework简介

C#里面所有的类型都有一个共享的基类, 这也意味之C#里面所有的类型都具备一些相同的基本功能, 例如任何类型都可以通过调用ToString()方法来转化成字符串...

4659

扫描关注云+社区