前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >框架设计原则和规范(三)

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

作者头像
韩伟
发布2018-03-05 15:18:10
9400
发布2018-03-05 15:18:10
举报
文章被收录于专栏:韩伟的专栏韩伟的专栏

此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,将分4天进行推送,今天推送6-7章。

1. 什么是好的框架

2. 框架设计原则

3. 命名规范

4. 类型设计规范

5. 成员设计规范

6. 扩展性设计

7. 异常

8. 使用规范

9. 设计模式

1. 扩展性设计

1.1. 扩展机制

1.1.1. 非密封类

1.1.1.1. 考虑用不包含任何虚成员,或受保护的成员的非密封类来为框架提供扩展性

用户扩展简单,安全性很高

1.1.2. 受保护的成员

1.1.2.1. 考虑将受保护成员用于高级的定制方案

1.1.2.2. 要在对安全性、文档及兼容性进行分析时,把非密封类中受保护的成员当作共有成员来对待

1.1.3. 事件与回调函数

提供了运行时的动态扩展

1.1.3.1. 考虑使用回调函数来允许用户向框架提供自定义的代码供框架执行

1.1.3.2. 考虑使用事件来允许用户对框架的行为进行定制,这样样就不需要用户对面向对象设计有深入的了解

1.1.3.3. 要优先使用事件,而不是简单的回调函数,其原因在于广大开发人员更熟悉事件,而且事件与Visual Studio的语句自动完成特性结合的很好

1.1.3.4. 避免在对性能要求很高的API中使用回调函数

1.1.3.5. 要在定义用了回调函数的API时,使用新的Func<...>,Action<...>或Expression<...>类型,而不要使用自定义的委托

.NET框架中的泛型委托定义:

public delegate void Action()

public delegate void Action<T1, T1>(T1 arg1, T2 arg2)

...

public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 agr2, T3 arg3, T4 arg4)

public delegate TResult Func<TResult>()

...

public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2, T3 arg3, T4 arg4)

Expression<> 和 Func<>相似,但是可以.Compile()方法编译

1.1.3.6. 要在用Expression<...>来代替Func<...>和Action<...>委托的时候进行测量,从而了解他可能对性能产生的影响。

1.1.3.7. 要理解在调用委托时可以执行任何代码,这可能会引起安全性、正确性及兼容性的问题。

如果用户代码激活了一个线程并等自己需要的锁释放,那么很可能会产生死锁;

除了死锁,还可能引入“重入”:回调函数不知怎么调用到了那个调用他的对象。

1.1.4. 虚成员

提供了编译时的静态扩展

1.1.4.1. 除非有合适的理由,不要使用虚成员。而且要对设计、测试以及维护虚成员的开销有清楚的认识

虚成员会更慢;

如果发行了虚成员的类型,那么相当于对用户做出了承诺,就是这些类型永远不会改变用户察觉得到的行为,以及他们与子类间的交互。

1.1.4.2. 考虑只有在绝对必要的时候采用虚成员提供扩展性,并使用Template Method模式

1.1.4.3. 要优先使用受保护的虚成员,而不是公有的虚成员。公有成员应该通过调用受保护的虚成员的方法来提供扩展性。

代码语言:javascript
复制
public Control {
public void SetBounds(...) {
    ...
    SetBoundsCore(...);  //调用受保护的虚成员提供扩展性
}
protected virtual void SetBoundsCore(...) {
    // 真正的执行工作代码
}
}

1.1.5. 抽象(抽象类型与抽象接口)

描述一个契约,但不提供完整的实现。

抽象提供了强大的扩展性,是现代面向对象框架广受欢迎的原因。

抽象的困难在于确定合适的成员,既不能太多也不能太少,太多的话难以实现,太少的话功能会变少。

如果没有一流的文档来说明抽象必须满足的前置条件和后置条件,最终结果只能是被淘汰。

抽象需要考虑是用类还是接口表达,前面有专门章节讨论

1.1.5.1. 除非为该抽象开发出多个具体实现,并且通过用到该抽象的API对其进行过实际测试,否则不要提供抽象

1.1.5.2. 要在设计抽象时谨慎的选择抽象类还是接口。

1.1.5.3. 考虑为抽象的具体实现提供参考测试。这类测试应该能告诉用户,他们是否正确的实现了契约。

