敏捷思惟-架构设计中的方法学(13)

十3、代码验证


  要保证架构的稳定和成功,利用代码对架构进行验证是一种实用的手段。代码验证的核心是测试,特别是单元测试。而测试的基本操做思路是测试优先,它是敏捷方法中很是重要的一项实践,是重构和稳定核模式的重要保障。

   面向对象体系中的代码验证

  代码验证是保证优秀的架构设计的一种方法,同时也是避免出现象牙塔式架构设计的一种措施。咱们在上一篇稳定化中提到说架构设计最终将会体现为代码的形式,所以使用形式化的代码来对架构进行验证是最有效的。

  因为是代码验证,所以就离不开编写代码,而代码老是和具体的语言、编译环境息息相关的。在这里咱们主要讨论面向对象语言,代码示例采用的Java语言。利用面向对象语言来进行架构设计有不少的好处:

  ◆ 首先,面向对象语言是一种更优秀的结构化语言,比起非面向对象语言,它可以更好的实现封装、下降耦合、并容许设计师在抽象层次上进行思考。这些因素为优秀的架构设计提供了条件。

  ◆ 其次,面向对象语言能够容许设计师只关注在框架代码上,而不用关心具体的实现代码。固然,这并非说非面向对象的语言就作不到这一点,只是面向对象语言的表现更优秀一些。

  ◆ 最后,面向对象语言能够进行很好的重用。这就意味着,设计师能够利用原有的知识、原有的软件体系,来解决新的问题。

  此外,利用Java语言,还能够得到更多的好处。Java语言是一种面向接口的语言。咱们知道,Java语言自己不支持多重集成,全部的Java类都是从Object类继承下来的。这样,一个继承体系一旦肯定就很难再更改。为了可以达到多重继承的灵活性,Java引入了接口机制,使用接口和使用抽象类并无什么不一样的地方,一个具体类能够实现多个接口,而客户端能够经过申明接口类型来使用,以下面这样:

