根据一种很好的旧软件开发做法,应在每个方法的顶部(即实现任何重要行为之前)放置一个条件语句作为屏障。 每个条件语句都检查输入值必须验证的不同条件。 如果条件未通过验证,代码会引发异常。 这种模式通常称为 If-Then-Throw。
但是,有了 If-Then-Throw,我们就可以编写出高效正确的代码吗? 是不是在所有情况下,这都足够了?
If-Then-Throw 不是在所有情况下都能解决所有问题,这不是什么新观点。 根据约定设计 (DbC) 是 Bertrand Meyer 几年前提出的方法,这种方法基于这样一种想法,即每个软件都拥有一个正式描述其输入和输出的约定。 If-Then-Throw 模式几乎涵盖了约定的第一部分,但它完全不涉及第二部分。 任何主流编程语言都不是天然支持 DbC 的。 不过,通过现有的一些框架,您可以尝试在常用语言(如 Java、Perl、Ruby、JavaScript 语言,当然还有 Microsoft .NET Framework 语言)中采用 DbC 方法。 在 .NET 中,可以通过 .NET Framework 4 中增加的代码约定库实现 DbC,该代码约定库位于 mscorlib 程序集中。 请注意,该库可用于 Silverlight 4 应用程序,不能用于 Windows Phone 应用程序。
我相信几乎每个开发人员都原则上同意,约定优先开发方法是一种极好的开发方法。 不过我认为,在 .NET 4 应用程序中积极使用代码约定的人并不多,因为 Microsoft 已提供了软件约定并将其集成在 Visual Studio 中。本文着重介绍约定优先方法在代码维护和简化开发方面的优势。 在开发下一个项目时,您可以借鉴本文观点向领导推荐代码约定。 以后,我将在本专栏中对某些方面深入探讨,如配置、运行时工具和编程功能(如继承)。
有关简单 Calculator 类的推论
代码约定关乎心态;您不应等到必须设计需要超级体系结构并采用很多前沿技术的大型应用程序时才想起使用代码约定。 请注意,如果管理不善,再强大的技术也可能带来问题。 只要熟练掌握代码约定,代码约定就适用于几乎任何类型的应用程序。 上海网站建设 我们从一个简单的类开始,一个经典的 Calculator 类,如下所示:
public class Calculator
{
public Int32 Sum(Int32 x, Int32 y)
{
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
return x / y;
}
}
您可能会同意,此处不是实际的代码,因为它至少缺少一个重要的部分:检查是否要尝试执行除数为零的运算。 我们要完善代码,因此,我们还假定还有一个问题要处理:该计算器不支持负值。 图 1 是该代码的更新版本,其中添加了一些 If-Then-Throw 语句。
图 1 实现 If-Then-Throw 模式的 Calculator 类
public class Calculator
{
public Int32 Sum(Int32 x, Int32 y)
{
// Check input values
if (x <0 || y <0)
throw new ArgumentException();
// Perform the operation
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
// Check input values
if (x < 0 || y < 0)
throw new ArgumentException();
if (y == 0)
throw new ArgumentException();
// Perform the operation
return x / y;
}
}
到现在,可以这样说,我们的类或者开始处理其输入数据,或者(如果输入无效)在执行任何运算之前引发异常。 该类生成的结果如何? 我们了解它们的哪些情况? 根据规范,这两个方法都应返回不小于零的值。 我们如何强制实现这一点并在它未发生时失败? 我们需要该代码的第三个版本,如图 2 所示。
图 2 检查前置条件和后置条件的 Calculator 类
public class Calculator
{
public Int32 Sum(Int32 x, Int32 y)
{
// Check input values
if (x <0 || y <0)
throw new ArgumentException();
// Perform the operation
Int32 result = x + y;
// Check output
if (result <0)
throw new ArgumentException();
return result;
}
public Int32 Divide(Int32 x, Int32 y)
{
// Check input values
if (x < 0 || y < 0)
throw new ArgumentException();
if (y == 0)
throw new ArgumentException();
// Perform the operation
Int32 result = x / y;
// Check output
if (result < 0)
throw new ArgumentException();
return result;
}
}
两个方法现在都包含三个不同阶段:检查输入值、执行运算和检查输出。 对输入和输出的检查分别有不同的目的。 输入检查可标记调用方代码中的 Bug。 输出检查可发现您自己的代码中的 Bug。 您是否真的需要对输出进行检查? 当然,通过某些单元测试中的声明可以验证检查条件。 在这种情况下,您不一定需要在运行时代码中包含这种检查。 但是,代码中的检查可使类具有自我描述性,可以清楚地表明它可以和不可以执行的操作,这与约定服务的条件十分相似。
如果将图 2 中的源代码与我们开始时介绍的简单类进行比较,就会看到源代码增加了很多行,这是一个只需要满足很少要求的简单类。 让我们再进一步。
在图 2 中,我们确定的三个步骤(检查输入、运算和检查输出)是顺序执行的。 如果运算的执行十分复杂,需要加入其他退出点,该怎么处理? 如果某些退出点引用会产生其他结果的错误条件,该怎么处理? 事情确实可能会很复杂。 但为了进行说明,向其中一个方法添加一个快捷退出就足够了,如图 3 所示。
图 3 快捷退出复制后置条件代码
public Int32 Sum(Int32 x, Int32 y)
{
// Check input values
if (x <0上海企业网站制作 || y <0)
throw new ArgumentException();
// Shortcut exit
if (x == y)
{
// Perform the operation
var temp = x <<1; // Optimization for 2*x
// Check output
if (temp <0)
throw new ArgumentException();
return temp;
}
// Perform the operation
var result = x + y;
// Check output
if (result <0)
throw new ArgumentException();
return result;
}
在示例代码中(这只是 一个示例),如果两个值相等(相乘而不是相加),则方法 Sum 尝试一种快捷方式。 但是,必须针对代码中的每个提前退出路径复制输出值检查代码。
关键是没有人可以合理地设想采用一种约定优先的软件开发方法而不使用一些重要工具,至少是使用特定的帮助程序框架。 检查初步条件相对容易且成本较低;如果手动处理执行后的条件,整个基本代码会难以处理,并且容易出错。 更不用说,对于开发人员而言,约定的其他一些附属方面会使类的源代码一片混乱,例如,在输入参数是集合时检查条件,以及确保每当调用方法或属性时类总是处于已知有效状态。
输入代码约定
在 .NET Framework 4 中,代码约定是一个框架,它提供了更加方便的语法来表达类约定。 具体而言,代码约定支持三种约定:前置条件、后置条件和固定条件。 前置条件指为了安全执行方法而应该验证的初步条件。 后置条件指方法正确执行或引发异常后应该验证的条件。 最后,固定条件指在任何类实例的生存期内始终为真的条件。 更准确地说,固定条件是在类与客户端之间的每个可能交互之后(即在执行包括构造函数在内的公共成员之后)都必须保持的条件。 固定条件是不会检查的,因此,在调用私有成员之后,可能暂时违反这种条件。
代码约定 API 由一组在类约定上定义的静态方法组成。 您可以使用 Requires 方法表示前置条件,使用 Ensures 方法表示后置条件。 图 4 说明如何使用代码约定重新编写 Calculator 类。
图 4 使用代码约定重新编写的 Calculator 类
using System.Diagnostics.Contracts;
public class Calculator
{
public Int32 Sum(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
Contract.Ensures(Contract.Result<Int32>() >= 0);
if (x == y)
return 2 * x;
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
Contract.Requires<ArgumentOutOfRangeException>(y > 0);
Contract.Ensures(Contract.Result<Int32>() >= 0);
return x / y;
}
}
粗略比较一下图 3 和图 4,可以看出 API 实现 DbC 的有效性。 方法代码重新回到一种高度可读的形式,在其中只有两个层次:约定信息(同时包括前置条件和后置条件)和实际行为。 您不必像图 3 中那样,将条件和行为混在一起。 因此,可读性大大提高,对于团队来说,代码维护容易多了。 例如,您可以根据需要,快速安全地添加新的前置条件或编辑后置条件,您只需在一个位置进行更改,还可以清楚地对更改进行跟踪。
约定信息可以通过普通 C# 或 Visual Basic 代码表示。 约定说明不同于经典的声明性属性,但它们保留了很强的声明性风格。 使用纯代码而不使用属性可提高开发人员的编程能力,因为在这种方式下,可以更自然地表达心中所想的条件。 同时,在重构代码时,代码约定更具指导性。 实际上,代码约定会指出方法的可用行为。 代码约定有助于在编写方法时遵循编码准则,有助于在前置条件和后置条件较多时保持代码的可读性。 即使可以像图 4 那样使用简洁的语法表达约定,在实际编译代码时,所获得的代码流也可能与图 3 中的代码差不多。 那么诀窍在哪里?
Visual Studio 生成过程中集成的另一个工具(代码约定重写程序)可以重塑代码,了解所表达的前置条件和后置条件的预定用途,并将它们的逻辑置入正确的代码块中。 作为开发人员,如果您在某处编辑代码以添加另一个退出点,则不必担心在哪里放置后置条件和在哪里将其复制。
表示条件
您可以参考代码约定文档确定前置条件和后置条件的准确语法;可以从 DevLabs 网站 bit.ly/f4LxHi 获取最新 PDF。 我简单概述一下。 您可以使用以下方法指示所需条件,不符合条件则引发指定的异常:
Contract.Requires<TException> (Boolean condition)
该方法有几个可能需要考虑的重载。 方法 Ensures 表示后置条件:
Contract.Ensures(Boolean condition)
在编写前置条件时,表达式通常仅包含输入参数,可能包含同一个类中的某个其他方法或属性。 如果是这种情况,则需要使用 Pure 属性对该方法进行修饰,以指明执行该方法不会改变对象的状态。 请注意,代码约定工具假定属性 getters 是纯的。
在编写后置条件时,可能需要访问其他信息,如局部变量的返回值或初始值。 为此,可以使用一些特定方法,例如,使用 Contract.Result<T> 获取方法的返回值(类型为 T),使用 Contract.OldValue<T> 获取方法开始执行时存储在指定局部变量中的值。 最后,还可以在方法执行期间引发异常时对条件进行验证。 在这种情况下,可以使用方法 Contract.EnsuresOnThrow<TException>。
缩写方法
约定语法肯定比使用纯代码更加紧凑,但也可能变得很大。 发生这种情况时,可读性又会受到影响。 一个自然的补救方法是将多个约定指令组合在一个子例程中,如图 5 所示。
图 5 使用 ContractAbbreviator
public class Calculator
{
public Int32 Sum(Int32 x, Int32 y)
{
// Check input values
ValidateOperands(x, y);
ValidateResult();
// Perform the operation
if (x == y)
return x<<1;
return x + y;
}
public Int32 Divide(Int32 x, Int32 y)
{
// Check input values
ValidateOperandsForDivision(x, y);
ValidateResult();
// Perform the operation
return x / y;
}
[ContractAbbreviator]
private void ValidateOperands(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
}
[ContractAbbreviator]
private void ValidateOperandsForDivision(Int32 x, Int32 y)
{
Contract.Requires<ArgumentOutOfRangeException>(x >= 0 && y >= 0);
Contract.Requires<ArgumentOutOfRangeException>(y > 0);
}
[ContractAbbreviator]
private void ValidateResult()
{
Contract.Ensures(Contract.Result<Int32>() >= 0);
}
}
ContractAbbreviator 属性指示重写程序如何正确解释带相应修饰符的方法。 实际上,如果不使用该属性将其限定为一种可扩展的宏,ValidateResult 方法(以及图 5 中的其他 ValidateXxx 方法)可能包含相当复杂的代码。 例如,在 void 方法中使用时,Contract.Result<T> 会引用什么内容? 目前,mscorlib 程序集不包含 ContractAbbreviator 属性,因此,开发人员必须在项目中进行明确定义。 该类很简单:
namespace System.Diagnostics.Contracts
{
[AttributeUsage(AttributeTargets.Method,
AllowMultiple = false)]
[Conditional("CONTRACTS_FULL")]
internal sealed class
ContractAbbreviatorAttribute :
System.Attribute
{
}
}
改进后的简洁代码
综上所述,代码约定 API(基本上就是 Contract 类)是 .NET Framework 4 本身的一部分,因为它属于 mscorlib 程序集。 Visual Studio 2010 在特定于代码约定配置的项目属性中提供了一个配置页。 对于每个项目,您必须明确启用约定的运行时检查。 您还需要从 DevLabs 网站下载运行时工具。 在该网站上,您可以选择适用于所用 Visual Studio 版本的安装程序。 运行时工具包括代码约定重写程序和接口生成器以及静态检查程序。
代码约定通过强制指示每个方法的行为和结果,有助于编写简洁的代码。 至少,它可在重构和改进代码时提供指南。 关于代码约定,还有很多内容需要讨论。 具体而言,在本文中,我只是简单提到固定条件,完全没有提及强大的功能约定继承。 在以后的文章中,我准备介绍这些内容以及相关内容。 请继续关注!