您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
将架构作为语言:一个故事
 
作者 Markus V?lter,火龙果软件 发布于:2014-10-11
   次浏览      
 

通常,架构要么是在Word文档中描述的一些软件系统中无形的、概念性的方面,要么就完全是由技术驱动的(“我们使用了一个XML架构”)。这两种方式都很糟糕:前者很难派上用场,而后者架构上的概念被技术宣传所掩盖。

什么才是好的表达?应该是随着架构的发展,演化出一门语言,让你得以从架构的角度来描述系统。根据我在多个真实项目中获得的经验,这种表达方式能够形象、无歧义地描述架构构建模块和具体系统,同时又不至于深入到技术决策的细节(技术决策应该有意识地放到另一个单独的步骤中)。

本篇论文的第一部分通过一个真实故事演示了这一思想。第二部分则总结了这一方法的关键点。

系统背景

我正与一位客户在一起,他是我负责定期咨询工作中的其中一位客户。客户决定构建一个全新的航空管理系统。航空公司使用该系统跟踪和发布不同的信息,如:飞机是否降落在指定的机场;航班是否延迟;飞机的技术状态等等。系统同时还要为Internet的在线跟踪系统以及在机场等地设置的信息监控器提供数据。无论从哪个方面来看,该系统都属于一个典型的分布式系统,系统的各个部分分别运行在不同的机器上。它有一个中央数据中心负责处理繁重的数字运算,还有其他机器分布放置在相对广阔的区域中。多年来,我的客户一直在构建类似这样的系统,现在他们计划引入新一代的系统。新系统必须能够支持15-20年时间的演进。单单从这一项需求就可以清楚地看出,他们需要对技术进行某种抽象,因为在这15-20年期间可能要经历8次技术潮流的变迁。对技术进行抽象还有另一个重要的理由,那就是系统的不同部分采用了不同的技术来构建,有Java,C++,C#。采用多种技术对于大型分布式系统而言并非特殊的需求。通常,我们会在后端使用Java技术,而在Windows前端使用.NET技术。

由于系统的分布式本质,不可能在同一时间更新系统的所有组成部分。这就产生了另一项需求,就是能够一部分一部分地更新该系统。这就反过来要求能够管理不同系统组件之间的版本冲突问题(确保组件A在组件B被升级到一个新的版本之后,仍然能够与之协作)。

起点

在我进入项目的时候,他们已经决定系统的主干应该是一个基于消息传递的基础架构(对于这类系统而言,这是一个不错的决策),并且他们评估了不同的消息传递主干在性能和吞吐量方面的表现。他们已经确定了在整个系统中使用一个业务对象模型,对系统操作的数据进行描述(对于这类系统而言,这实际上不是一个好的决策,但它不影响这个故事的结论)。

因此,当我进入项目后,他们向我简要地介绍了系统的所有细节,以及他们已经做出的架构决策,然后询问我这些决策是否正确。但是我很快就发现,虽然他们了解了很多需求,也已经在架构的某些方面做出了细致的决策,但是却没有形成我所说的一致的架构(consistent architecture):即对组成实际系统构建模块的定义,也就是定义系统中的各种事物。他们没有掌握谈论这个系统的语言。

实际上,这只是我进入项目时的一个初步印象。当然,我认为该项目存在一个巨大的问题:如果你并不知道组成系统的各种事物,就很难一致地谈论和描述该系统,当然更无法一致地构建该系统。你需要定义一门语言。

背景:这门语言是什么?

当你拥有一门语言,并能够从架构的角度谈论系统时,你就拥有了一个一致的架构1。那么语言应该是什么样的呢?显然,它首先并且至少是一套定义良好的术语。定义良好首先意味着所有的利益相关者都要认同术语的含义。如果从非正式的角度来看,术语和术语的含义可能就足以定义一门语言了。

然而——这里可能显得有些突然——我一向鼓吹的是要用一门正式语言来描述架构2。要定义一门正式语言,你需要的不仅仅是术语和术语的含义。你还需要一种语法来描述如何通过这些术语组成“语句”(或者模型),同时需要一种具体的句法去表示它们3。

使用一门正式的语言来描述你的架构,会带来许多好处,随着故事的逐渐展开,这些好处也会展露无遗。同时,在本文的末尾我会对其进行总结。

发展出一门语言以描述架构

