面向服务概述

来源:互联网 发布:mysql数据库命令大全 编辑:程序博客网 时间:2024/06/05 19:00
 

Programming WCF Services附录A译稿

本书全面介绍了使用WCF设计与开发面向服务应用程序的相关知识。附录A则展示了我对面向服务的理解,以及面向服务的具体应用场景。但是,如果要了解面向服务的发展方向以及它在软件行业所占的地位,首先就要了解它的起源与发展,因为没有任何一种新的方法学是一蹴而就的,而应该是经历了数十年渐进的演化。在简要地介绍了软件工程的发展历程以及发展趋势之后,附录给出了面向服务应用程序的定义(不仅仅是指纯粹的架构),阐释了服务的本质,以及这种方法学的价值。接着,附录还给出了面向服务的原则,其中增加的抽象原则对于大多数应用程序而言,具有更强的实践意义与具体应用。

软件工程简史

二十世纪二十年代末期,世界上第一台现代计算机诞生于波兰,这是一台电子机械,大约与打字机一般大小,主要用于消息的加密。后来,这台设备被卖给了德国商业部。30年代,它被德国军方用来实现通信加密,也就是闻名遐迩的“英格玛(Enigma)”。Enigma使用机械转子(Mechanical Rotors)根据不同的密码字母改变从键到灯板的电流路径。Enigma并非通常意义上的计算机:它只能实现加密(Enciphering)与解密(Deciphering)(现在,我们将它们称之为Encryption与Decryption)。如果操作者要改变加密算法,必须通过改变转子的顺序、初始位置,以及连接键盘到灯板的线缆,改变机器的机械结构。这就导致“程序”与它要解决的问题(加密)紧密地耦合在一起,是采用机械设计的计算机。

在上个世纪四十年代末期到五十年代期间,终于诞生了世界上第一台真正意义上的电子计算机,它主要用于国防。这些机器能够运行代码解决问题,但无法执行预先制定的任务。这类计算机执行的代码存在的硬伤,则在于“语言”是面向机器的,因而程序完全依赖于硬件。针对一台机器编写的代码无法运行在另外一台机器上。最初,这个缺陷并没有引起足够的重视,因为在当世只有屈指可数的几台计算机。随着计算机的大量生产,在六十年代初出现了汇编语言,它使得代码能够不依赖于特定的机器,可以运行在多种机器上。但是,代码却与机器的体系架构密切相关。针对8位机编写的代码不能运行在16位机上,更不用说寄存器或内存以及内存地址之前的区别。因此,维护程序的代价开始逐步增加。随着计算机被广泛的应用在民用以及政府部门,为满足有限的资源与预算,需要提供更好的解决方案。

六十年代,诸如COBOL和FORTRAN等高级语言引入了编译器的概念。开发者可以在抽象层面上编写机器编程语言,编译器能够将它编译为实际的汇编代码。编译器开启了将代码与硬件以及硬件架构解耦的先河。第一代语言存在的问题是代码是非结构化编程的,代码内部通过使用跳转指令或go-to语句依赖于它自身的结构。即使代码结构发生微小的改变,也可能对程序的多个地方产生灾难性的影响。

在七十年代,结构化编程语言例如C和Pascal占据了统治地位。它通过函数与结构,完全解除了代码与内部地址及内部结构的依赖。正是在七十年代,开发者与研究者首次开始将软件作为工程科学进行研究。在所属权利益的驱动下,许多公司开始考虑软件的重用,即代码段能够重用在不同的程序上下文中。例如C语言,基本的重用单元就是函数。基于函数重用的问题是函数依赖于它操作的数据,如果数据是全局的,在重用上下文中改变一个函数,就会影响不同地方使用的其它函数。

面向对象