List employees=new Vctor();


  若是须要将Vctor换成LinkedList,那么除了上面的建立代码,其它的代码不须要再作更多的修改。而Vctor这个具体类除了实现List这个接口之外,还实现了Cloneable、Collection、 RandomAccess、Serializable。这说明除了List接口以外,咱们还能够经过以上所列的接口来访问Vector类。所以接口继承可以成为类继承的补充手段,发挥十分灵活的做用。同时又避免了多重继承的复杂性。可是接口中只可以定义空方法,这是接口的一个缺陷。所以在实际编程中,接口和抽象类一般是一块儿使用的。咱们在Java的java.util包中看到Collection接口以及实现Collection接口的AbstractCollection抽象类就是这方面的例子。你能够从AbstractCollection抽象类(或其某个子类)中继承,这样你就可使用到AbstractCollection中的缺省代码实现,因为AbstractCollection实现了Collection接口,你的类也实现Collection接口;若是你不须要利用AbstractCollection中的代码,你彻底能够本身写一个类,来实现Collection接口(这个例子中不太可能发生这种状况,由于工具类的重用性已经实现设计的很是好了)。Java中有不少相似的例子。Java语言设计并非咱们讨论的重点,更加深刻的讨论能够参看专门的书籍,这里咱们就不做太多的介绍了。

  以上花了一些篇幅来讨论面向对象设计和面向接口设计的一些简单的预备知识。这些知识将成为代码验证的基础。

   接口和架构

  这里的接口指的并非Java中的Interface的概念,它是广义的接口,在Java语言中具体表现为类的公有方法或接口的方法。在COM体系或J2EE体系中还有相似但不彻底相同的表现。对于一个系统的架构来讲,最主要的其实就是定义这些接口。经过这些接口来将系统的类联系在一块儿,经过接口来为用户提供服务,经过接口来链接外部系统(例如数据库、遗留系统等)。所以,咱们为了对架构进行验证的要求,就转化为对接口的验证要求。

  对接口进行验证的基本思路是保证接口的可测试性。要保证接口具备可测试性,首先要作的是对类和类的职责进行分析。这里有几条原则,能够提升接口的可测试性。

  一、 封装原则

  接口的实现细节应该封装在类的内部,对于类的用户来讲,他只须要知道类发布出的公有方法,而不须要知道实现细节。这样,就能够根据类的共有方法编写相应的测试代码,只要知足这些测试代码,类的设计就是成功的。对于架构来讲,类的可测试性是基础,可是光保证这一条还不够。

  二、 最小职责原则

  一个类(接口)要实现多少功能一直是一个不断争论的问题。可是一个类实现的功能应该尽量的紧凑,一个类中只处理紧密相关的一些功能,一个方法更应该只作一件事情。这样的话,类的测试代码相应也会比较集中,保证了类的可测试性。回忆在分层模式中咱们讨论的那个例子,实现类为不一样的用户提供了不一样的接口,这也是最小原则的一个体现。

  三、 最小接口原则

  对于发布给用户使用的方法,须要慎之再慎。通常来讲,发布的方法应该尽量的少。因为公布的方法可能被客户频繁的使用,若是设计上存在问题,或是须要对设计进行改进,都会对现有的方法形成影响。所以须要将这些影响减到最小。另外一方面,一些比较轻型的共有方法应该组合为单个的方法。这样能够下降用户和系统的耦合程度,具体的作法能够经过外观模式,也可使用业务委托模式。关于这方面的讨论,能够参考分层模式。较少的接口能够减轻了测试的工做量,让测试工做更加集中。

  四、 最小耦合原则

  最小耦合原则说的是你设计的类和其它类的交互应该尽量的少。若是发现一个类和大量的类存在耦合关系,能够引入新的类来削弱这种耦合度。在设计模式中,中介模式和外观模式都是此类的应用。对于测试,尤为是单元测试来讲,最理想的状况是测试的类是一个单纯的类,和其它的类没有任何的关系。可是现实中这种类是极少的,所以咱们可以作的是尽量的下降测试类和其它的类的耦合度。这样,测试代码相对比较简单,类在修改的时候,对测试代码的影响也比较小。

  五、 分层原则

  分层原则是封装原则的提高。一个系统,每每有各类各样的职责,例若有负责和数据库打交道的代码,也有和用户打交道的代码。把这些代码根据功能划分为不一样的层次,就能够对软件架构的不一样部分实现大的封装。而要将类的可测试性的保证发展为对架构的可测试性的保证。就须要对系统使用分层原则,并在层的级别上编写测试代码。关于分层的详细讨论,请参见分层模式。

  若是你设计的架构没法知足上述的原则,那么能够经过重构来对架构加以改进。关于重构方面的话题,能够参考Martin Fowler的重构一书和Joshua Kerievsky的重构到模式一书。

  若是咱们深刻追究的话,到底一个可验证的架构有什么样的意义呢?这就是下一节中提到的测试驱动和自动化测试的概念。

   测试驱动

  测试驱动的概念可能你们并不陌生。在RUP中的一样概念是测试优先设计(test-first design),而在XP中则表现为测试优先编程(test-first programming)。其实咱们在平常的工做中已经不知不觉的在进行测试驱动的部分工做了,可是将测试驱动提升如此的高度则要归功于敏捷方法。测试驱动的基本思想是在对设计(或编码)以前先考虑好(或写好)测试代码,这样,测试工做就不只仅是测试,而成为设计(或代码)的规范了。Martin Fowler则称之为"specification by example"

  在敏捷测试领域。一种作法是将需求彻底表述为测试代码的形式。这样,软件设计师的需求工做就再也不是如何编写需求来捕获用户的须要,而是如何编写测试来捕获用户的须要了。这样作有一个很明显的好处。软件设计中的最致命的代码是在测试工做中发现代码不可以知足需求,发生这种状况有不少的缘由,可是其结果是很是可怕的,它将致使大量的返工。而将需求整理为测试代码的形式,最后的代码只要可以通过测试,就必定可以知足需求。固然,这种确定是有前提的,就是测试代码要可以完整、精确的描述需求。作到这一点可不容易。咱们能够想象一下,在对用户进行需求分析的时候,基本上是没有什么代码的,甚至连设计图都没有。这时候,要写出测试代码,这是很难作到的。这要求设计师在编写测试代码的时候,系统的总体架构已经胸有成竹。所以这项技术虽然拥有美好的前景,可是目前还远远没有成熟。

  虽然咱们没有办法彻底使用以上的技术,可是借用其中的思想是彻底有可能的。

  首先,测试代码取代需求的思想之因此好,是由于测试代码是没有歧义的,可以很是精确的描述需求(由于代码级别是最细的级别),并紧密结合架构。所以,从需求分析阶段,咱们就应该尽量的保持需求文档的可测试性。其中一个可能的方式是使用CRC技术。CRC技术可以帮助设计人员分析需求中存在的关键类,并找出类的职责和类之间的关系。在RUP中也有相似的技术。业务实体表明了领域中的一些实体类,定义业务实体的职责和关系,也可以有助于提升设计的可测试性。不管是哪种方法,其思路都是运用分析技术,找出业务领域中的关键因素,并加以细化。

  其次,测试驱动认为,测试已经不只仅是测试了,更重要的是,测试已经成为一种契约。用于指导设计和测试。在这方面,Bertrand Meyer很早就提出了Design by Contract的概念。从软件设计的最小的单元来看,这种契约其实是定义了类的制造者和类的消费者之间的接口。

  最后,软件开发团队中的全部相关人员若是都可以清楚架构测试代码,那么对于架构的设计、实现、改进来讲都是有帮助的。这里有一个关于测试人员的职责的问题。通常来讲,咱们认为测试人员的主要职责是找出错误,问题在于,测试人员大量的时间都花费在了找出一些开发人员不该该犯的错误上面。对于现代化的软件来讲,测试无疑是很是重要的一块,可是若是测试人员的平常工做被大量本来能够避免的错误所充斥的话,那么软件的质量和成本两个方面则会有所欠缺。一个优秀的测试人员,应该把精力集中在软件的可用性上,包括是否知足需求,是否符合规范、设计是否有缺陷、性能是否是足够好。除了发现缺陷(注意,咱们这里用的是缺陷,而不是错误),测试人员还应该找出缺陷的缘由,并给出改正意见。

  所以,比较好的作法是要求开发人员对软件进行代码级别的测试。所以,给出架构的测试代码,并要求实现代码经过测试是提升软件质量的有效手段。在了解了测试驱动的思路以后,咱们来回答上一节结束时候的问题。可验证架构的最大的好处是经过自动化测试,可以创建一个不断改进的架构。在重构模式中,咱们了解了重构对架构的意义,而保证架构的可测试性,并为其创建起测试网(下一节中讨论),则是架构可以得以顺利重构的基本保证。咱们知道,重构的基本含义是在不影响代码或架构外部行为的前提条件下对内部结构进行调整。可是,一旦对代码进行了调整,要想保证其外部行为的不变性就很难了。所以,利用测试驱动的思路实现自动化测试,自动化测试是架构外部行为的等价物,不论架构如何演化,只要测试可以经过,说明架构的外部行为就没有发生变化。

   针对接口的测试

  和前文同样,这里接口的概念仍然是广义上的接口。咱们但愿架构在重构的时候可以保持外部行为的稳定。但要作到这一点可不容易。发布的接口要保证稳定,设计师须要有丰富的设计经验和领域经验。前文提到的最小接口原则,其中的一个含义就是如此,发布的接口越多,从此带来的麻烦就越多。所以,咱们在设计架构,设计类的时候,应该从设计它们的接口入手,而不是一上手就思考具体的实现。这是面向对象思想和面向过程思想的一大差异。

  这里,咱们须要回顾在稳定化这一模式中提到的从变化中寻找不变因素的方法。稳定化模式中介绍的方法一样适用于本模式。只有接口稳定了,测试脚本才可以稳定,测试自动化才能够顺利进行。将变化的因素封装起来,是保持测试脚本稳定的主要思路。变化的因素和须要封装的程度根据环境的不一样而不一样。对一个项目来讲,数据库通常是固定的,那么数据访问的代码只要可以集中在固定的位置就已经可以知足变化的须要了。可是对于一个产品来讲,须要将数据访问封装为数据访问层(或是OR映射层),针对不一样的数据库设计可以动态替换的Connection。

   测试网

  本章的最后一个概念是测试网的概念。若是严格的按照测试优先的思路进行软件开发的话。软件完成的同时还会产生一张由大量的测试脚本组成的测试网。为何说是测试网呢?测试脚本将软件包裹起来,软件任何一个地方的异动,测试网都会马上反映出来。这就像是蜘蛛网同样,可以对需求、设计的变动进行快速、有效的管理。

  测试网的脚本主要是由单元测试构成的。所以开发人员的工做除了编写程序以外,还须要编织和修补这张网。编织的含义是在编写代码以前先编写测试代码,修补的含义是在因为软件变动而致使接口变动的时候,须要同步对测试脚本进行修改。额外的工做看起来彷佛是加大了开发人员的工做量。但在咱们的平常实践中,咱们发现事实正好相反,一开始开发人员虽然会由于构建测试网而致使开发速度降低,可是到了开发过程的中期,测试网为软件变更节约的成本很快就可以抵消初始的投入。并且,随着对测试优先方法的熟悉和认同,构建测试网的成本将会不断的降低,而起优点将会愈来愈明显:

  ◆ 可以很容易的检测出出错的代码,为开发人员扫除了后顾之忧,使其可以不断的开发新功能,此外,它仍是代码日建立的基础。

  ◆ 为测试人员节省大量的时间,使得测试人员可以将精力集中在更有效益的地方。

  此外,构成测试网还有一个额外的成本,若是开发团队不熟悉面向对象语言,那么因为接口不稳定致使的测试网的变更会增大其构建成本。

   总结   从以上的讨论能够看出,架构和代码是分不开的,架构脱离了代码就不可以称得上是一个好的架构。这是架构的目标所决定的,架构的最终目标就是成为可执行的代码,而架构则为代码提供告终构性的指导。所以,用代码来验证架构是一种有效的作法。而要实现这个作法并非一件容易的事情,咱们须要考虑代码级别的架构相关知识(咱们讨论的知识虽然局限在面向对象语言,可是在其它的语言中一样能够找到相似的思想),并利用它们为架构设计服务。