让我们继续这个故事。我的客户与我都同意值得花上一天的时间去审阅某些技术需求,并为架构建立一门正式语言来体现这些需求。实际上,我们一边讨论整个架构,一边构建出语法、某些约束以及一个编辑器(使用oAW的Xtext工具)。

开始

我们首先从组件的概念开始。我们对组件概念的定义是相对比较宽松的。它只是与架构相关的构建模块的最小单元,封装了应用程序的功能。同时,我们假定组件是能够被实例化的,以便使架构中的组件概念对应上OO编程中的类。因此,根据我们定义的初始语法,首先构建的模型应该是这样:

component DelayCalculator {}
component InfoScreen {}
component AircraftModule {}

注意,在这里我们做了两件事情:我们首先定义了系统中存在组件的概念(使得组件成为我们要构建的系统的构建模块),其次我们还(初步)决定系统中存在三个组件DelayCalculator,InfoScreen和AircraftModule。我们为架构提出了一套构建模块,作为一个概念型的架构,并将这些构建模块的一套具体范本作为应用程序架构4。

接口

当然,上述关于组件的概念并无太大用处,因为组件无法交互。领域逻辑清晰地表明,DelayCalculator必须接收来自AircraftModules的消息,从而计算航班的延误状态,然后将结果转发给InfoScreens。我们知道,它们应该以某种方式交换信息(记住:已经作出了消息传递决策)。但是,我们决定不引入消息,而是将一组相关的消息抽象为接口5。

component DelayCalculator implements IDelayCalculator {}
component InfoScreen implements IInfoScreen {}
component AircraftModule implements IAircraftModule {}
interface IDelayCalculator {}
interface IInfoScreen {}
interface IAircraftModule {}

我们认识到,上面的代码看起来有几分像是Java代码。无需惊讶,既然我的客户具有Java背景,那么系统的首选目标语言自然就是Java。因此,我们就要从他们习惯使用的语言中,抽取出广为人知的概念衍生为我们自己的语言。然而,我们很快注意到这样的表示方式没有太大用处:我们无法表示组件“使用了某个特定的接口(与提供接口相对)”。了解一个组件需要哪些接口是很重要的,因为我们希望能够了解(而且之后要用工具进行分析)组件具有的依赖关系。这对于任何一个系统都很重要,而对于版本管理的需求而言,则尤为重要。

因此,我们对语法稍加修改,支持如下的表达形式:

component DelayCalculator {
  provides IDelayCalculator
  requires IInfoScreen
}
component InfoScreen {
  provides IInfoScreen
}
component AircraftModule {
  provides IAircraftModule
  requires IDelayCalculator
}
interface IDelayCalculator {}
interface IInfoScreen {}
interface IAircraftModule {}

描述系统

那么,我们来看看这些组件是如何被使用的。我们清晰地认识到组件需要支持实例化。很显然,系统中有许多架飞机,每架飞机都运行了一个AircraftModule组件,而InfoScreens的实例数量更多。不够明确的是我们是否需要多个DelayCalculators,但我们决定推迟对它的讨论,先处理实例化的问题。

因此,我们需要能够表示组件的实例化。

instance screen1: InfoScreen
instance screen2: InfoScreen
...

接着,我们讨论了如何把系统的各实例“接上线”:如何表示某个InfoScreen与某个DelayCalculator“交谈”?我们必须找出某种方式来表示实例之间的关系。由于这两个类型各自具有了“可兼容”的接口,因此,DelayCalculator可以与InfoScreen“交谈”。但是暂时还难以把握这种“交谈”关系。我们还注意到一个DelayCalculator实例通常会与多个InfoScreen实例“对话”。因此,我们必须以某种方式在语言中引入下标来表示实例的个数。

经过几番修改,我引入了端口(Port)的概念(实际上在组件技术以及UML中,这是一个众所周知的概念,但是相对于我的客户而言,却是一个新名词)。端口是在组件类型上定义的一个通信端点,当拥有端口的组件被实例化时,端口也会一同被实例化。因此,我们对组件描述语言进行重构,以支持如下的表示形式。端口通过provides和requires关键字进行定义,紧接着是端口的名称和下标,一个冒号以及与端口相关联的接口。

component DelayCalculator {
  provides default: IDelayCalculator
  requires screens[0..n]: IInfoScreen
}
component InfoScreen {
  provides default: IInfoScreen
}
component AircraftModule {
  provides default: IAircraftModule
  requires calculator[1]: IDelayCalculator
}