上述问题的解决方案是引入面向对象,例如Smalltalk,以及之后产生的C++。面向对象语言将函数和函数操作的数据包裹在一起,放到一个对象中。函数(现在则称为方法)封装逻辑,对象则封装数据。面向对象通过类层级的形式以支持领域建模(Domain Modeling)。重用机制是基于类的,允许直接重用,或者通过继承(Inheritance)进行特化(Specialization)。但是,面向对象仍然存在自身的问题。首先,生成的应用程序(或伪代码)是单一的应用程序。类似C++的编程语言并不能识别二进制形式的生成代码。即使只是针对细微的修改,开发者每一次都必须重新部署大量的代码。这对开发过程、质量、发布时间以及成本都会产生负面影响。由于类作为重用的基本单元,这些单元会在源代码中被定义为类的格式。因此,应用程序会依赖于它使用的语言。我们不能让一个Smalltalk编写的客户端去调用或继承C++的类。而且,继承实际上是一种糟糕的重用机制,大多数情况下,它都是弊大于利,因为派生类与基类的实现密切相关,从而在类层级中引入了垂直的依赖关系。面向对象忽略了许多现实问题,例如部署与版本控制。序列化与持久化则是存在的另一个问题。大多数应用程序都无法凭空获取对象。对象包含了某些持久化状态,这些状态需要组合为对象,但却无法保证持久状态与可能的新的对象代码的兼容性。如果对象被跨进程或跨机器分发,就无法使用C++的调用方式,因为C++需要直接内存引用,并不支持分布式调用。开发者必须编写宿主进程,使用一些远程调用技术例如TCP套接字执行远程调用,但这样的调用迥异于通常的C++调用方式,从而抵消了C++语言的优势。

面向组件

随着时间的推移,相继产生了一些新的技术,例如静态库(.lib)与动态库(.dll),它们能够解决面向对象存在的问题。终于,在1994年人们首次提出面向组件技术,称为COM(组件对象模型)。面向组件提供了可交换的、可互操作的二进制组件。与共享源代码文件不同,客户端与服务器都支持二进制类型系统(例如IDL),以元数据的表示方式放入到封装的二进制组件中。组件在运行时被发现以及装载,例如拖动一个控件到窗体上,则该控件会在客户端机器的运行时自动被装载。客户端程序仅仅是服务的抽象与契约,称为接口。只要接口不变,服务就能够任意扩展。代理能够实现相同的接口,通过为远程调用封装底层机制实现无缝的远程调用。公共二进制类型系统的可用性使得跨语言的互操作性成为可能,这样,Visual Basic的客户端就能够调用C++的COM组件。重用的基本单元是接口,而不是组件,多态的实现是可互换的。通过为每个接口、COM对象以及类型库分配唯一的标识符可以解决版本冲突的问题。然而,作为现代软件工程学的一个根本性突破,COM在大多数开发者的眼中却如鸡肋一般,食之无味,弃之可惜。COM未必是丑陋的实现,因为它能够与操作系统顶层结合在一起,而操作系统却不用考虑COM的实现。编写COM组件所使用的最佳语言(例如C++和Visual Baic)是面向对象的,而不是面向组件的。因为面向组件语言的编程模型过于复杂,需要框架(如ATL)来消除两种模型之间的鸿沟。正是认识到这一问题,微软于2002年发布了.NET 1.0。.NET对比COM、C++以及Windows,不仅更加简洁,而且还能够无缝地与单独的、新的面向组件运行时集成。.NET支持COM的所有优势,并实现了许多技术要素,例如类型元数据共享、序列化以及版本的统一与标准化。.NET具有更强的功能与COM协作,但COM与.NET又都存在相似的问题:

技术与平台

应用程序与代码依赖于技术与平台。COM与.NET都只能应用于Windows平台。它们要求客户端以及服务也应该是COM或者.NET,而无法支持与其它技术的互操作,不管它们是在Windows平台下,还是其它操作系统。虽然利用Web服务使得技术之间的互操作成为可能,但它却要求开发者放弃使用本地框架进行实现的大部分优势,从而引入了复杂性。

并发管理

当开发商(Vendor)发布一个组件时,并不能假定该组件不会被它的客户端多线程的并发访问。事实上,唯一安全的假设就是开发商要求组件支持多线程访问。因此,组件必须是线程安全的,同时必须包含一个同步锁。如果应用程序的开发者在构建应用程序时,聚集了多个开发商开发的多个组件,则多个锁的引入就会使得应用程序易于死锁。必须避免死锁与应用程序和组件之间的依赖。

事务

如果应用程序希望组件只参与到一个单独的事务中,则需要运行组件的应用程序协调事务以及组件之间的事务流,这是一个严格的编程要求。它同样会引入应用程序与组件之间关于事务协调的耦合,

通信协议

如果组件被跨进程或跨机器边界部署,则组件将依赖于远程调用、传输协议以及编程模型要素(例如可靠性与安全性)的实现细节。

通信模式

组件可以被同步或异步调用,也能够联机或断开调用。一个组件能够以上述的各种方式调用,但应用程序必须知道准确的选项。