Windows Form定义了一个IComponent接口,同时还提供了一个Component类来实现IComponent接口。一个类型可以选择派生自Component类,也可以选择只是实现IComponent借口。这让开发人员能选择最合适自己的方法。

1.2. 基类

1.2.1. 考虑将基类定义为抽象类,即使它不包含任何抽象成员,这样可以明确告诉使用者,这个类完全是为了让用户使用它们来派生自己子类的。

1.2.2. 考虑把基类与用于主要场景的类型分开,并放到单独的名字空间中。

1.2.3. 避免在命名基类时使用“Base”后缀——如果公共API中会用到这个类

有些基类还是会被框架暴露的API所用到,而不是子类,增加后缀只会对使用该方法的用户造成不必要的干扰

1.3. 密封

sealed 关键字可以阻止一个类被派生,或者一个成员(方法、属性、字段)被覆盖

1.3.1. 除非有恰当理由,不要把类密封起来:

l 静态类可以

l 类的受保护成员保存了需要高度保密的机密信息

l 类继承了许多成员,分别密封那些成员太麻烦,不如整个类密封

l 类是修饰属性(Attribute),需要能在运行时快速查找

1.3.2. 不要在密封类中生命受保护的成员或虚成员

1.3.3. 考虑在覆盖成员时将其密封

引入虚成员所可能导致的问题,对覆盖成员来说同样存在。把覆盖成员密封起来可以从继承层次中的这一级开始避免发生问题。

2. 异常

  • 异常增强了API的一致性。异常的唯一目的就是为了报告错误,而返回值有多重用途。
  • 用返回值来报告错误时,错误处理的代码与可能发生错误的代码距离总是很近。开发人员可以选择在附近捕获异常,或者交给上层处理,选择性更多。
  • 更容易使错误处理的代码局部化。如果使用返回值,几乎每一行功能性代码都要有一个if语句。
  • 错误码很容易被忽略,异常在代码控制流中扮演了一个积极的角色
  • 异常可以包含丰富的信息来对错误的原因加以描述
  • 异常允许用户定义未处理异常的处理程序(unhandled exception handler)
  • 异常有助于检测。

2.1. 抛出异常

2.1.1. 不要返回错误码

2.1.2. 要通过抛出异常的方式来报告操作失败

如果某个方法无法完成它的名字所对应的任务,那么我们应该认为这是方法层面的操作失败并抛出异常

2.1.3. 考虑在代码遇到严重问题且无法继续安全的执行时,通过调用System.Environment.FailFast(.NET框架2.0版新特性)来终止进程,而不要抛出异常

2.1.4. 不要在正常的控制流中使用异常,如果能够避免的话

2.1.5. 考虑抛出异常可能对性能造成的影响

每秒抛出100个异常可能会影响性能

2.1.6. 要为所有的异常撰写文档,并把它们作为契约的一部分

2.1.7. 不要让公有成员根据某个选项来决定是否抛出异常

2.1.8. 不要把异常用作公有成员的返回值或输出参数

2.1.9. 考虑使用辅助方法来创建异常

从不同地方抛出同一个异常很常见,为了避免代码重复,可以使用辅助函数来创建异常并对其属性进行初始化。

代码语言:javascript
复制
void ThrowNewFileException ( ... ) {
    string description = // build localized string
    throw new FileIOException(description);
}

2.1.10. 不要在异常过滤程序(exception filter)中抛出异常

C#不支持异常过滤程序

2.1.11. 避免显示的从finally代码块中抛出异常。隐式的抛出异常,即在调用其他方法时由其他方法抛出异常,是可以接受的。

2.2. 为抛出的异常选择合适的类型

2.2.1. 不要为使用错误而创建新的异常,应该抛出框架中已有的异常

使用异常包括:

传入了null作为参数;ArgumentNullException

参数不合法;ArgumentException

无效的操作;InvalidOperationException

不支持的操作等 NotSupportedException

2.2.2. 考虑为程序错误创建并抛出自定义异常——如果对它的处理方式和对其他异常的处理方式有所不同。否则应该抛出已有有的异常

程序错误表示那些能够在代码中进行处理,而且通常是在代码中进行处理的错误。

2.2.3. 不要创建新的异常类型——如果对该异常的处理和对框架中已有的异常并没什么不同。

2.2.4. 要创建新的异常类型来表达独一无二的程序错误

2.2.5. 避免设计出会导致系统失败的API。如果此类失败可能会发生,就应该调用Enviroment.FailFast,而不是抛出异常

