架构之美第十一章-架构结构
那么,好的架构师如何来处理这些关注点?我们曾经提到过,需要将系统组织成一些结构,每种结构都定义了特定类型的组件之间的具体关系。架构师的主要关注点就是对系统进行组织,让每种结构有助于解答一个关注点所定义的问题。关键的结构决定将产品划分为组件,并定义了这些组件之间的关系(Bass、Clements和Kazman 2003; Booch、Rumbaugh和Jacobson 1999; IEEE 2000; Garlan和Perry 1995)。对于任何产品,都有许多结构需要设计。每种结构都必须单独设计,这样它就表现为一个独立的关注点。在接下来的几节中我们会讨论一些结构,你可以利用它们来考虑前面列表中的关注点。例如,“信息隐藏结构”展示了如何将系统组织成一些工作指派。这种结构也可以用作改变的路线图,展示了建议的改变,以及哪些模块支持这些改变。针对每种结构,我们描述了一些组件及其之间的关系,正是这些组件和关系确定了这种结构。对照前面的列表,我们认为下面的结构是最重要的。
1.3.1 信息隐藏结构
组件与关系:主要组件是一些“信息隐藏模块”,每个模块都是针对一组开发人员的工作指派,每个模块都包含了一种设计决定。如果一项决定可以改变,同时又不影响任何其他模块,我们就说这项设计决定是一个模块的秘密(Hoffman和Weiss 2000,第7章和
第16章)。模块间最基本的关系是“整体-部分”关系。如果“信息隐藏模块A”的秘密是“信息隐藏模块B”的秘密的一部分,那么A就是B的一部分。请注意,必须能够在改变A的秘密的同时,不改变B的其他部分。否则,根据我们的定义,A就不是B的一个子
模块。例如,许多架构都有一些虚拟设备模块,它们的秘密是如何与特定的物理设备通信。如果虚拟设备分成不同类型,那么每种类型可能构成该虚拟设备模块的一个子模块,其中每种虚拟设备类型的秘密将是如何与这种类型的设备进行通信。
每个模块都是一份工作指派,包含了一组要写的程序。根据不同的语言、平台、环境,“程序”可以是能在计算机上执行的方法、过程、函数、子程序、脚本、宏或其他指令序列。第二种信息隐藏模块结构是基于程序和模块之间的“包含”关系。如果模块M的一部分工作指派是要编写程序P,那么M就包含P。请注意,每个程序都包含在一个模块中,因为每个程序必然是某些开发人员的工作指派的一部分。
这些程序中的一些可以通过模块的接口来访问,而另一些则是内部的。模块也可能通过接口发生关系。A模块的接口是一组假定,这些假定包括该模块之外的程序可以对该模块做出的假定,也包括该模块中的程序对其他模块的程序和数据结构所做的假定。如果改变B的接口就要求A也发生改变,那么我们就说A“依赖”B的接口。“整体-部分”结构是层次状的。在这个层次结构的叶节点上的模块不包含可识别的子模块。“包含”结构也是层次状的,因为每个程序都只包含在一个模块之中。“依赖”关系不一定是层次状的,因为两个模块可能互相依赖,要么是直接互相依赖,要么是通过一个较长的“依赖”关系形成的环。请注意,“依赖”不应该与后面小节中定义的“使用”混淆起来。
信息隐藏结构是面向对象设计方法的基础。如果一个信息隐藏模块设计为一个类,这个类的公有方法就属于该模块的接口。
满足的关注点:信息隐藏结构的设计应该能满足可变性、模块化和可构建性的要求。
1.3.2 使用结构
组件与关系:根据前面我们的定义,信息隐藏模块包含一个或多个程序(在上一小节中定义)。当且仅当两个程序共享一个秘密时,它们才属于同一个模块。“使用结构”(Uses Structure)的组件是一些可以单独调用的程序。请注意,程序可以相互调用,或被硬件调用(例如,被一个中断例程调用),调用也可能来自于不同命名空间的程序,如操作系统例程或远程过程。而且,调用发生的时间可以是任何时候,从编译时到运行时。只有在相同绑定时间操作的程序之间,我们才考虑形成一种使用结构。首先只考虑运行时操作的程序可能最容易。以后,我们也可以考虑那些编译时或载入时操作的程序之间的使用关系。如果程序B必须存在并满足其规格说明,程序A才能满足其规格说明,我们就说A使用了B。换言之,B必须存在且操作正常,A才能操作正常。使用关系有时候也称为“要求存在正确的版本”。进一步的解释和例子,参见(Hoffman和Weiss
2000)的第14章。
使用结构确定了我们可以构建并测试怎样的工作子集。在软件系统的使用结构中,期望的属性是它定义了一种层次结构,这意味着其中不出现环。如果在使用关系中出现环,那么环中所有程序都必须存在且正常工作,才能让其他的程序正常工作。由于也许不能够创建完全没有环的使用关系,架构师可能将使用环中的所有程序作为单一的程序,以这种方法来创建子集。子集必须要么包含全部程序,要么都不包含。如果在使用关系中没有环,软件采用的就是一种层次结构。在最底层,即第0层,是所有不使用其他程序的程序。第n层包含了所有的程序,它们使用了第n-1层或以下层的
程序。这些层常常描绘为一系列的层次,每个层次表示了使用关系中的一个或几个层。在使用结构中对相邻的层分组,有助于简化表示,并允许在关系中出现小环的情况。进行这种分组有一个指导原则,即一个层次中的程序应该比它上一个层次中的程序执行速度快9倍,执行频率高9倍(Courtois 1977)。
具有层次使用结构的系统可以同时构造一层或几层。这些层次有时候称为“抽象层”,但这是一种错误的名称。因为这些组件是独立的程序,而不是完整的模块,它们不一定抽象(隐藏)了什么东西。
通常大型的软件系统包含太多的程序,这让程序间使用关系的描述不太容易理解。在这种情况下,使用关系可以用于程序的组合,如模块、类或包。这样的组合描述丧失了重要的信息,但有助于展示“全局”。例如,你有时候可以在信息隐藏模块之间建立使用关系,但是除非一个模块中所有的程序都属于实际使用层次的同一层,否则就会丧失重要的信息。
在某些项目中,系统的使用关系开始并没有完全确定,要到系统实现时才能确定,因为开发者会在实现过程中决定他们使用哪些程序。但是,系统的架构师可能在设计时创建一种
“允许使用”关系,约束开发者的选择。今后,我们不会区分“使用”和“允许使用”。定义良好的使用结构将创建系统的适当子集,可以用于驱动迭代式或增量式的开发循环。满足的关注点:产品化和生态系统。
1.3.3 进程结构
组件与关系:信息隐藏模块结构和使用结构是静态的结构,存在于设计时和编码时。我们现在转向运行时结构。参与进程结构的组件是进程。进程是运行时的事件序列,由程序控制(Dijkstra 1968)。每个程序都作为一个或多个进程的一部分执行。一个进程中的事件序列的执行独立于另一进程中的事件序列,除非这两个进程彼此同步,例如一个进程等待来自另一个进程的信号或消息。进程由支持系统分配资源,包括内存和处理器时间。系统可能包含固定数量的进程,也可能在运行时创建和销毁进程。请注意,在Linux和Windows操作系统中实现的线程也符合这个进程定义。进程是几种不同关系中的组件。下面是一些例子。
进程提供工作
一个进程可能会创建工作,该项工作必须由其他进程完成。这种结构在确定系统是否死锁时是很重要的。
满足的关注点:性能和容量。
进程取得资源
在动态分配资源的系统中,一个进程可能控制由另一个进程使用的资源,后者必须请求并归还这些资源。因为发起请求的进程可能从几个控制器那里请求资源,所以每项资源可能都有一个不同的控制进程。
满足的关注点:性能和容量。
进程共享资源
两个进程可能共享资源,如打印机、内存或端口等。如果两个进程共享一项资源,就需要通过同步来防止使用冲突。每一种资源可能有不同的关系。
满足的关注点:性能和容量。
进程包含在模块中
每个进程由一个程序控制,正如前面提到的,每个程序包含在一个模块之中。因此,我们可以认为进程包含在模块之中。
满足的关注点:性能和容量。
1.3.4 访问结构
系统中的数据可能划分成具有属性的段,如果程序对段中的任何数据拥有访问权,就对该段中的所有数据拥有了访问权。请注意,为了简化描述,我们应该让段的规模最大化,具体做法是添加一个条件,即如果两个段被同一组程序访问,这两个段就应该合并。数据访问结构包含两种类型的组件:程序和段。这种关系被命名“有权访问”,它是程序和数据段之间的关系。如果这种结构让程序访问的权限最小化,并且严格执行,我们就认为系统更安全。
满足的关注点:安全性。
1.3.5 结构小结
表1-1总结了前面的软件结构,包括它们的定义和它们满足的关注点。
表1-1:结构小结
结构 | 组件 | 关系 | 关注点 |
信息隐藏 | 信息隐藏模块 | 整体-部分 | 可变性,模块化,可构建性 |
使用 | 程序 | 使用 | 产品化,生态系统 |
进程 | 进程(任务,线程) | 提供工作,提供资源,共享资源,包含在模块中 | 性能,可变性,容量 |
数据访问 | 程序和数据段 | 有权访问 | 安全性,生态系统 |
感谢光临Darren的博客