第六章经典软件体系结构第六章经典软件体系结构本章介绍经典的软件体系结构,主要包括Mary Shaw等人所总结的一些软件体系结构分格。
众所周知,计算机学科历史较短,因此,软件架构的研究历史更短。
本章所谓的经典软件体系结构也仅仅是指在20世纪80年代至21世纪初期间提出的成功的软件体系结构。
本章将介绍调用-返回分格软件体系结构、数据流风格软件体系结构、基于事件的软件体系结构、层次软件体系架构与MVC 软件体系结构。
6.1 调用-返回风格软件体系结构相对于一些基础学科,计算机学科的发展历史较短。
20世纪50年代才出现了适合商业应用的软件开发语言。
软件设计与开发也经历了从初级到高级的历程。
回顾历史,从20世纪50年代到60年代末,属于非结构化简单程序开发阶段,软件设计方面采用简单程序模型,典型的开发语言Algol(1958)、COBOL(1959)和Basic(1964);从20世纪70年代初到80年代初,属于结构化设计编程阶段,典型的开发语言有Pascal(1968)、C(1972)、FORTRAN和Perl。
虽然从20世纪60年代开始就有了面向对象概念的讨论,但是直到20世纪80年代初正式开始才面向对象设计编程阶段,就有了代表性的面向对象软件开发语言包括SmallTalk、Ada、C++(1983)和Java(1995)。
本节首先简要回顾非结构化编程,然后介绍对软件设计有重要的影响的调用-返回风格软件体系结构,重点介绍其中的主程序 - 子程序软件体系结构与面向对象软件体系结构,并对它们进行讨论。
6.1.1 非结构化编程简介非结构化编程技术是历史上最早的编程范型。
紧随其后的是结构化设计/编程技术与面向对象设计/编程技术。
一个由非结构化语言编写的程序通常包含一系列有序的命令,或者称为语句,通常是每个语句占一行,每行都有标记一个行号或者可能是标签。
这样做的目的事允许程序执行流可以依据行号从一行跳到程序的其他指定的行。
支持非结构化编程的语言包括早期的BASIC与COBOL。
非结构化编程引入基本的循环、分支与跳跃等控制流的概念。
在非结构化编程中,虽然出现了较为原始的子程序的概念,但其与结构化编程中过程(Procedure)的概念有本质的区别。
非结构化编程中的子程序允许有多个入口和多个出口,理论上允许程序在任何地方转入子程序或退出子程序,而在这结构化编程中是不允许的。
虽然非结构化编程所编写的程序很难理解,并且经常遭到批评,但是早期的银行与金融机构软件代码都是由非结构化编程语言(例如COBOL)写的。
目前,非结构化编程已成为历史,现在很少使用(除了有些银行与金融业企业仍然使用COBOL进行开发,但和早期的版本相比有了很多改善,例如COBOL 97包括常规改善并且具备了面向对象的特征),因此本节将不把注意力放在非结构化编程方面,而主要介绍采用结构化设计方法的主程序-子程序软件体系结构与采用面向对象设计方法的面向对象软件体系结构。
在介绍这两种软件体系结构以前,首先介绍调用-返回风格的体系结构的概念。
6.1.2 调用-返回风格软件体系结构的概念调用-返回风格体系结构(Call and Return Architecture)是在过去的30年内具有深远影响的软件体系结构。
本节要介绍的共享数据的主程序-子程序体系结构与面向对象体系结构可以被认为是调用-返回风格体系结构的子类型。
因此,熟悉调用-返回风格架构对于理解这两个体系结构有重大意义。
利用调用 - 返回风格软件体系结构设计的软件系统使用的是分而治之策略,其主要的思想是将一个复杂的大系统分解为一些子系统,以便降低复杂度,并且增加可修改性。
这种系统的程序执行顺序通常只由一个单线程控制。
例如,一个软件架构C,只有从另外一个调用者软件构件B得到控制以后,才能执行该构件C的程序;当运行结束以后,将控制返回给软件构件B。
该程序的运行控制流如图6.1所示。
图中的长箭头可以理解为是一个程序构件对另外一个构件的调用,而短箭头为程序运行控制流的走向,这也就是名称“调用-返回”的由来。
图6.1 调用-返回风格软件体系结构大的程序运行控制流示意图每个软件构件都设计为有一个唯一的程序执行起点和唯一的程序执行终点,或者称为程序执行入口和程序执行出口。
程序从其执行入口开始执行该构件的代码,运行至该构件的程序执行的出口,然后程序执行结束,而将控制返回给程序调用构件。
上述程序构件通常叫做子程序(Subroutine),从一个构件到另外一个构件的控制传递叫做子程序调用(Call)。
子程序调用程序都有参数传递。
拥有整个软件入口的构件叫做主程序(Main Program),它控制子程序的执行顺序。
在这种风格的系统中,也可能有一些被动的数据构件,其中存储供子程序的访问的共享数据。
可以被所有软件构件访问的共享数据被称为全局变量。
这种调用-返回风格体系结构的组织与执行的动态控制可以由高级程序语言的过程(Procedure)、函数(Function)或者方法(Method)来实现。
调用-返回风格架构可以被组织成任何形式,例如,可以将程序设计为如图6.2所示的结构图。
在该图中,箭头代表调用,方块内有“S ub”字样的是子程序,有“Global Date”字样的为数据构件。
图6.2设计的逻辑关系比较复杂,因此,通常人们往往喜欢将其组织成如图6.3所示的层次结构。
这样组织成层次结构的调用-返回风格架构设计叫做共享数据的主程序-子程序软件体系架构,其原因是在调用-返回体系结构中包括一个控制其他组件的主程序,该程序选择调用在其下层的软件组件(模块、函数),而该软件组件又往往调用其下层的软件组件。
每次调用都执行一个子任务,并且返回一个值。
主程序-子程序体系结构是较早出现的软件体系结构。
以上介绍的调用-返回风格软件体系结构对于理解当今流行软件体系结构有着重要的意义。
下面将分别介绍该软件体系结构下的主程序-子程序软件体系结构与面向对象软件体系结构。
6.1.3 主程序-子程序软件体系结构主程序-子程序软件体系结构在设计上使用层次化的划分方法,该体系结构中使用由编程语言直接支持的单一的控制线程。
子程序的结构是明确的,子程序通常组成程序模块。
子程序的调用呈现层次状,其正确与否往往取决于其调用的子程序的正确与否。
主程序-子程序软件体系结构图如吐6.3所示。
图6.3 共享数据的在主程序-子程序软件体系结构和非结构编程比较,主程序-子程序软件体系结构能够较好地支持系统的可改变性及可伸缩性等性能。
由于该体系结构采用了分而治之策略,将一个复杂问题分解为多个独立的子问题,从而实现了对系统分复杂度的有效控制。
C 语言开发程度即属于典型的主程序-子程序风格的软件体系结构。
C 语言开发的程序通常包括一个main 函数(即主程序)和一些自定义的函数(即子程序)。
主程序-子程序体系结构中典型地存在一个控制线程,每个阶层的组件从其父组件得到该控制并且通过它传给它的子组件。
使用主程序-子程序软件体系结构的设计通常采用如图6.4所示的自顶向下的功能化设计方法。
图6.4 自顶向下的的功能化设计方法自顶向下功能化设计方法的设计思想是,系统从功能的角度进行设计,从高层开始,逐步细化为详细的设计。
该设计从系统要完成的功能需求出发,首先将一个整体问题(功能)分为几个子问题(子功能),然后再考虑将每个子问题(子功能)再次划分为几个更小的子问题(子功能),依次下去,直到不可再分为止。
通过这样的设计,得到一颗如图6.4所示的根朝上的树形结构,其中,每个节点都是一个子程序。
可以理解为系统的功能可以由多个子程序完成。
高层子程序调用低层子程序,而低层主程序又调用比它更低的子程序,等等。
结构化程序设计得到了广泛的应用,例如阿波罗11号的地面控制系统的子系统Apollo Reentry Runge–Kutta数值集成器软件采用结构化程序设计,利用FORTRAN语言开发,在当时最快的计算机IBM 360 -75上运行。
结构化设计从数据流图开始,然后将数据流图转换为程序结构图(Structured Chart)。
设计数据流图从数据输入开始,对数据的各个处理过程以及最后的输出进行描述;将提供给用户的业务流程图(”物理模型”)进行功能建模,转化成开发人员更易理解的一系列”逻辑模型”图,以图形化的方法描绘数据在系统中的流动和处理过程。
这些图需要用规范的DFD描述,达到系统在建立前有关信息就能被充分理解的目的。
在设计时要求严格划分开发阶段,用规范的方法与图表工具精确的描述各阶段的工作,每个阶段都以规范是哪个的文档资料作为其成果,最终得到满足用户需要的系统。
相对于非结构化的设计,结构化设计有以下优点。
(1)逻辑设计与物理设计分开。
(2)开发过程中形成一套规范的文档,以便后期修改和维护。
结构化设计的缺点为开发周期长,系统难以适应环境的变化以及开发过程复杂繁琐。
结构化设计适用于组织相对稳定、业务处理过程规范、需求明确且在一定时期内不坏发生大的变化的大型复杂系统的开发,经验表明,当程序小于10万行时,结构化的程序设计比较成功。
当程序大于10万行时,结构化程序表现不加,原因是当程序规模非常大时,程序代码编写耗时长,软件测试变得越来越困难,难以保证软件的可靠性。
自顶向下的程序设计方法存在如下问题。
(1)功能烟花困难。
自上而下的方法穿件了符合最初的需求的较好的软件系统模型。
但是当系统改变或者增加新的需求是,功能结构变得越来越笨拙。
由于软件被设计为树形结构,修改与更新,通常要求广泛的“剪枝”与“嫁叶”,使系统维护变得越来越困难。
(2)现实中的系统功能不容易描述。
大型加护系统很难从功能方面进行刻画。
很多大型系统没有“顶部”,例如,一个涉及数据查询、数据改变与保持数据一致性的软件系统,如果按照功能化自向下的设计,则该系统可能被设计为一个基于一个唯一的“虚拟”顶点并且会产生非常复杂的结构。
(3)功能化设计丢掉了数据与数据结构。
自顶向下设计中捕捉不到所设计软件涉及的数据,通常,同样的数据被多个函数共享(如更新、删除、插入和询问数据库表)。
因为系统分解只突出问题的功能方面,数据结构对问题的影响被丢失了。
(4)由于功能设计得到的软件产品产生的可复用代码较少。
自顶向下的设计连续不断的将系统分为越来越简单的程序模块。
每个程序块被单独地分析确认,没有太多地考虑系统的其他部分。
但是每个程序单元的设计都仅仅考虑极为有限的需求,因为这些特定的需求不太可能出现于下个问题,所以产生的设计与代码没有普遍性与通用性,从而通常不可复用。
6.1.4 面向对象的软件体系结构在结构化设计以及编程中,简单的程序可以由一个主程序中的一系列语句组成的一个较长的文件代表,而在负载的程序中,通过子程序或者函数执行一些特定的任务。