体系结构也代表了系统宏观共性的东西,下面从建筑风格谈起。

从“建筑风格”谈起

建筑风格等同于建筑体系结构的一种可分类的模式,通过诸如外形、技术和材料等 形态上的特征加以区分。比如:
image.png
它们都有自己独特的共性特点,每一种建筑风格的产生都是人们对居住的思考和尝试,是一个长期建筑历史发展的结果。

之所以称为风格,是因为经过长时间的实践,它们已经被证明具有良好的工艺可行性、性能和实用性,并且可以直接用来遵循和模仿

软件体系结构风格

对于软件系统来说,大部分的软件设计是例行设计,即有经验的软件开发人员经常会借鉴现有的解决方案,然后按照新系统的要求,将其改造成新的设计。
image.png

软件体系结构风格(Architectural Styles)是描述特定系统组织方式的一些惯用范例,它反映的是众多系统所共有的一些结构和语义。使用软件体系结构风格可以促进设计的重用和大量代码的重用,使系统更加易于理解,而且标准化的风格也有利于系统的互操作。
image.png

常见的体系结构风格

image.png
卡内其梅龙大学的 Garland 和 Shaw 教授是软件体系结构最早的研究者,他们认为体系结构风格是按照结构组织的模式来定义系统,具体地说,就是定义了构件、连接件的语法和连接方法,同时他们也提出五种通用的体系结构风格:
image.png

  • 第一种是独立构件风格,典型的系统有进程通信和事件系统等。其中事件系统又可以进一步地分成隐式调用和显式调用。
  • 第二种是数据流风格,主要包括批处理和管道过滤器这样一些类型。
  • 第三种是是以数据为中心的风格,像常见的仓库、数据库系统、超文本系统等都属于这种风格。
  • 第四种是虚拟机风格,主要包括解释器、基于规则的系统。
  • 第五种是调用/返回风格,像主程序-子程序、面向对象、层次结构等都属于这种风格。

主程序-子程序风格

主程序-子程序风格是结构化程序设计的一种典型风格,从功能设计出发,把系统进行模块化的分解,由主程序调用那些所分解的子程序模块,来实现完整的系统功能。
image.png
这种风格的系统是由一个主程序和一系列子程序组成,主程序和子程序以及子程序之间通过调用和返回来形成一个层次化的系统结构,C语言编程的程序就是典型的主程序-子程序风格。

面向对象风格

image.png
面向对象风格建立在数据抽象和面向对象的基础之上,它把数据和它相应的操作都封装在一个抽象数据类型或者对象中,整个系统可以看成是一个对象的集合,每一个对象都有自己的功能集合。这种风格的系统它的构件是类和对象,对象之间通过函数调用和消息传递来进行交互,它的特点是具有信息隐藏性,构件之间只通过接口和外部进行交互,它的内部结构是被封装隐藏起来的,像 C++和 Java 程序都是典型的面向对象风格。

管道-过滤器风格

管道-过滤器风格是把系统的任务分成一系列连续的处理步骤,这些步骤通过系统的数据流进行连接,一个步骤的输出是下一个步骤的输入。

image.png
上图显示了管道过滤器风格的一个直观结构,其中的构件称为过滤器,过滤器对于输入的数据流进行内部处理,然后产生输出数据流,而传送数据的连接件就称为管道。

下图是一个媒体播放器的例子:
image.png
它的输入是一个 AVI 文件,包括了视频和音频数据,这个文件经过分离器进行处理,把视频和音频数据分离成两个流,其中视频数据传送给视频解码器,音频数据传送给音频解码器,视频解码器和音频解码器分别把压缩的视频和音频数据解码成原始的图象数据和原始的音频数据,然后再分别输出给显示器和声卡。

在管道过滤器风格中,构件具有良好的隐藏性和高内聚低耦合的特点,可以很好地支特软件的重用和扩展,但是这种结构不适合交互应用的情况,如果管道过长或者过滤器过于复杂的话,系统的性能就会大大降低。

以数据为中心的风格

我们经常使用剪贴板,这个程序可以把剪贴的数据进行短时间的存储,然后支持文档和应用之间的数据传递和交换,那么剪贴板和其他应用程序之间应该是一个什么样的结构呢?
image.png
剪贴板需要一个公共区域来存储交换的信息,这个区域就像是一个共享的数据仓库,不同的应用程序通过Copy/Paste来访问剪贴板,并且使用这个区域来交换格式化的信息,这是一种典型的仓库体系结构。

