切削刃

代码约定:继承和 Liskov 原则

Dino 埃斯波西托

Dino Esposito
就像现实生活合同软件合同将您绑定到附加约束和成本你东西的时间。当站在一份合同时,可能要确保你不会职权。当它来到软件合同 — — 包括 microsoft 的代码的合同。NET 框架 — — 几乎每个开发人员将最终体现的周围类合同计算成本的疑虑。合同是否适合您的软件,不管的版本的类型吗?或者是合同而大多是应该脱掉零售代码的调试援助吗?

埃菲尔,介绍软件合同的第一语言有本机语言关键字定义的先决条件,应该和不变量。因此,在埃菲尔,合同是语言的一部分。在源代码中使用的类,如果合同将成为代码的一个组成部分。

中。净,虽然合约是框架的一部分,并不属于受支持的语言。这意味着运行时检查可以启用或禁用在将。特别是,在。您就可以在每个生成配置基础上决定有关合同的网。在 Java 中,东西都几乎相同。使用外部框架,或者将合同代码添加到要编译的源或问周围框架,相应地修改字节码的工具。

在本文中,我将讨论在代码合同证明特别有用的驾驶您对软件的总体设计的高质量的几个方案。

代码合同是什么

软件开发人员的常绿的最佳实践写仔细检查他们收到的任何输入的参数的方法。如果输入的参数不匹配方法的期望,则引发异常。这种称为如果再抛模式。合同的前提条件,与此相同的代码看起来更好、 更紧凑。更有趣的是,它还会读取更好,因为前提让你清楚只是需要的而不是测试针对什么不是很理想。所以,乍一看,软件合同只是看起来好写方法,以防止类方法中的异常。嗯,有它比刚才更多。

你认为合同的每个方法的简单的事实表明你现在在想更多关于这些方法的作用。最后,设计获取 terser 和 terser。与合同也代表了有价值的文档,特别是重构目的形式。

代码的合同,不过,不限于先决条件,即使先决条件是最容易的部份的软件合同捡起。前提条件,应该和不变量组合 — — 在整个代码中的广泛应用 — — 为您提供了决定性的优势,并带来了一些高质量的代码。

断言 vs。代码合同 vs。测试

代码合同不完全像断言和调试的其他文书。虽然合同可以帮助您跟踪的 bug,他们不要更换好的调试器或全熟组的单元测试。断言,像代码合同说明必须在某一时刻验证程序的执行过程中的一个条件。

失败的断言是一种症状,有什么地方不对劲。断言,但是,不能告诉你它失败的原因和问题的来源。代码合同的失败,另一方面,告诉你很多。它共享有关一种失败的详细信息。因此,例如,您可以了解是否引发异常,因为给定的方法收到不能接受的值、 计算预期的返回值中失败或包含无效的状态。而断言告诉您只对检测到的不良症状,则代码合同可以显示宝贵的信息应如何使用该方法。此信息可能最终帮助您了解什么有固定为停止违反给定的断言。

软件合同与单元测试如何相关的?很明显,一个并不排除其他和两个功能是种正交。测试工具是一个外部程序,通过应用选定的类和方法,看他们的行为方式输入固定的工作。合同是要喊出时有什么不舒服, 的类方法。要测试的合同,但是,您必须运行该代码。

单元测试是一个伟大的工具,赶上回归后重构过程很深。合同也许是更多比测试方法的预期的行为的文档信息。若要获取设计值的测试,你必须练习测试驱动的开发 (TDD)。合约可能是比 TDD 文档和设计方法的简单工具。

合同向代码中添加额外的信息,并把它留给你来决定是否该信息应使它已部署的二进制文件。单元测试包括外部的项目,可以估计代码如何做的。是否您编译合同信息或不,有事先明确合同信息有助于为文档和设计的援助。

代码合同和输入数据

合同请参阅总是在正常执行流的程序适用的条件。这似乎表明您可能要使用合同的理想的地方是只服从输入严格控制由开发人员的内部图书馆。直接暴露对用户输入的类不一定是合同的好地方。如果您对未筛选的输入数据设置的先决条件,合同可能会失败并引发异常。但这真的是你想要什么?大多数情况下,您要柔和或有礼貌的消息返回给用户。你不想异常和不想抛出,然后陷阱的例外只是正常恢复。

中。网、 代码合同属于库,可能是好的补充 (以及在某些情况下,替换) 数据注释。数据注释很大程度上的对 UI,因为在 Silverlight 和 ASP。必须了解这些批注和调整代码或 HTML 组件的网络输出。域层,不过,您经常需要不仅仅是属性,和代码合同是理想的替代品。我不说你不能得到相同的功能,你可以用代码合同的属性。我找到的可读性和表现力,结果却与代码合同属性比任何时候都更好。(顺便说一句,这正是为什么代码合同团队通过属性喜欢纯代码。)

继承的合同

软件合同是在几乎所有平台,支持他们,可继承的。NET 框架也不例外。当您从一个现有派生新类时,派生的类拾取行为、 上下文和父母的合同。这似乎是理所当然的事情。继承的合同不会造成任何不变量和应该的问题。虽然是有点问题的前提条件。让我们来解决不变量和考虑中的代码图 1

图 1继承不变量

public class Rectangle
{
  public virtual Int32 Width { get; set; }
  public virtual Int32 Height { get; set; }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(Width > 0);
    Contract.Invariant(Height > 0);
  }
}
public class Square : Rectangle
{
  public Square()
  {
  }

