首页 > 技术文章 > 软件工程简史

larissa-0464 2019-12-04 20:11 原文

从《WCF服务编程》一书中抄下来的,感觉对面向对象、面向组件和面向服务进行了很好的解释,但是对于我而言,需要反复看才能理解。

 


 

20世纪40-50年代,第一台通用的计算机才真正问世,它主要用于国防。这个时期的计算机可以运行代码并处理某些问题,而不仅仅是单个的预定义的任务。其缺点是计算机执行的代码与机器的硬件是紧密耦合的。实际上,当初计算机的软件与硬件之间并没有明显的区别(软件一词是在1958年才提出的)。起初,这个缺点并未引起大家的关注,因为世界上还没有手提电脑,都是超大型计算机。随着计算机产品大量生产,问题出现了。在20世纪60年代早期,汇编语言的出现才真正把机器与代码解耦开来,代码可以执行在不同的机器上。但是,代码与计算机的架构关联,即编写在8位机上的程序不能运行在16位机上,因为计算机的寄存器、可用内存和内存分布都不一样。因此,维护程序的成本逐渐增加。随着计算机的广泛应用,民众和政府部门为满足有限的资源与预算,提供了更好的解决方案。

20世纪60年代,更高级别的语言,如COBOL和FORTRAN,引入了编译器的概念。开发人员可以编写更加抽象的程序,编译器可以将其转化为实际的汇编代码。编译器第一次把代码从硬件和计算机架构解耦开来。第一代语言存在的问题是代码会导致非结构化的编程,这些代码通过使用jump或者go to语句依赖于它自身的结构。即使一行代码的更改也会导致程序中多个地方修改。

20世纪70年代,结构化语言,如C或Pascal占据了统治地位。它通过函数与结构体可以把代码与代码内部的布局和结构解耦出来。正是这时,开发者和研究者首次开始把软件行业作为软件工程进行研究。为了降低成本,许多公司开始考虑软件的重用,即如何让代码重用在其他上下文里。使用像C这样的编程语言,重用的基本单元是函数但是基于函数重用的问题是函数与其使用的数据是紧密耦合的,如果数据是全局的,那么任何函数的修改可能会导致其他函数无法使用。

*面向对象

20世纪80年代出现的问题的解决办法就是,使用类似于Smalltalk和C++这样的面向对象的语言。面向对象语言将函数与其操作的数据封装到对象里。函数(现在称方法)封装逻辑,对象则封装数据。面向对象(OO)通过类级别的形式以支持领域模型。这个重用机制是基于类的,可以通过继承来实现重用。但是,面向对象仍然存在自己的问题。首先,生成的应用程序(或伪代码)是单一的应用程序。像C++这样的编程语言并不能识别二进制生成的代码。即使只是进行细微的修改,开发者每次都必须重新部署所有的代码,这会影响应用程序的开发过程、质量、发布时间及成本。重用的级别是类,并且是源代码形式。因此,应用程序会依赖于它使用的语言。语言的重用要求技能的统一(所有开发人员必须熟悉同一种语言),这会导致额外的问题。语言的重用也限制了应用程序的规模,因为如果公司要求使用多种语言,那么就必须重复投资多个开发语言和框架。最后,必须访问源代码文件才能够重用对象,这样就必须让开发人员、代码控制及团队之间高度耦合,因为独立编译十分困难。而且,继承是一种糟糕的重用方式,大多数情况下,它都是坏处比好处多,因为子类必须知道基类的实现细节(这会在类层次间引入纵向耦合)。