仓库体系结构(Repository Architecture)是一种以数据为中心的体系结构, 适合于数据由一个模块产生而由其他模块使用的情形。
image.png
在这种系统中,所有的功能模块都访问和修改一个单一的数据存储,这存储也被称为是一个集中式的仓库,而功能模块之间是相互独立的,他们之间的交互都是通过这个仓库来完成。这种体系结构适合于实现那些经常发生改变而且具有复杂数据处理的任务,只要仓库的定义良好,就可以很方便地增添功能模块,从而实现向系统添加新的服务。但是这种系统的主要问题在于每个功能模块和仓库之间的耦合非常高,集中式的仓库很有可能成为系统性能的瓶颈。

示例1:程序设计语言编译器
image.png
这是一个现代程序设计语言编译器的系统结构图,它采用的就是一种仓库形式的体系结构风格,从图中可以看出编译器、调试器和编辑器都是单独的工具,编译器增量的产生程序的语法分析数和符号表,代码调试器和编辑器对它们进行读取和使用。

示例2:基于数据库的系统结构
image.png
一般而言,数据库系统也是典型的仓库体系结构风格,主要包括两种构件,一种是中心数据库,保存当前系统的数据状态,另一种是多个独立的应用,对数据库进行读取。对于复杂的应用系统来说可能会涉及到多个异构数据库的集成,一个数据库可以被多个应用访问,一个应用也可以访问多个数据库,当然现在有很多异构数据库的集成技术,感兴趣的可自行了解。

层次结构

image.png
层次化已经成为一种复杂系统设计的普遍性原则,很多复杂软件的设计,从操作系统到网络系统再到一般的应用,几乎都是以层次结构来建立的,在层次结构中,系统被分成若干层次,每一层次由一系列的构件所组成,层次之间存在接口,通过接口形成调用和返回的关系,下层构件向上层构件提供服务,上层构件被看作是下层构件的客户端。

示例1:安卓操作系统层次结构
image.png
前面软件设计原则提到过,这里不再重复。

示例2:网络分层模型
image.png
↑这是一个网络分层模型,它包括物理、数据链路、网络、传输、会话、表示和应用等层次,并严格限定某一层的构件只能和同一层的对等实体,以及和它相邻的下一层进行交互。

客户机/服务器结构

客户机/服务器体系结构(Client/Server)是一种分布式系统模型,它把应用程序的处理分成了两个部分,一个是客户机,负责和用户进行交互,另一个是服务器,为客户机提供服务。

最早的分布式系统是两层的,一个是客户机,一个是数据库的服务器。后来发展成为三层结构,中间增加了一个应用服务器层。现在已经发展成为一个多层的体系结构:
image.png

两层C/S结构

两层的客户机服务器结构也称为胖客户端模型,在这种系统中,数据库服务器负责数据的管理,客户机实现应用逻辑并和用户进行交互。

具体过程如下:
image.png
用户往客户机的系统界面上输入数据,并发出业务处理请求。客户机上的业务处理程序接受请求进行处理,在需要进行数据存取的时候,向服务器发出数据存取请求,服务器将执行数据库连接、查找和存取的操作,并把结果进行返回。客户机的业务处理程序处理完用户请求之后,在表示层上进行输出。

胖客户端与瘦客户端

两层结构的主要问题在于客户端的负担过重,系统维护和升级比较困难,扩展性也不太好。与胖客户端相对应,还有一种瘦客户端,二者的区别在于业务逻辑到底在客户端多一些还是在服务器端多一些:
image.png

  • 胖客户端:客户端执行大部分的数据处理操作
  • 瘦客户端:客户端具有很少或没有业务逻辑

三层C/S结构

image.png
针对两层结构的问题,人们提出了三层客户机服务器结构,它把应用系统分成表示层、功能层和数据层三个部分,并且把这三层进行明确的分割,在逻辑上使其独立,其中:

  • 表示层:是应用的用户接口部分,包括所有与客户机交互的边界对象,如窗口、表单、 网页等,负责实现用户和应用之间的对话。
  • 功能层(业务逻辑层):包括所有的控制和实体对象,实现应用程序的处理逻辑和规则。
  • 数据层:实现对数据库的存储、 查询和更新。