以上模型表示,任何一个DelayCalculator实例都要连接多个InfoScreens。从DelayCalculator实现代码的角度来看,通过screen端口可以访问到一组InfoScreen。而AircraftModule则只能与一个DelayCalculator“对话”,正如下标[1]所示。

新的接口标识启发了我的客户对IDelayCalculator进行了修改,因为他们注意到对于不同的通信对象,应该有不同的接口(因此还应该有不同的端口)。我们对应用程序架构作出了如下修改:

component DelayCalculator {
  provides aircraft: IAircraftStatus
  provides managementConsole: IManagementConsole
  requires screens[0..n]: IInfoScreen
}
component Manager {
  requires backend[1]: IManagementConsole
}
component InfoScreen {
  provides default: IInfoScreen
}
component AircraftModule {
  requires calculator[1]: IAircraftStatus
}

注意,端口的引入改善了应用程序架构,因为我们拥有了体现角色的接口(IAircraftStatus,IManagementConsole)。

现在,我们拥有了端口,因此我们能够命名通信端点。这就使得我们能够轻而易举地描绘出系统:互连的组件实例。注意,引入了新的结构connect。

instance dc: DelayCalculator
instance screen1: InfoScreen
instance screen2: InfoScreen
connect dc.screens to (screen1.default, screen2.default)

保持大局观

当然,从某种情况来看,为了不至于混淆所有的组件、实例和连接器(connectors),我们无疑需要引入某种命名空间的概念。自然,我们也可以将这些内容分别放到不同的文件中(工具支持保证了“转到定义”和“查找引用”仍然正常)。

namespace com.mycompany {
  namespace datacenter {
    component DelayCalculator {
      provides aircraft: IAircraftStatus
      provides managementConsole: IManagementConsole
      requires screens[0..n]: IInfoScreen
    }
    component Manager {
      requires backend[1]: IManagementConsole
    }
  }
  namespace mobile {
    component InfoScreen {
      provides default: IInfoScreen
    }
    component AircraftModule {
      requires calculator[1]: IAircraftStatus
    }
  }
}

当然,将组件和接口的定义(本质上是类型的定义)与系统的定义(连接的实例)分开,是一个很好的想法,因次,我们如下定义了一个系统:

namespace com.mycompany.test {
  system testSystem {
    instance dc: DelayCalculator
    instance screen1: InfoScreen
    instance screen2: InfoScreen
    connect dc.screens to (screen1.default, screen2.default)
  }
}

在一个真实的系统中,DelayCalculator必须能够在运行时动态地发现所有可用的InfoScreens。手动地描述这些连接是没有什么意义的。因此,我们需要继续前进。我们定义了一个查询,它可以采用naming/trader/lookup/registry的基础架构在运行时执行。每隔60秒,查询会被执行一次,查找任何上线的InfoScreens。

namespace com.mycompany.production {
  instance dc: DelayCalculator
  // InfoScreen instances are created and
  // started in other configurations
  dynamic connect dc.screens every 60 query {
    type = IInfoScreen
    status = active
  }
}

可以使用相似的办法实现负载均衡或者容错能力。一个静态的连接器能够指向一个主要实例以及备份实例。或者,在当前使用的组件实例变为不可用时,可以重新执行一个动态查询。

为了支持实例的注册,我们在它们的定义中添加了额外的语法。一个registered的实例会在注册记录中使用自己的名称(通过命名空间识别)以及所有提供的接口,自动注册其本身。还可以指定额外的参数,如下的例子就为DelayCalculator注册了一个主要的实例和一个备份的实例。

namespace com.mycompany.datacenter {
  registered instance dc1: DelayCalculator {
    registration parameters {role = primary}
  }
  registered instance dc2: DelayCalculator {
    registration parameters {role = backup}
  }
}

 

   
次浏览       
相关文章

企业架构、TOGAF与ArchiMate概览
架构师之路-如何做好业务建模?
大型网站电商网站架构案例和技术架构的示例
完整的Archimate视点指南(包括示例)
相关文档

数据中台技术架构方法论与实践
适用ArchiMate、EA 和 iSpace进行企业架构建模
Zachman企业架构框架简介
企业架构让SOA落地
相关课程

云平台与微服务架构设计
中台战略、中台建设与数字商业
亿级用户高并发、高可用系统架构
高可用分布式架构设计与实践
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...