程序状态决定软件质量

来源:互联网 发布:数控编程代码案例 编辑:程序博客网 时间:2024/06/06 08:48

1.    概述

软件开发经过这么多年的发展,已经造就出来一系列的方法论,从最初的面向过程,然后到面向对象,再然后为了补偿面向对象表现能力的不足而出现的面向方面、面向服务等。而为了推进和支持这些方法论的发展,众多研究机构和商业企业也研究出来了一系列的语言、过程和工具。那为什么会出现这么多方法论、语言、过程和工具?由于世界的飞速发展,企业对于信息化的需求也越来越高,这也就直接导致了软件本身复杂度的提高。在软件复杂度日益提高的今天,软件的制造者-程序员的开发水平却没能因此而得到本质性的提高。于是出现了所谓的软件危机,接着也就出现了所谓的软件工程,软件开发也就被提升到了所谓的工程层次。在软件工程理论不断得到完善的过程中,很多研究机构和商业企业参考这些理论:为了减少软件开发的难度和效率,出现了各种所谓的高效率和高质量的编程语言以及所谓的程序方法论;为了减少软件开发的复杂度,出现了所谓的软件过程,试图从工程的角度规范整个的软件开发流程;为了有效地支持这些层出不穷的方法论、编程语言和软件过程,同时也出现了很多软件开发集成和CASE工具等。

从方法论、编程和软件工程以及工具的发展来看,我们不难看出,我们都在解决同一个问题,那就是减少软件开发的复杂度,而最终的目的就是保证软件本身的质量(正确性、性能、可伸缩性、可维护性等)。但是软件开发发展到今天,软件的复杂度是如此之高,我们已经没有任何的办法来保证一个软件百分之百的正确。所以在衡量最终的软件质量的时候,我们也只能给出一个相对的指标。虽然如此,我们的软件设计师们似乎也已经看到这一点,于是也就是出现了所谓的契约式设计(Design By Contract)。这种设计理念的确可以保证软件的开发质量(在这篇文章提到的软件质量只针对软件的正确性,因为正确性是软件其他质量的基本前提,也就是说正确性是软件的本质之所在),但是如上面所提到的一样,我们最终也还是无法保证软件百分之百的正确。

软件开发的本质就是完全正确而有效地解决现实中的问题,从本质上来讲也就是提高软件开发的质量。那为什么契约式设计(Design By Contract)能够提高软件开发的质量?这篇文章将从程序状态的角度来探讨这个问题。

2.    程序状态

所谓的程序状态就是程序在运行的时候在某个特定的场景中所表现出来的外在特征。如果把程序当作是一系列场景的连续转换的有限状态机,那么程序在某个场景中表现出来的状态(程序状态)决定了最终的程序状态。我们不能单纯用好或坏来描述某个程序状态,因为在特定的场景中程序状态所表现出来的意义是不同的。所以我们需要以一种很模糊的观点来考察程序状态,也就是所谓的禅学观点:好即是坏,坏就是好。

在这个状态机中,某个特定场景下的程序状态将决定后续状态,从而决定最终的程序状态。因此每个程序状态所处的场景都会是程序的一个敏感点、关键点。如果我们能够保证每个敏感点或关键点的程序状态都是正确的话,那么我们就能保证最终程序状态的正确性。

然而,在现实的环境中,软件复杂度已经决定了程序状态本身的复杂,而程序状态的复杂性也最终决定程序的正确性。按照这种推理,足够高的软件复杂度将导致软件不能达到百分之百的正确,如果将这些复杂度分派到众多的程序状态,如果我们能够保证程序状态的正确性,那么必然会降低软件的复杂度,从而提高软件的质量。

契约式设计(Design By Contract)恰恰就是将软件的复杂度分派到众多的程序状态,通过保证每个程序状态的正确性来保证最终软件的质量。

