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

此文是《.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. 要优先使用受保护的虚成员,而不是公有的虚成员。公有成员应该通过调用受保护的虚成员的方法来提供扩展性。

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. 考虑使用辅助方法来创建异常

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

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. 不要在框架的代码捕获具体类型不确定的异常时把错误吞了

try {
   File.Open(...);
}
catch (Exception e)
{ } //吞了异常,不要这样做

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

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

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语句。这是保持异常调用栈不变的最好方法。

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隐式参数的名字。

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. 要为所有的异常(至少)提供下面这些常用的构造函数

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模式

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

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

2.5.3. Try-Parse模式

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模式的方法提供一个会抛出异常的对应成员

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

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

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

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏互联网技术栈

《Effective Java 》系列一

编写实例受控类有几个原因。实例受控使得类可以确保他是一个Singleton或者是不可实例化的。他还使得不可变类可以确保不会存在两个相等的实例。

1804
来自专栏ASP.NETCore

.NET Core中妙用unsafe减少gc提升字符串处理性能

昨天在群里讨论怎么样效率的把一个字符串进行反转,一般的情况我们都知道,只要对String对象进行操作, 那么就会生成新的String对象,比如"1"+"2" 这...

4401
来自专栏程序员宝库

这些 Java 面试题必须会---鲁迅

写在前面 春天来了,万物复苏的季节到了. 许多程序猿安奈不住生理需求,我要涨工资,我要跳槽. 毕竟金三银四嘛. 那么要从众多的面试者中获得求职机会,我们就要面对...

30610
来自专栏黑泽君的专栏

java注解用法详解——@SuppressWarnings

  在java编译过程中会出现很多警告,有很多是安全的,但是每次编译有很多警告影响我们对error的过滤和修改,我们可以在代码中加上 @SuppressWarn...

1.1K3
来自专栏后台全栈之路

Python 调用 C 动态链接库,包括结构体参数、回调函数等

项目中要对一个用 C 编写的 .so 库进行逻辑自测。这项工作,考虑到灵活性,我首先考虑用 Python 来完成。

54411
来自专栏python3

python3--序列化模块,hashlib模块

__len__    len(obj)的结果依赖于obj.__len__()的结果,计算对象的长度

2331
来自专栏JavaQ

99%的高级程序员都这样使用null

如果使用某个对象或对象里属性前先判断是否为null,那就需要思考一下你的代码是否已经烂掉了。 null是什么意思,你能说清楚它的意图吗?方法返回了null,是出...

3386
来自专栏听Allen瞎扯淡

一起 Static 和 Synchronized 引发的血案

这两天在定位一个网上问题的时候发现一个很诡异的现象,系统夜间的汇总任务跑了很长一段时间才能结束,而且日志显示这些汇总任务的每个子任务都很快就结束了,但整体任务还...

5392
来自专栏WindCoder

Java设计模式学习笔记—工厂模式

想学习设计模式很久了,趁现在有时间边学习边记录一下。目前设计模式学习主要基于菜鸟教程的设计模式,后期不排除会追加从其他地方学来内容。

781
来自专栏FreeBuf

PHP网站渗透中的奇技淫巧:检查相等时的漏洞

PHP是现在网站中最为常用的后端语言之一,是一种类型系统 动态、弱类型的面向对象式编程语言。可以嵌入HTML文本中,是目前最流行的web后端语言之一,并且可以和...

5538

扫码关注云+社区

领取腾讯云代金券