image.png
在硬件部署时,三层结构的表示层可以配置在客户端,功能层和数据层分别放在不同的服器上,这样就大大降低了客户端的负荷,也减轻了系统维护和升级的成本、工作量。在增加新的业务处理时,可相应的增加装有功能层的服务器,系统的灵活性和伸缩性变得很强。尤其是随着系统规模的加大, 这种结构的优势就变得更加明显。但需要注意的是,三层结构中,各层之间的通信效率如果很低,即使分配给各层的硬件很强,系统在整体上也达不到所要求的性能,所以在设计时必须慎重考虑三层之间的通信方法、频率和数据量

B/S结构

随着互联网技术的发展,浏览器/服务器(Browser/Server)结构,也称B/S结构,逐渐流行,它是三层C/S风格的一种实现方式。
image.png
它的客户端就是刘览器,表示层负责处理客户端所需要的展现逻辑,应用层负责所有的业务逻辑,数据层负责对数据库的操作。层次架构的基本思想是把一个应用分成多个逻辑层,其中每一层都有通用或特定的角色,这样做可以使应用更易于扩展。

集群结构

image.png
在实际部署时,功能层并不一定只驻留在同一台服务器上,数据层也是这样,如果功能层或者数据层分布在多台服务器上,那么就形成了集群式的系统结构。

MVC结构

许多C/S结构的应用系统都是从数据库中检索数据,再展现给用户,在用户更改数据之后,系统再把更新的内容存储到数据库中。
image.png
具体实现的时候,数据存取逻辑是和用户界面绑在一起的,这样可以提高应用程序的性能,但是这种做法是有问题的。一般而言,用户界面的更改往往要比数据存取更加频繁,而且除了数据存取之外,应有程序的业务逻辑中往往还会包含很多其他的业务逻辑,显然用户界面一旦更改,就会影响到数据存取逻辑部分,这样是不满足高内聚低耦合的设计原则的。

那么纯粹的B/S结构能不能解决这个问题?
image.png
由于用户界面仍然需要显式地调用功能层的业务逻辑,所以还是很难避免用户界面修改导致业务逻辑修改这个问题。所以需要进一步改进这个结构,以保证各个部分可以单独修改而不影响其他部分。

首先分析一下影响 Web 系统模块化的一些因素:

  • 在基于 Web 的应用程序中,用户界面逻辑的更改往往比业务逻辑频繁;
  • 如果将UI代码和业务逻辑组合一起并放在UI中,则每次更改界面都可能引起对业务逻辑的修改;
  • 在某些情况下,应用程序以不同的方式显示同一数据,比如表格、各种图形等等;
  • 与业务逻辑相比,用户界面代码对设备的依赖性往往更大;
  • 设计美观而有效的用户界面与开发复杂业务逻辑需要不同的编程技能;
  • 通常为用户界面创建自动测试比为业务逻辑更难、更耗时;

为了解决上述问题,人们提出了模型-视图-控制器结构,也称 MVC 结构:
image.png
它是把应用程序的数据模型、业务逻辑和用户界面分别放在独立的构件中,这种结构是在传统的B/S体系结构基础上,加入了一个新的元素——控制器,由控制器来决定视图和模型之间的依赖关系,这样用户界面的修改就不会对数据模型或者业务逻辑造成太大的影响。其中模型用于管理应用系统的行为和数据,它在发生变化时会通知视图,并且允许视图查询它的当前状态,同时也允许控制器访问在模型中封装的业务功能,视图负责模型内容的展现,控制器则是定义了应用系统的控制行为、负责分派用户的请求,并且针对用户请求选择相应的视图。

下图是一个 Web 应用的执行过程:
image.png
客户端发送一个 HTTP 请求,这个请求首先会进入到控制器,然后控制器去获取数据把它封装成模型,最后再把模型传递到视图中进行展现。这个过程存在下面一些问题:

  • 每一次请求必须经过“控制器->模型->视图”过程,才能看到最终展现的界面,整个过程比较复杂
  • 视图依赖于模型,也就是说,如果没有模型,视图是无法展现最终的效果
  • 渲染视图的过程在服务端完成,呈现给浏览器的是带有模型的视图页面,系统的性能无法得到很好的优化