2.2.6. 不要仅仅为了拥有自己的异常而创建并使用新的异常

2.2.7. 要使用合理的、最具针对性(最低层派生类)的异常

如对于传入null参数,应该用ArgumentNullException而不是基类ArgumentException

抛出System.Exception(所有异常的基类)无论如何都是错的

2.2.8. 错误消息的设计

在异常中携带的文本信息

对于已经处理的异常,异常消息并没什么用,只有当异常未被处理的时候它们才能发挥作用。因此错误消息的目的应该是帮助开发人员修正代码的错误,而不是给最终用户看。

2.2.8.1. 要在抛出异常时为开发人员提供丰富而有意义的错误消息

2.2.8.2. 要确保异常消息的语法(自然语言,如英语,中文)正确无误

2.2.8.3. 要确保异常消息中的每个句子都有句号

如果异常消息要输出给用户界面看,就不用添加字符串的句号了

2.2.8.4. 避免在异常消息中使用问号和惊叹号

2.2.8.5. 不要在没有得到许可的情况下在异常消息中泄露安全信息

2.2.8.6. 考虑把组件抛出的异常消息本地化——如果想让母语为其他语言的开发人员也能使用组件

2.2.9. 异常处理

2.2.9.1. 不要在框架的代码捕获具体类型不确定的异常时把错误吞了

代码语言:javascript
复制
try {
   File.Open(...);
}
catch (Exception e)
{ } //吞了异常,不要这样做

2.2.9.2. 避免在应用程序的代码中,在捕获具体类型不确定的异常时,把错误吞了

2.2.9.3. 不要把任何特殊的异常排除在外——如果编写catch代码块的目的就是为了转移异常

代码语言:javascript
复制
catch (Exception e) {
// 不好的代码
// 不要这样做
if (e is StackOverflowException || e is OutOfMemoryException ||e is ThreadAbortException)
throw;
...
}

2.2.9.4. 考虑捕获特定类型的异常——如果确实理解该异常在具体环境中产生的原因,并能对错误做出适当的反应

应该只有在你知道自己能从一个异常中完全恢复时,才捕获该异常。在执行一些操作时,你可能知道产生异常的原因,但却不知道如何从中恢复,在这种情况下不要捕获异常。

2.2.9.5. 不要捕获不应该捕获的异常。通常应该允许异常沿着调用栈向上游传递。

这样能让异常在确实发生处被察觉,从而准确定位问题

2.2.9.6. 要在进行清理工作时使用try-finally,避免使用try-catch。

2.2.9.7. 要在捕获并重新抛出异常时使用空的throw语句。这是保持异常调用栈不变的最好方法。

代码语言:javascript
复制
public void DoSomething(FileStream file) {
long position = file.Position;
try {
 ... // 读取文件
} catch {
 file.Position = position; // unwind on failure
 throw; // 重新抛出
}
}

2.2.9.8. 不要用无参数的catch块来处理不符合CLS规范的异常(不是派生自System.Exception的异常)

CLR2.0做了修改,不符合CLS规范的异常会封装在RuntimeWrappedException中,这样就可以用catch处理了

2.2.10. 封装异常

要确保在错误消息中使用的术语能够为用户理解。而很多异常都是从底层抛出的,并为高层所捕获。其中的错误消息描述了一些概念,由于这些概念只有从事底层API相关开发的人才能理解,因此它们对解释问题产生的原因没有什么抱住。

因此需要对底层的异常进行封装。

2.2.10.1. 考虑对较低层次抛出的异常进行适当的封装——如果较低层次的异常在较高层次的运行环境中没有什么意义

如果用户想要查看内部异常,那么就不要对异常进行封装。

只有当原来的异常几乎没有什么意义,对调试也没有什么帮助时,才应该对其进行封装再重新抛出。

2.2.10.2. 避免捕获并封装具体类型不确定的异常

这只是吞掉错误的另外一种形式

2.2.10.3. 要在对异常进行封装时为其指定内部异常(inner exception)

throw new ConfigurationFileMissingException(..., e);

2.3. 标准异常类型的使用

2.3.1. Exception与SystemException

2.3.1.1. 不要抛出System.Exception或System.SystemException异常

2.3.1.2. 不要在框架代码中捕获System.Exception或System.SystemException异常,除非打算重新抛出

2.3.1.3. 避免捕获Exception或SystemException异常,除非是在顶层的异常处理器中