3.    契约式设计(Design By Contract

契约式设计的理念由来已久,是由Bertrand.Meyer提出来的。当然一个方法论,一个理念是需要通过实践来证明的。因此也就出来了所谓的契约式设计语言Eiffel,随之很多编程语言如JavaC++等也加入了对契约式设计的支持。

3.1.1.    什么是契约式设计

契约式设计的理念其实很简单,就是在设计和编码的阶段向面向对象的程序中加入断言(assertion)。而所谓的断言,实际就是必须为真的假设,只有这些假设为真,程序才可能做到正确无误,从而保证程序状态的正确性。契约式设计的主要断言包括先验条件(preconditon)、后验条件(postcondition)以及不变式(invariant):

n  先验条件针对方法,它规定了在调用该方法之钱必须为真的条件

n  后验条件也是针对方法,它规定了方法顺利执行完毕之后必须为真的条件

n  不变式针对整个累,它规定了该累任何实例调用任何方法时都必须为真的条件

3.1.2.    契约式设计的优点

一个理念的可接受程度往往取决于它人们带来的利益。契约式设计也是,不像那些所谓的过程和工具,它给程序设计注入了新新的理念,这对程序员以及程序本身来说都不能不说是一件幸事。这主要表现在以下几个方面:

n  对于程序员而言,契约式设计理念符合程序员的思维模式

n  它可以为程序本身提供优质而有用的文档

n  能够在运行时进行检测,更好地支持测试和调试工作

3.1.3.    契约式设计原则

原则一 区分命令和查询

查询返回一个结果,但不改变对象的可见性质。命令改变对象的状态,但不返回结果。

原则二 将基本查询和派生查询分开

派生查询可以用基本查询来定义。

原则三 针对每个派生查询,设定一个后验条件,使用一个或多个基本查询的的结果来定义它

这样一来,只要我们知道基本查询的值,也就能知道派生查询的值。

原则四 对于每个命令都撰写一个后验条件,规定每个查询的值

结合“用基本查询来定义派生查询”的原则,我们现在已经能够知道每个命令的全部可见的特性。

原则五 对于每个查询和命令,采用一个合适的先验条件

先验条件限定了客户调用查询和命令的时机。

原则六 撰写不变式来定义对象的恒定特定

类是某种抽象的体现,应当将注意力集中在最重要的属性上,这也符合20/80的原则,以保证程序的阅读者建立关于类抽象的精确的概念模型。

3.1.4.    契约式设计示例

根据上面的有些基本原则,我们实现了一个简单的契约式设计案例。

无庸置疑Eiffel是表达契约式设计理念最好的选择。但在这个示例中,我们不使用Eiffel。因为目前很多人(包括我)对Eiffel不是太熟悉,其次我们只是在表达一种理念,从本质上说来,一种好的、普遍的理念是可以通过任何有效的方式来进行表达。所以我们采用了C++,而且这个C++是普遍意义上的没有加入契约式设计支持的C++。更多内容,请参考附录一

4.    程序状态自检测

在上面,我们谈到了程序状态,谈到了契约式设计,也罗列出了契约式设计的一些基本原则。上面我有论述,可以把程序当做一个有限状态机,每个敏感点都可以决定最终的程序状态,从而决定最终的软件质量。也就是说,如果我需要得到好的软件质量的话,那么我就需要保证每个敏感点的程序状态。如果说契约式设计能够很多地保证的软件的质量,那么说它必然需要保证每个敏感点的程序状态。那么契约式设计的基本原则是如何保证程序的状态的呢?

命令改变了程序的状态,为了验证(前验和后验)程序状态改变前后的状态的变化,我们必然要通过查询(基本查询或/和派生查询)或不变式属性来获取程序的状态来进行检测。而查询本身并不会改变程序的状态。所以说,在某个类中,如果我们能够保证类中定义的没个命令都能够得到验证,那么类的实例状态就可以得到保证。以此类推。我称这个过程为程序状态自检测,即通过验证命令来验证查询从而来验证程序状态。

5.    结束语

如上所述,我们不难看出,契约式设计对于软件开发的确有着很重要的意义。相对工具和过程那种补偿人不足的方式,契约式设计以一种完善人的方式从本质上提高软件开发的质量。它的意义主要表现在三个方面,一是提高软件质量,二是有助于得到优秀的设计,三是有助于提高文档与代码的一致性。为了提高软件开发的质量,为了完善自我,我们何不一试。 

附录一 契约式设计示例

在这个示例中,我们设计了一个简单的集合类。

第一步 我们定义前验和后验表达式:

//前验表达式

#define DbcRequire(condition) /

GetCallStack()->Push((condition),__FILEW__,__FUNCTIONW__,__LINE__);

//后验表达式

#define DbcEnsure(condition) /

GetCallStack()->Push((condition),__FILEW__,__FUNCTIONW__,__LINE__);

 

第二步 我们根据契约式设计原则,定义我们的集合类:

template<typename T>

class DbcCollection

{

       typedef std::vector<T> ElementsType;

 

public:

       //////////////////////////////////////////////////////////////////////////

       //创建

       inline DbcCollection(void);

       inline ~DbcCollection(void);

       //////////////////////////////////////////////////////////////////////////

       //命令

       inline void Add(const T& element);

       inline void Insert(int index,const T& element);

       inline void Remove(int index);

       inline void Update(int index,const T& element);

       //////////////////////////////////////////////////////////////////////////

       //基本查询

       inline const T& Get(int index) const;

       inline int GetLength() const;

       //////////////////////////////////////////////////////////////////////////

       //派生查询

       inline bool IsEmpty() const;

       inline bool Contains(const T& element);

 

private:

       ElementsType m_elements;

};

第三步 实现集合类,为了简单起见,我们只列出一个方法的实现:

              template<typename T>

inline void DbcCollection<T>::Add(const T& element)

       {

              //前验。通过派生查询Contains,验证此集合不包含新添加的元素

              DbcRequire(!m_DbcCollection.Contains(element));

              int oldCount = m_DbcCollection.GetLength();

//添加新元素到集合中

              m_elements.push_back(element);

              //后验。通过基本查询GetLength,验证集合的元素数量增加了1

              DbcEnsure(m_DbcCollection.GetLength() == (oldCount+1));

//后验。通过派生查询Contains,验证新元素已经被添加进了集合中

              DbcEnsure(m_DbcCollection.Contains(element));

}

 
原创粉丝点击