OO忽略了很多现实问题,比如部署和版本问题。序列化和持久化则是存在的另外一系列问题。大部分应用程序都无法解决这类问题;它们有些持久化状态信息需要保存到对象里。但是,却没有办法强制持久化状态和潜在的新对象之间的兼容性。面向对象假定了整个应用程序运行于单个进程中,以避免客户端和对象之间因为隔离而出现故障。如果对象销毁,还能够从中获得客户端及进程中的其他所有对象。单个进程则意味着客户端和对象有统一的标识,而不会存在安全隔离。因为它们与对象有相同的标识,所以可以验证和授权客户端。单个进行也会影响系统的可伸缩性、可用性、响应性、吞吐量和健壮性。如果对象需要跨越多个进程或机器,就没办法使用原生态的C++来调用对象,因为C++需要内存引用,并不支持分布式调用,所以开发人员需要手动在各个进程里放置对象。开发人员必须编写宿主进程,使用远程调用技术(如TCP套接字)来调用远程对象,但是,这样的调用与原生态的C++调用方式不同,无法使用面向对象带来的好处。

 

*面向组件

随着时间的推移,相继产生了一些新的技术,如静态库(.lib)和动态库(.dll),以及1994年出现的面向组件技术,即COM,它们都能够解决面向对象存在的问题。面向组件提供了可交换的、互操作的二进制组件。使用它,而不是源代码文件,客户端和服务可以共享一套二进制类型系统以及表示二进制组件的元数据格式。组件在运行时加载,允许这样的场景,如拖动一个控件到窗体上,让控件在客户机运行时自动加载。客户端应用只需要知道抽象的服务,这个契约称为接口。代理可以实现相同的接口,通过封装远程调用来实现调用。公共二进制类型系统可以让跨语言互操作成为可能,这样,VB客户端就能够调用C++的COM组件。重用的单元是接口而不是组件,多态的实现是可互换的。通过为每个接口、COM对象和类型库分配唯一标识符来解决版本冲突的问题。

虽然COM是现代软件工程领域的一个重大突破,但是在大部分开发人员眼中却如鸡肋一般。COM未必是丑陋的实现,因为它是基于特定的操作系统构建的,且编写COM组件的语言都是OO的(如C++和VB)而不是面向组件的。这使得编程模型十分复杂,需要像ATL着这样的框架才能连接不通的世界。认识到这些问题后,微软在2002年发布了.NET 1.0。.NET相对于COM、MFC、C++和Windows,它所有的代码都运行在一个新的组件对象运行时中。.NET支持所有的COM特性,并且强制和标准化一些机制,如元数据共享、动态组件加载、序列化和版本控制。

.NET具有更强的功能与COM协作,但两者又都存在类似的问题:

#技术与平台

应用和代码依赖于技术和平台。COM和.NET都只能应用于Windows平台。两者都希望客户端与服务是基于COM和.NET的,因为它们不支持与其他技术的互操作。虽然可以使用Web服务来实现互操作,但是会强制开发人员放弃使用原始框架的诸多好处,从而因为自己的复杂性,与互操作机制耦合在一起。因此,它也破坏了规模经济定律。

#并发管理

当供应商发布新的组件时,并不能假设所有的客户端不会对其进行多线程的并发访问。实际上,唯一安全的假设就是组件支持多线程并发访问。因此,组件必须是线程安全的,同时必须使用同步锁机制。然而,如果应用程序开发人员集成了多个厂商的多个组件,那么就很有可能出现死锁。必须避免应用程序和组件之间耦合带来的死锁。

#事务

如果多个组件参与到单个事务里,那么托管组件的应用程序必须协调事务并把事务流从一个组件传播到另一个组件,这是一个非常严肃的编程问题。不论事务之间如何协调,它都会带来应用程序与组件之间的耦合问题。

#通信协议

如果组件是跨进程或跨机器部署的,则组件就依赖于远程调用、传输协议和编程模型的潜在特性(如可靠性和安全)

#通信模式

组件可以以同步和异步方式调用,也可以联机或离线调用。组件可以使用任意的模式调用,应用程序必须知道这些需求。使用COM和.NET,开发异步,甚至队列解决方案仍然是开发人员的工作,这种解决方案或许不难,但是会在组件和解决方案之间引入耦合。

#版本控制

