沿着“重用”我们一路走来——SA、OO(DP)、Component、SOA、AOP
Posted on 2006-12-29 13:09 idior 阅读(12612) 评论(48) 编辑 收藏 举报自从有了软件开发以来,消除重复,提高软件的重用性就一直是我们所追求的一个重要目标,本文将围绕着这一主题,带大家重走一遍结构化(SA)、面向对象(OO)与设计模式(Design Pattern)、组建化(Component)、面向方面(AOP)直至面向服务(SOA)这条软件开发技术的发展之路。让我从“重用”这么一个简单甚至是片面的的角度让大家对这些出现在众多文章中的名词有一个比较清晰的认识。虽然结构化,面向对象甚至设计模式已经被大家所熟知,不过"组建化"、"AOP"、"SOA"这些名词的意义可能很多人还尚未了解,而对它们的产生背景就更加陌生了。为什么需要某项新技术,这恰恰是我在深入学习它之前最关心的问题。
结构化编程,恐怕园子里几乎没多少人曾经用过它来开发一个超过1000行代码的程序,我“有幸”在大一暑假(确切地说是大二短学期)完成一个作业的时候经历了一把。在结构化的世界中经常会出现这样或那样的重复,相似的代码随处可见。虽然那时还很菜,不过很自然的,你会把一些常用的函数按功能归纳到各个模块中,在以后的项目中不断地重用它。很明显,此时你会尽量减少使用全局变量,因为在这种时候我们最怕的就是一种被称为“side effect”的问题,也就是你在A处修改了某个变量的值,却无意中影响了B处的结果。这样我们就会尽量把代码模块化,让一个模块中的变量不会被另一个模块使用。Hmmm...难道你没有从中看出封装的影子吗?将变量封装到对象内部,防止外界的修改这不正是面向对象的特性之一吗?那么,原来在结构化中的一个个功能模块此时就变成了面向对象中的一个个类了,对象由此而来。我们知道面向对象有三个重要特性——封装,继承,多态。那么继承和多态又是如何为减少重复代码做贡献的呢?继承相当容易理解,子类继承父类,自然的获得了父类的功能,父类实现功能的代码不用在子类中重复一遍,自然减少了重复。多态就没继承那么直接了。其实多态也正是在初学面向对象编程时,很多人都不理解甚至不知道它有什么用的一个概念。虽然本文的主要目的不是介绍多态,不过我很想就“消除重复”这个话题,谈谈我对多态的理解。
仍旧以计算工资这个老例子为例,有两种类型的员工:正式工、小时工,它们的工资计算方法各不相同。先看一下在结构化中的实现:
int GetSalary(string employeeName) { int baseSalary,bonus; if (isOfficalEmploree(employeeName)) { baseSalary=1000; bonus=500*GetRate(employeeName); } else { baseSalary=50*GetHours(employeeName); bonus=0; } return baseSalary+bonus; }
现在只有两个类型的员工,计算工资的方法也不复杂,你已经可以从中看到“重复”的影子。我们可以将这种类型的问题归结如下:
if (... ) { A(); B(); C(); } if (... ) { A(); B(); C(); } //...
函数的处理步骤基本相同,只不过A,B,C在不同情况下(也就是面向对象中的不同类型)有不同的处理逻辑。我们再用面向对象的方法改写上面的例子:
int GetSalary(Employee employee) { int baseSalary=employee.GetBaseSalary(); int bonus=employee.GetBonus(); return baseSalary+bonus; }
重复出现的A,B,C不见了,这不正是减少了重复代码吗?无论什么样的员工,GetSalary方法都可以得到重用。(上面这个例子仅用于说明消除重复这个问题,请不要细究它的设计是否合适)
正是因为这个原因,接口才在面向对象语言中大行其道。
public void UseQueue(IMessageQueue q) { //... q.Send("Hello Reusable Method!"); //... }
上面这段代码意味着什么?使用了IMessageQueue使该方法获得了最大程度的重用性,不论具体的MessageQueue怎么变,该方法总是可以重用的。 (具体例子参见Separate Contract from Implementation一文)
结构化语言在通过模块化,函数指针等等技术的帮助下虽然能部分解决重用问题,但是它已经显得捉襟见肘,此时出现了面向对象语言,在灵活运用封装,继承,多态的基础上,我们最大限度地解决了源代码级的重用问题。注意,此时出现了一个新名词——源代码级的重用。既然有源代码级的重用,那么自然也就有其他级别的重用,显然我们此时已经不满足于在代码层次上的重用。我们希望实现更高级别的重用。其实在面向对象刚出来那会,也并没有多少人能够真正理解该技术并灵活运用其特性提高代码的复用性。没多久,被称为GOF的四个人出了一本书——《Design Patterns》。在这本书中,非常深刻地阐述了应该如何运用面向对象技术以及我们能从中获得的好处,它告诉了我们如何编写可复用的代码,并把软件复用技术提升到了另一个高度——思想的复用。在设计软件的时候,我们可以站在比代码更高的层面来看待如何复用软件设计中的这些模式。现在几乎人人皆知《Design Patterns》这本圣经,可有多少人注意过这本书的子标题呢?——《Elements of Reusable Object-Oriented Software》。
从代码到思想,设计模式完成了一次复用的飞跃。那么还有其他形式的复用吗?随着软件开发技术的发展,更高的复用需求随之而来。
试想一下,如果你在一个项目中需要用到一个以前写过的功能,那么你需要做什么呢?你不得不把以前的代码全部拷贝到新的项目中。过了一段时间,你突然发现这段经常被别的项目重用的代码存在一个bug,需要做一些小的修改,那么你能想象你需要为此付出的代价吗?源代码级别的重用在一个单一的项目中不存在太大的问题,但是面对更高级别的重用——跨项目的重用,就暴露出了它所存在的问题。如何解决?对.NET技术非常熟悉的读者肯定想到了引用了dll组件。将需要被重用的代码放入一个单独的dll组件中,需要使用该功能的项目只要添加这个dll的引用即可。将来被重用的代码发生变化时,只需将这个dll组件更换成新版本即可。此时,你已经在不经意用到了构建化技术(构建,组件均代表了Component这个词)。构建化技术使我们获得了二进制级别的重用,它在一定程度上解决了项目间的复用问题。不仅如此,在某些组件技术中,还实现了跨语言的重用。如在COM以及.NET技术的支持下,我们可以使用VB调用由C++或其他语言编写的组件。而之所以能实现这一点,全依赖于组件的自描述功能,也就是说组件中不仅仅包含一个个类,还额外包含了描述这些类、方法、参数等等的类型信息。也正是由于这些信息的存在,才造就了一个新的编程利器——“反射(Reflection)”。
在OO以及Component技术的帮助下,我们已经能够非常方便地实现一些软件功能的复用。不过这些复用通常是针对那些在一个或多个项目中经常用到的小功能。随着软件技术的发展,软件的规模越来越大,越来越复杂,此时对复用技术又提出了更高的要求。我们希望在两个独立的应用系统之间也能实现复用。即系统A需要某些功能,而恰恰之前的系统B具有这些功能,我们在开发系统A的时候,一种方法就是所有的功能都由自己开发,这样系统的实现形式非常统一,功能交互的方法,参数等等也可以完全由自己控制。但是这样做显然是一种资源的浪费,如果系统B的不是很复杂还可以考虑采用这种方法,但是在大多数情况下,重写系统B的代价是不可接受的。另一个方法就是由系统B暴露出核心功能的接口,之后系统A利用这些接口与系统B产生信息交互,这样实现了系统B在另一个系统中的重用。这种应用系统间的重用在一个企业内部,以及企业与企业之间广泛存在。典型的例子就是对遗留系统的重用以及对外部系统的重用。在这种情况下,系统A与系统B分别运行在两个独立的进程中,他们可能在同一台机器中,也可能在两台不同的机器上。为了实现功能的交互,我们不可避免的要用到分布式对象技术。通过组件中的类型元数据,我们可以轻易地在客户端生成远程对象的接口,并进一步创建出代理对象实现客户端对远程对象的访问。Remoting中的Soapsuds.exe就是在.NET的分布式系统中实现此类功能的一个工具。所以说组件技术在实现应用系统间复用的时候也起了相当重要的作用。
现在让我们再回过头来看看面对对象技术的应用在这种分布式环境下又发生了怎样的变化。首先需要注意的是系统B并不需要将它所有的业务对象暴露出来供系统A使用,我们需要重用的是系统B的功能,此时我们通常会利用Facade模式将系统B的核心功能集中到一个Facade对象中,所以继承、多态这些面向对象特性在分布式对象中基本上是用不到的。其次,Facade对象中的方法都是粗粒度的,也就是说其中的一个方法通常是完成一次性完成一个复杂的功能。比如,你不可能像下面这样使用一个远程对象:
Person p=RemoteGatway.GetPerson("idior"); p.Age=24; p.Sex=Sex.Male; //...
而应该采取类似下面的方法:
Person p=RemoteGatway.GetPerson("idior"); p=p.SetBaseInfo(new PersonInfo(24,Sex.Male,...));
这样做的原因是避免产生过多的网络调用,而在这一点上普通对象恰恰与此相反。另外为了实现性能上的scalability(伸缩性),Facade对象通常是无状态的(具体内容参考Distributed Application --- Applying Remoting & Enterprise Service)。由此看来,为了实现系统间的交互,Facade对象基本上完全丧失了面向对象中一个对象应该具有的特性,我把这种对象称之为服务对象。面对服务的概念呼之欲出。不过这些分布式对象往往局限于某个特殊的分布式技术,比如.NET的Remoting、Cobra或者是EJB,如果系统A与系统B都是采用的同一种技术,那么就不存在太大的问题。不过在实际的应用中,又哪来那么多巧合呢。如果一个.NET的应用想与一个Java应用交互,可想而知这是一件多么困难的事情。并且在企业的应用中我们不可能不考虑整个系统的安全性以及事务性,让两个不同的分布式系统配合起来工作并保证安全与事务几乎是件不可能完成的任务。然而这种性质的重用需求又是如此的广泛,终于,为了解决此问题,我们迎来了SOA。
SOA最关键的一个特性就是loosely coupled(松耦合)。现在你应该能够明白为什么这点最为重要了,为了实现异构系统间的集成,服务只有具备了松耦合的特性才能彼此交互。SOA中的服务就是之前提到的Facade对象的升级版,只不过用于描述服务的元数据以及用于调用服务的方式都必须具有松耦合的特性,即不依赖于特定的平台。对比我们之前提到的组件技术,它也提供了服务对象的元数据,但是这些元数据的描述是与具体的组件技术相关的,而调用组件中对象的技术也是依赖于某一特定平台的。为了获得松耦合的特性,我们采用了Web Services技术作为SOA的具体实现。其中WSDL用于描述服务的元数据,而SOAP则规定了如何调用服务并返回结果。这些规范都是符合特定标准的,而各个厂家也都将遵循这些标准。事实上之前提到的Facade对象,在Java下往往会由EJB中的Session Bean实现,在.NET下可以用企业服务中的ServicedComponent实现,而在各自的环境下都可以方便地将该对象升级为Web服务。如在JBuilder中可以使用某些菜单项方便地将一个Session Bean发布成一个Web Service,而COM+1.5也提供了SOAP Services的功能,利用它可以非常简单地将一个ServicedComponent以Web Service的形式发布。
由此,我们可以看出SOA较好地解决了应用系统间的重用问题。SOA是一套系统集成的解决方案,它与传统的分布式对象技术的主要区别在于它的松耦合性。
最后提一下AOP,之前我们谈到的重用都是针对一个功能的实现,你可曾想过调用功能的重用问题。任何一个功能为了使用它,你必须调用相应的对象中的某些方法。而这段调用代码如果不断地出现在你的代码中,你难道不觉得它是一种重复吗?举个简单的例子,为了记录系统的行为,我们经常需要一个日志,而记录日志的方法调用几乎散布在系统的每个角落。我们多么希望能够只在一个地方定义一次调用代码,就能够记录下所有的日志。AOP或许能够帮你解决此类问题,具体内容可以参考我之前写的一篇文章No Buzzword AOP。
综上所述,我们可以发现,软件的重用问题一直贯穿了软件开发技术的发展。本文也正是从该角度对目前繁杂的开发技术做了一个大概的介绍,希望对大家理解这些概念有所帮助。
推荐文章:
WS-Addressing 从理论到实践 --- SOA基础规范介绍
Distributed Application in .Net
后记:
写这篇文章的时候发现自己底子实在太薄,中途都想放弃,结结巴巴总算是完成了。现在觉得文章越来越难写,希望自己能坚持下去。还有一点我发现我文章的读者潜水的居多啊,基本在评论中没啥交互。