  public Square(Int32 size)
  {
    Width = size;
    Height = size;
  }

  [ContractInvariantMethod]
  private void ObjectInvariant()
  {
    Contract.Invariant(Width == Height);
  }
  ...
}

矩形基类有两个变量: 宽度和高度都大于零。 广场的派生的类中添加另一个不变的条件: 宽度和高度必须匹配。 即使从逻辑的角度来看,这是有意义。 广场就像一个矩形除外,它具有附加约束: 宽度和高度必须始终是相同的。

应该,事情大多是工作方式相同。 派生的类重写的方法,只是添加更多的应该补充了基类的功能,像是一种特殊情况的所有父不会不会的父类别和更多。

先决条件,然后又如何? 这正是为什么总结合同跨类层次结构是一个微妙的操作。 类方法逻辑上来说,是一个数学函数相同。 获取一些输入的值和产生一些输出。 在数学中,生成的函数值的范围是称为圆锥曲线 ; 域是可能的输入值的范围。 通过添加不变量和应该到派生的类的方法,你只是增加方法的圆锥曲线的大小。 但通过添加先决条件,您限制方法的域。 这是你真的应该担心的东西吗? 阅读上。

利斯科夫原则

固是受欢迎的首字母缩写而产生的五个关键原则的软件设计,包括单一责任、 打开/关闭、 接口隔离和依赖倒置的缩写。 固体中的 L 代表替换原则。 您可以了解很多有关利斯科夫原则在bit.ly/lKXCxF

简单地说,利斯科夫原则指出应该始终是安全的在父类别预计的任何地方使用子类。 这是为重点的听起来,我们走出与普通对象定位框的东西。 没有任何的面向对象的语言的编译器可以确保始终保存的原则的魔法。

它是精确开发商有责任确保它是安全的地方父类别预计使用派生的任何类。 通知说,"安全"。平原对象定位使得在父类别预计的地方使用派生的任何类。 "有可能"并不相同,"安全"。要实现利斯科夫原则,您需要坚持简单规则: 域的一种方法不能收缩在子类中。

代码合同和利斯科夫原则

除了正式和抽象的定义,利斯科夫原则有很多事来做软件合同,并可以轻松地将特定的技术等方面。NET 代码的合同。 关键的一点是派生的类不能只是添加的先决条件。 在这样做时,它会限制被接受的一种方法,可能创建运行时失败的可能值的范围。

请务必注意违反原则,并不一定导致运行时异常或不良行为。 但是,它是一个可能的反例中断您的代码的符号。 换句话说,违反的影响可能会波及整个代码库和邪恶症状明显不相关的领域。 它使整个基本代码更加努力,维护和发展 — — 这几天一桩大罪。 想象你在代码图 2

图 2说明利斯科夫原则

public class Rectangle
{
  public Int32 Width { get; private set; }
  public Int32 Height { get; private set; }

  public virtual void SetSize(Int32 width, Int32 height)
  {
    Width = width;
    Height = height;
  }
}
public class Square : Rectangle
{
  public override void SetSize(Int32 width, Int32 height)
  {
    Contract.Requires<ArgumentException>(width == height);
    base.SetSize(width, width);
  }
}

类广场从矩形继承,并只添加一个前提条件。 此时,将会失败 (这表示可能的反例) 下面的代码:

private static void Transform(Rectangle rect)
  {
    // Height becomes twice the width
    rect.SetSize(rect.Width, 2*rect.Width);
  }

变换最初编写方法对付矩形类的实例,它做得很好。 假设有一天您扩展系统,开始将广场的实例传递给相同 (非接触) 代码,如下所示:

var square = new Square();
square.SetSize(20, 20);
Transform(square);

取决于方形和矩形之间的关系,变换方法可能会启动失败没有明显的解释。

更糟的是,你可能很容易魔如何解决这个问题,但由于类的层次结构,它不可能要掉以轻心的东西。 因此你最终会修复 bug 的一种解决方法,如下所示:

private static void Transform(Rectangle rect)
{
  // Height becomes twice the width
  if (rect is Square)
  {
    // ...
return;
  }
  rect.SetSize(rect.Width, 2*rect.Width);
}

但你的努力,不管的臭名昭著的泥球刚刚开始变大。 谈好的事情。NET 和 C# 编译器是如果您使用代码合同来表达的先决条件,您得到警告从编译器如果你违反利斯科夫原则 (请参见图 3)。

The Warning You Get When You’re Violating the Liskov Principle

图 3你当你违反利斯科夫原则的警告

最了解、 最适用的固体原则

因教过。几年来的净设计类,我认为我可以安全地说固体的原则,利斯科夫原则是到目前为止最不理解和应用。 很多时候,在软件系统中检测到的怪异行为可以跟踪,利斯科夫原则的违反。 好不够,代码合同可以帮助大大在这一领域,只要你仔细看看编译器警告。

Dino Esposito是《Programming Microsoft ASP.NET 4》(Microsoft Press,2011 年)的作者,同时也是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2008 年)的合著者。埃斯波西托在意大利是频繁的扬声器,在全球范围内的行业活动。你可以跟随他在 Twitter 上twitter.com/despos

衷心感谢以下技术专家对本文的审阅:曼努埃尔 Fahndrich