为了使数据展现过程更直接,而且有良好的用户体验,需要对 MVC 模式进行改进,下图是一种改进的方案:
image.png
首先从浏览器发出 AJAX 请求,然后服务端接受这个请求,返回 JSON 的数据给浏览器,最后在浏览器中进行界面的渲染。如果我们把浏览器这一端视为前端,服务器视为后端的话,改进后的 MVC 模式可以简化为前后端分离的模式,现在有一种 REST 的机制可以实现这个模式,这一部分的内容在下一章进行讲解。

事件风格

程序调试器的工作过程如下:
image.png
首先在调试器设置一个断点,编译器、文本编辑器和变量监视器通过注册的方式和断点发生关联,程序运行到断点处的时候,系统就会触发文本编辑器和变量监视器,也就是说是断点事件隐含地造成它们的调用,这时,文本编辑器滚动屏幕到断点处,变量监视器刷新变量的当前值,这是一种事件风格的系统。

image.png
事件驱动分成显式调用和隐式调用两种类型:

  • 显式调用:各个构件之间的互动是由显性调用函数或程序完成的,而且调用过程与次序也是固定的、预先设定的
  • 隐式调用:调用过程与次序不是固定的、预先未知,各构件之间通过事件的方式进行交互

在事件风格的系统中,隐式调用是一种常见的形式,系统把应用看成是一个构件的集合。
image.png
作为事件源的构件并不是直接调用其他构件,而是触发或者广播一个或多个事件,其他响应事件的构件作为事件处理器预先在事件中进行注册,当事件被触发的时候,事件管理器就会调用这些已经注册的构件进行事件的处理。

image.png
上图是程序调试器,其中,事件源是调试器,编辑器与变量监视器是事件的处理器,集成开发环境IDE是事件管理器。编辑器和变量监视器向调试器进行注册,接收断点事件,遇到断点的时候,由调试器来发布事件,进而就触发了编辑器和变量监视器。

事件风格的实现策略之一:选择广播式
image.png
实现事件处理的第一种方式,是发布订阅模型。在这个模型中有两种角色,分别是发布者和订阅者。订阅者可以订阅一个或多个频道,而发布者可以向指定的频道来发送消息,所有订阅了这个频道的订阅者就会收到这个消息,但是没有预定的程序不会收到这个消息。

事件风格的实现策略之二:观察者模式
image.png
另一种实现方式是观察者模式,观察者被注入到被观察者中进行事件的监听,随时对被观察者的变化做出反应。这种方式和发布订阅模式的不同在于,发布订阅模式是在观察者和被观察者中间增加了一层间隔,二者之间是一种松耦合的关系。

软件体系结构风格的选择

上面介绍了几种常见的体系结构风格,但是在实际应用时需要借助丰富的经验来进行判断和选择。

一般情况下,大部分的实际系统往往是几种体系结构的组合应用。在系统分析和设计过程中,首先要把整个系统作为一个功能体来进行分析和权衡,得到一个最顶层的体系结构。如果系统中的元素还是比较复杂,那就继续进行分解,再得到某一部分的局部体系结构。也就是说要把焦点集中在系统的总体结构上,避免过多地考虑实现细节。

在体系结构的选择上,需要考虑技术因素和质量因素两个方面:

  • 技术因素:
    • 使用什么样的构件和连接件
    • 运行时,构件之间的控制机制是如何被共享、分配和转移
    • 数据如何通讯
    • 数据和控制是如何交互
  • 质量因素:
    • 可修改性:要考虑到算法、数据表示和系统功能的变化和可扩展性
    • 性能
    • 可复用性

根据实际的开发经验,可以总结一些体系结构的选择原则:

  • 一般层次化在任何系统都是要使用的
  • 如果系统的功能可以分解成一系列连续的处理步骤,那么就可以考虑批处理或者管道-过滤器风格
  • 如果系统的核心问题是数据管理,而且数据是持续存储的,那么可以使用仓库结构风格
  • 如果任务之间的控制流是可以预先设定的,可以考虑主程序-子程序,或者面向对象的风格
  • 对于任务需要高度灵活可配置或者任务是被动的,就可以考虑事件系统或者客户机/服务器的结构
  • 如果设计了一种计算,但是没有机器可以支持它的运行,可以考虑使用虚拟机或者解释器的体系结构

最后,软件体系结构的选择和应用需要经验,也需要创新。