2.3.2. ApplicationException

这本不应该属于.NET框架,最初的想法是用派生SystemException的类来表示CLR(或系统)自己抛出的异常,而用派生自ApplicationException的类来表示非CLR抛出的异常。但是,很多异常类都没有遵循这一模式。

2.3.2.1. 不要抛出System.ApplicationException或从它派生新类

2.3.3. InvalidOperationException

2.3.3.1. 如果对象处于不正确的状态,要抛出InvalidOperationException

如果参数本身不对应该用ArgumentException,这不依赖于任何其他对象的状态。

否则应该用InvalidOperationException

2.3.4. ArgumentException\ArgumentNullException\ArgumentOutOfRangeException

2.3.4.1. 要在用户传入无效参数时抛出ArgumentException异常或其子类型。如果可以的话,要尽量使用位于继承层次末尾的异常类型

2.3.4.2. 要在抛出ArgumentException异常或其子类时设置ParamName属性,表示哪个参数引发了异常。

2.3.4.3. 要在属性的setter中,以“value”作为value隐式参数的名字。

代码语言:javascript
复制
public FileAttributes Attributes {
set {
 if (value == null) {
 throw new ArgumentNullException(""value"", ...);
 }
}
}

2.3.5. NullReferenceException\IndexOutOfRangeException\AccessViolationException

2.3.5.1. 不要让公共API显式的或隐式的抛出这三个异常。这些异常是专门留给执行引擎来抛出的,大多数情况下它们表示代码存在缺陷

2.3.6. StackOverflowException

2.3.6.1. 不要显式的抛出此异常,应该只有CLR才能抛出

2.3.6.2. 不要捕获此异常

2.3.7. OutOfMemoryException

2.3.7.1. 不要显式的抛出此异常,应该只有CLR才能抛出它

2.3.8. ComException\SEHException\ExecutionEngineException

2.3.8.1. 不要显式的抛出这些异常,应该只有CLR才能抛出它

2.4. 自定义异常的设计

2.4.1. 要从System.Exception或其他常用的异常基类派生新的异常

2.4.2. 避免太深的继承层次

2.4.3. 要在命名异常类时使用“Exception”后缀

2.4.4. 要使异常可序列化。为了使异常能够在跨应用程序域和跨远程边界时仍能正常使用,这样做是必须的

2.4.5. 要为所有的异常(至少)提供下面这些常用的构造函数

代码语言:javascript
复制
public class SomeException : Exception, ISerializable {
    public SomeException();
    public Someexception(string message);
    public SomeException(string message, Exception inner);
    //序列化所需方法
    protected SomeException(SerializationInfo info, StreamingContext context);
}

2.4.6. 要通过ToString的一个覆盖方法来报告与安全性有关的信息,前提是必须先获得相应的许可(安全性)。

2.4.7. 要把安全性相关的信息保存在私有的异常状态中,并确保只有可信赖的代码才能得到该信息。

2.4.8. 考虑为异常定义属性,这样就能从程序中去的除了消息字符串之外与异常有关的额外信息

2.5. 异常与性能

2.5.1. 不要因异常可能对性能造成的负面影响而使用错误码

2.5.2. Tester-Doer模式

代码语言:javascript
复制
ICollection<int> numbers = ...
...
if (!numbers.IsReadOnly) {  // <--Tester 先测试,避免异常
    numbers.Add(1);          // <--Doer 可能抛出异常的操作
}

2.5.2.1. 考虑在成员中使用Tester-Doer模式来避免因异常而引起的性能问题——如果成员在常用场景中都有可能抛出异常

2.5.3. Try-Parse模式

代码语言:javascript
复制
public struct DateTime {
    //可能抛出异常的方法
    public static dateTime Parse(string dateTime){
 ...
    }
    //Try-Parse的方法,返回bool表示是否处理成功
    public static bool TryParse(string dateTime, out DateTime result){
 ...
    }
}

2.5.3.1. 考虑在成员中使用Try-Parse模式来避免因异常引起的性能问题,如果成员在常用代码中都可能会抛出异常。

2.5.3.2. 要在实现Try-Parse模式时使用“Try”前缀,并用布尔类型作为方法的返回类型

2.5.3.3. 要为每个使用Try-Parse模式的方法提供一个会抛出异常的对应成员

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2015-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 韩大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,将分4天进行推送,今天推送6-7章。
  • 1. 扩展性设计
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档