在编写应用程序时也许会使用一个版本的组件,而在产品发布时使用的是另外的版本。COM和.NET都需要忍受DLL Hell带来的问题,所以两者都保证:客户端会在运行时获得与编译时版本一样的组件。这种保守的做法阻止了新组件的使用。COM和.NET都提供了自定义版本的策略,但是都存在出现DLL Hell问题的可能。还没有内置的版本包容机制来解决应用程序与它使用的组件之间的版本问题。

#安全

组件需要对它们的调用者进程验证和授权,但是,组件如何才能知道它所使用的安全验证主体,或者用户所对应的角色?不仅如此,组件还需要确保来自客户端的通信是安全的。当然,这会给客户端带来一些限制,从而迫使客户端必须与COM使用的安全框架耦合在一起。

现有的技术

从抽象层次上来看,互操作、并发、事务、协议、版本控制和安全类似于把任意应用程序连接在一起的管道。

对普通的应用程序来说,开发和调试的大部分时间都花费在这样的技术问题上,而不是业务逻辑和特征上。更糟糕的是,因为最终用户(或者开发经理)很少关注组件技术(与业务特性相比),而开发人员显然没有足够的时间去开发健壮的组件。实际上大部分组件都是专有的(因而很难重用或迁移)、低劣的,因为大部分开发人员不是安全或同步专家,也没有足够的时间和资源去开发完美的组件。

解决方案就是使用提供了服务技术框架的开发服务。第一个尝试就是MTS,于1996年发布。MTS不仅提供了对于事务的支持,而且包含安全、托管、激活、实例管理和同步。J2EE发布于1998年,COM+发布于2000年,而.NET Enterprise Services却发布于2002年。这些平台都提供了足够完善的组件接口(适用于多种级别的需求),使用这些平台的应用程序能够更好地专注于业务逻辑。然而,这些平台没有被大规模地使用,其原因便是边界问题。很少有系统是孤立运行的,大部分系统需要与其他的系统交互。如果其他系统不使用相同的技术平台,就无法平滑地进行互操作。例如,没有办法把COM+事务传递给J2EE组件。因此,当跨系统边界时,组件(组件A)需要与平台中间的公共决策者交互。但是,组件B呢?邻近组件A吗?如果考虑组件B,它是无法理解组件A的各种接口的,所以B必然无法交互。因此系统边界正在自外向内缩进,这也阻碍了现有技术框架的应用。

*面向服务

如果回顾软件工程发展的历史,就会发现一个现象:每种新的技术和方法都与前面的技术无法兼容,而且都是在尝试解决之前技术的不足之处。但是,每种新技术又会带来新的挑战。因此,可以说现代软件工程就是对低耦合的不断追求。

换句话说,耦合虽然不好,但是不可避免。真正解耦的应用程序是没有任何用处的,因为它也没有任何价值。开发人员只能通过耦合事务来提升价值。事实上,编写不同的代码就是把各种事务耦合起来。现实的问题就是如何明智地进行耦合。有两种耦合,即好的耦合和坏的耦合。好的耦合是业务级别的耦合。开发人员通过耦合软件功能来实现系统用例或特性而提升价值。坏的耦合就是写的组件依赖别的东西。使用.NET或COM存在的问题不是概念上的错误,而是开发人员不能依赖现有的组件技术,而仍要编写大量的代码这一事实。实际的解决方案不仅仅是提供现有的技术平台,而是要为它们建立标准的组件框架。然而,这些组件框架是标准的,边界问题就不会这么明显,应用程序就可以使用已经编写好的组件。但是,所有的技术(.NET&JAVA等)都是为了客户端线程对对象进行操作。如何才能用.NET线程去执行JAVA的对象?解决办法就是避免“调用堆栈”的方式,相反应该使用消息交换。技术厂商可以独立于消息格式,在事务、安全凭据等问题上达成一致的协议。当消息被其他端接收时,组件会被消息转化为原始调用(.NET或Java线程可以执行的对象)。因此,任何尝试标准化组件的技术都是基于消息的。正是认识到过去的问题,在2000年后期,面向服务已经成为面向组件问题的最佳解决办法。在面向服务的应用程序中,开发人员只要关注于编写业务逻辑,以及通过可交换的、可互操作的服务终结点公开逻辑。客户端调用这些终结点,而不是服务代码或它的打包。客户端与服务终结点之间的交换是基于标准的消息,而服务会发布一些描述自己功能的信息,自己能做什么、客户端如何调用服务等。服务元数据与C++的头文件、COM类型库、.NET程序集元数据类似。不兼容的客户端(即客户端与组件期望的对象不兼容)不能调用服务,因为调用会被平台拒绝。这是面向对象和面向组件编译时概念的延伸,那就是与对象元数据不兼容的客户端无法调用它。要求与顶端服务平台框架兼容是至关重要的。否则,对象就必须一致检查客户端的调用消息是否满足比如安全、事务、可靠性等方面的要求。不仅如此,服务终结点可以被符合交互规则的任何客户端调用,而不论客户端实现技术如何。