版本控制

在编写应用程序时,可以使用一个版本的组件,而在发布产品时使用另一个版本的组件。处理版本冲突的问题会导致应用程序依赖于它所使用的组件。

安全

组件需要对它们的调用者进行验证与授权。那么,组件如何才能知道它所使用的安全权限,以及用户所对应的角色?不仅如此,组件还需要确保来自客户端的通信是安全的,而对客户端施加确定的限制会反过来增加组件与组件安全之间的耦合度。

COM与.NET都试图采用一些技术解决上述提及的部分(不是全部)问题,例如COM+以及企业服务(相似的,Java使用J2EE),但事实上,这些应用程序都被淹没在大量的公共基础功能实现中。在正常规模的应用程序中,大量的工作、开发以及调试时间都花费在实现这样的公共基础功能上,而不是关注业务逻辑与特性。事情更加糟糕的是,终端用户(或者开发经理)很少去关注这些公共基础功能(与业务特性相对),而开发者却没有足够的时间去开发健壮的公共基础功能。而且,大多数公共基础功能的解决方案都是专有的(这意味着无法重用、迁移与租赁)、低劣的,因为大部分开发者都不是安全专家,也不是同步处理专家,开发者也没有时间与资源专门开发这些公共基础功能。

面向服务

如果我们仔细阅读了刚才概述的软件工程发展简史,就会注意到这样一个模式:每个新的方法学与技术都会融合前一代技术的优点,并致力于改善前一代技术的缺陷。然而,每个新产生的技术又会面临新的挑战。我这里所谓的现代软件工程,就是对过去技术的去芜存菁,降低耦合程度。

不同的是,耦合虽然糟糕,它却是不可避免的。一个绝对解耦的应用程序毫无用处,因为它不具有任何价值。开发者只能通过耦合其它内容,才能为系统添加职责。编写代码的行为就是将两个内容关联起来。真正的问题是耦合的范围究竟有多宽。我相信世上只存在两种类型的耦合。好的耦合仅限于业务层的耦合。开发者通过实现系统用例或特性,将软件的功能结合起来,完成对职责的添加。坏的耦合则将所有的内容都集成在一起。.NET与COM存在的问题,不是概念上的错误,而是基于开发者必须编写大量的公共基础功能这一事实。

正是认识到过去的问题,在2000年末,面向服务方法学作为应对面向对象以及面向组件缺陷的解决方案呈现在人们眼前。在面向服务的应用程序中,开发者只需要关注于业务逻辑的编写,以及通过可交换的、可互操作的服务终结点暴露业务逻辑。客户端调用这些终结点,而不是服务代码或者它的实现包。客户端与服务终结点的交互基于标准的消息交换,服务发布各种标准元数据,描述服务的功能,以及客户端调用服务操作的方式。元数据就是服务,相当于C++的头文件,COM的类型库,或者.NET程序集的元数据。服务的终结点是可重用的,在交互的约束(例如同步、事务以及安全通信)下,服务是与客户端兼容的,而与客户端的实现技术无关。

从多个角度看,服务都是组件的一个本质上的飞跃,就像组件是对象的一个本质上的飞跃一样。在软件行业中,面向服务是我们目前所知的构建可维护的、健壮的以及安全的应用程序的最佳方案,也是最可行的方案。

在开发面向服务应用程序时,我们能够实现服务代码与客户端使用的技术与平台的解耦,也与并发管理、事务传播和管理以及通信可靠性、协议和模式无关。总的来讲,实现从客户端到服务的消息传递的安全,就是对调用者的认证,它属于服务范围之外。服务根据需求仍然要实现服务自身的本地授权。在大多数情况下,客户端并不知道服务的版本:只要终结点支持客户端期望访问的契约,客户端就不用考虑服务的版本。为了处理客户端与服务之间传递数据的版本兼容,面向服务同时还构建了版本兼容的标准。

面向服务的价值

由于客户端与服务之间的交互是基于行业标准的,这个行业标准包括了保障调用安全的方式、传播事务流的方式以及管理可靠性的方式等等。我们也可以使用现有的这些公共基础功能的实现。这就保证了应用程序的可维护性,因为应用程序与准确性无关。即使公共基础功能发生演化,应用程序也不会受到影响。面向服务的应用程序是健壮的,因为开发者能够使用可用的、已验证的、通过测试的公共基础功能。同时也提高了开发者的效率,因为他们可以将更多的时间投入到功能特性的实现,而不是这些公共基础功能。面向服务的真正价值就是:允许开发者从代码中抽取出公共基础功能的实现,更多地关注业务逻辑和需要的功能特性。