从很多方面来看,服务是组件的进化就像组件是对象的进化一样,而对象又是从函数进化而来的。面向服务就是构建可维护的、健壮的且安全的应用程序的最好方式。

改进面向组件编程缺点的结果就是,当开发面向服务应用程序时,需要把服务代码从客户端使用的技术和平台,包括并发管理问题、事务传播和管理、通信的可靠性、协议和模式中解耦出来。总的来说,保护消息传输也超出了服务的范围,包括验证客户端。但是,如果需要,服务也需要作出本地授权。与此类似,只要终结点支持客户端希望的契约,客户端就不需要关注服务的版本。标准已经包含了对于客户端与服务端交互消息版本问题的兼容性。

面向服务的优势

面向服务可以开发出容易维护的应用程序,因为应用程序在需要的地方是完全解耦的。即使技术更新,应用程序仍然不受影响。面向服务的应用程序是健壮的,因为开发人员可以使用可用的、已验证过和测试过的技术。同时提高了开发人员的效率,因为他们可以把更多的时间花在系统功能特性而不是技术上。面向服务的价值就是:允许开发人员从具体的技术中分离出来,把更多的时间和精力放在定义业务和功能特性上。

面向服务还包括许多其他的好处,比如跨技术操作性就是核心价值的体现。即使不使用服务也可以实现互操作,但直到面向服务出现以后才能够应用到实践中。区别就是使用已有的技术平台,需要依赖这些技术平台来提供互操作性。

当编写服务时,不需要关注客户端使用什么平台,因为互操作性进行了封装。面向服务的应用程序不单单只是提供了互操作性,它还可以让开发人员跨越平台边界。一种边界就是技术和平台的边界,跨越边界就是互操作性关注的内容。但是,其他边界也可能存在于服务和客户端之间,比如安全和信任边界、地域边界、组织边界、时区边界、事务边界、甚至是业务模型边界。无缝跨越各个边界是可能的,因为交互式基于消息的。

每个应用程序都可以是面向服务的,而不仅仅是企业应用才需要互操作和可伸缩性。针对每种应用都编写一种技术框架显然过于荒谬,既浪费人力物力,也浪费时间和金钱,从而导致质量下降。正如.NET的每个应用程序都是面向组件的一样(使用COM很难实现),C++的每个应用程序都是面向对象的,若使用WCF,则每个应用程序都应该是面向服务的。

面向服务的应用程序

服务是通过标准接口公开给外部世界的功能单元。简单来说,面向服务的应用程序就是把多个服务聚合到单个逻辑化、高内聚的应用程序里,它更像是多个对象的聚合。

在服务内部,开发人员仍然使用特定的语言、版本、技术和框架、操作系统、API等来编写代码。但是在服务之间则必须使用标准的消息和协议、契约以及元数据交换。

程序里的服务可以处于某一个地方,也可以分布在Intranet或Internet的任何角落里。它们可以属于不同的公司,或者被不同的技术平台所开发,甚至在不同的时区里。所有这些差别都可以隐藏起来。客户端发送标准的消息给服务,而程序组件则负责在服务和客户端之间转换消息并发送出去,这些平台的差异都被忽略了。

推荐阅读