面向服务还包括许多广受欢迎的价值,例如跨技术的互操作性,就是核心价值的体现。虽然不借助于服务,我们也能够实现互操作性,但直到面向服务的诞生,才能够应用到实践中。两者的区别在于后者能够通过已有的公共基础功能为开发者提供互操作性。编写服务时,通常不用考虑客户端执行在什么平台上,因为面向服务完全实现了无缝的互操作性。面向服务应用程序所能提供的不仅仅是互操作性,它还允许系统跨越边界。其中一种边界就是技术与平台的边界,跨越这样的边界则完全体现了互操作性。但是,边界可能还存在于客户端与服务之间,例如安全与信任边界、地域边界、组织边界、时区边界、事务边界,甚至是业务模型边界。无缝地跨越这些边界是可能的,原因在于基于消息的交互标准。例如,保障消息安全的标准,建立客户端与服务安全交互的标准,即使交互双方存在于不具有直接信赖关系的域(或站点)中。事务标准允许客户端的事务管理器将事务传递到服务端的事务管理器,并让服务参与到事务中,即使两个事务管理器从来没有直接登记彼此的事务。

面向服务应用程序

一个面向服务应用程序只是简单地将服务组合到一个单一逻辑的、整体的应用程序(参见图A-1)中,这类似于聚合了对象的面向对象应用程序。
 figure 1.1.gif
图A-1 面向服务应用程序

应用程序自身可以将组合服务公开为新的服务,就好像一个对象可以由多个小的对象组成一样。

在服务内部,开发者仍然使用传统编程的概念,例如特定的编程语言,版本,技术与框架,操作系统,API等。但是,服务之间则必须使用标准的消息与协议、契约以及元数据交换。

应用程序中的不同服务全部可以放到相同的位置上,或者分布放到企业网或互联网上。它们也可以来自于多个开发商,使用各种不同的技术与平台进行开发,版本独立,甚至执行在不同的时区。所有的这些公共基础功能特性对于在应用程序中与服务交互的客户端而言,都是隐藏的。客户端发送标准消息到服务,两端的公共基础功能通过消息以及与平台无关的传输型表示形式进行转换,并对客户端与服务之间存在的区别实现封送(Marshal)。

既然面向服务框架为了将服务连接在一起,提供了现有的公共基础功能,那么服务的粒度越小,就越能够有助于应用程序对这些基础设施的使用,开发者所要编写的公共基础功能就越少。在极端的情况下,每一个基本的类都应该是服务,以便于最大程度地利用现有的互联性,避免编码实现公共基础功能。理论上讲,它能够轻松地实现事务型整数,安全字符串以及可靠的类。然而实际上,粒度过于细化会影响到使用的框架(例如WCF)的性能。我相信随着时间的流逝,以及面向服务技术的发展,服务边界的内聚性会越来越强,服务的粒度会越来越小,甚至于每个基本的构建模块都可以成为服务。显然,这是历史的发展趋势,那就是通过方法学的改进以及抽象,以性能换取效率的提高。

要素与原则

面向服务方法学负责管理服务之间所发生的内容(参见图A-1)。它有一套设计原则与最佳实践,用于构建面向服务应用程序,称为面向服务架构原则:

服务边界是明确的

任何服务总是被限制在边界例如实现技术和分布位置之后。服务公开的契约与数据类型不会将它的实现技术与分布位置透露给客户端,从而隐藏了这些边界的本质。坚持这一原则可以使得服务与位置和技术无关。不管是以何种方式思考这一原则,它所表达的思想就是客户端知道服务的实现越多,则客户端与服务的耦合度就越高。要减小潜在的耦合度,服务就必须明确地公开它的功能,而且只有操作(或数据契约)才会被明确地被公开给客户端共享。服务的其余内容会被封装起来。面向服务技术采用了默认为“否决(Opt-Out)”的编程模型,公开的内容则被明确标记为参与(Opt-In)[注]。
译注:Opt-Out与Opt-In本身属于发送广告中的两种不同行为与授权方式。在这里,Opt-Out指的是如果服务的成员没有明确地进行设置,则默认是不暴露的,即否决机制。Opt-In则指的是只有明确标记了需要暴露的成员,则该成员才会参与到服务中,能够被跨越服务边界调用。

服务是自治的

服务无需获取它的客户端或其它服务的内容。服务的运行与版本应该与客户端无关。这一原则允许服务脱离客户端单独演化。服务的安全也是独立的,它能够保护服务自身以及传递的消息,而不用考虑客户端使用的安全级别。这样做(除了尽人皆知的常识之外)同时也能够解除客户端与服务安全之间的耦合。

服务共享操作契约与数据样式,而不是类型与特定技术的元数据。

服务要做的就是决定公开在服务边界之外内容应与技术无关。服务能够将本地的数据类型转换为某种与技术无关的表示形式,而不是共享本地的、特定技术的内容,例如程序集版本号或者它的类型。此外,服务应该禁止客户端知道本地的实现细节,例如实例管理模式或并发管理模式。服务只公开逻辑操作。服务对于操作的实现方式以及执行方式,对于客户端而言是不透明的。

服务与策略保持一致

服务应该发布一种策略,指示它所能完成的内容以及客户端与服务交互的方式。策略所体现的访问约束(例如可靠通信)不应依赖于服务的实现细节。并非所有的客户端都能与所有的服务交互。这种不兼容性是完全有效的,它能够防止特殊的客户端访问服务。发布的策略是客户端决定它们能否与服务交互的唯一方法,同时不应有任何的带外机制让客户端做出这样的决策。不同的是,服务必须能够在策略的标准表达形式中,表示服务能够执行的内容以及客户端能够与之通信的方式。如果无法表示,就意味着服务的设计是拙劣的。注意,如果服务是私有的(即不是公有服务),那么实际上它可能不会发布任何策略。这一原则暗示如果服务需要,就应该能够发布策略。

实用原则

前面列举的原则是非常抽象的,对它们的支持主要体现在开发、调用以及设计服务的技术方面。因此,应用程序可能会不同程度地遵循这些原则,正如开发者可以在C++中编写非面向对象的代码那样。然而,精心设计的应用程序应该力图坚持这些原则。因此,我又补充了一些更加实用的原则:

服务是安全的

服务与它的客户端必须使用安全通信。至少,从客户端传递到服务的消息必须是安全的,客户端必须具有验证服务的方法。同时,客户端可能会在消息中提供它们的安全证书,这样服务才能够对它们进行授权与认证。

服务在系统中应保持一致的状态

执行客户端请求时,禁止进行部分替换的条件。服务访问的所有资源在客户端调用之后必须是一致的。服务不能有任何剩余内容作为错误的结果,例如部分地影响系统状态。服务不应寻求它的客户端的帮助,在发生错误后,服务会将系统恢复为一致的状态。

服务是线程安全的

服务必须设计为线程安全,才能够维持多线程的并发访问。服务同样能够处理因果关系或逻辑线程的重入。

服务是可靠的

如果客户端调用服务,客户端总是能够以确定的方式获知消息是否被服务接收。消息应该按照发送的顺序处理,而不是接收的顺序。

服务是健壮的

服务与它的错误分离能够防止错误影响服务本身或其它服务。服务不能要求客户端根据服务遇到的错误类型改变它们的行为。这能够有助于客户端与错误处理层面上的服务解耦。

可选原则

我们可以将实用原则看作是强制原则,同时还有一套可选原则,这些原则并非所有应用程序所必需的,虽然坚持这些原则通常是一个不错的主意:

服务是互操作的

设计的服务应该能够被任意的客户端调用,而不用考虑客户端的技术。

服务的规模是不变的

不管客户端有多少,也不管服务的承载是多少,服务代码都应该相同。随着系统的发展,这样的设计才能够极大地降低维护服务的成本,服务也能够支持不同的部署场景。

服务是可用的

服务总是能够接收客户端的请求,而不会因此停止。如果服务不可用,则意味着客户端需要解决服务的问题,反过来就会引入耦合。

服务是及时响应的

服务开始处理客户端的请求时,不能让客户端等待太久。如果服务不能及时响应,则意味着客户端需要解决服务的问题,反过来就会引入耦合。

服务是受限的
服务执行的任意操作应尽可能短,不能消耗太多时间去处理客户端的请求。长时间的处理过程意味着客户端需要解决服务的问题,反过来就会引入耦合。