—选自《企业应用架构模式》
目 录
1引言
构建计算机系统并非易事。随着系统复杂性的增大,构建相应软件的难度将呈指数增大。同其他行业一样,我们只有在不断的学习中进步,从成功经验中学习,从失败教训中学习,才有望克服这些困难。本书的内容就是这样一些“学习”经验。我希望它们的撰写和编排方式,能够有助于读者更快地学习这些内容,并且,和我在总结出这些模式之前相比,能更有效地与他人进行交流。
在引用中,我想设定本书讨论的范围,并提供一些相关的背景知识与材料。
0.1 架构
软件业的人乐于做这样的事——找一些词汇,并把它们引申到大量微妙而又互相矛盾的含义。一个最大的受害者就是“架构”(architecture)这个词。我个人对“架构”的感觉是,它是一个让人印象深刻的词,主要用来表示一些非常重要的东西。当然,我也会小心,不让这些对“系统结构”的“不恭之词”,影响到读者对本书的兴趣。
很多人都试图给“架构”下定义,而这些定义本身却很难统一。能够统一的内容有两点:一点是“最高层次的系统分解”;另一点是“系统中不易改变的决定”。越来越多的人发现:表述一个系统架构的方法不只一种;一个系统中也可能有很多种不同的架构,而且,对于什么在架构上意义重大的看法也会随着系统的生命周期变化。
Ralph Johnson经常在邮件列表上发帖,并提出一些令人关注的见解。就在我完成本书初稿的同时,他又发表了一些关于“架构”的观点。他认为,架构是一种主观上的东西,是专家级项目开发人员对系统设计的一些可共享的理解。一般地,这种可共享的理解表现为系统中主要的组成部分以及这些组成间的交互关系。它还包括一些决定,开发者们希望这些决定能及早做出,因为在开发者看来它们是难以改变的。架构的主观性也来源于此——如果你发现某些决定并不像你想象的那么难以改变,那么它就不再与架构相关。到了最后,架构自然就浓缩成一些重要的东西,不论这些东西是什么。
在本书中,我提出一些自己的理解,涉及企业应用主要组成部分和我希望能尽早做出的决定。在这些架构模式中,我最欣赏的就是“层次”,将在第1章中进行详细介绍。全书实际上就是关于如何将企业应用组织成不同的层次,以及这些层次之间如何协同工作。大多数重要的企业应用都是按照某种形式的层次分层设计的;当然,在某些情况下,别的设计方式(如管道方式、过滤器方式等)也有它们自己的价值。在本书中我们将不会讨论这些方式,而把注意力集中在层次方式上,因为它是应用最广的设计方式。
本书中的一些模式毫无疑问是关于架构的,它们表示了企业应用各主要组成部分间的重要决定;另外一些模式是关于设计的,有助于架构的实现。我没有刻意区分这两类模式,因为正如我们前面讨论的,是否与架构相关往往带有主观性。
0.2 企业应用
编写计算机软件的人很多,我们通常把这些活动都称为软件开发。但是软件的种类是不同的,每种软件都有自身的挑战性和复杂性。我是在与几个从事电信软件开发的朋友交谈后,意识到这个问题的。企业应用在某些方面要比电信软件简单得多——多线程问题没有那么困难,无需关注硬件设备与软件的集成。但是,在某些方面,企业应用又比电信软件复杂得多——企业应用一般都涉及到大量复杂数据,而且必须处理很多“不合逻辑”的业务规则。虽然有些模式是适合所有软件的,但是大多数模式都还只适合某些特定的领域和分支。
我的工作主要是关于企业应用的,因此,这里所谈及的模式也都是关于企业应用的。(企业应用还有一些其他的说法,如“信息系统”或更早期的“数据处理”。)那么,这里的“企业应用”具体指的是什么呢?我无法给出一个精确的定义,但是我可以罗列一些个人的理解。
先举几个例子。企业应用包括工资单、患者记录、发货跟踪、成本分析、信誉评估、保险、供应链、记账、客户服务以及外币交易等。企业应用不包括车辆加油、文字处理、电梯控制、化工厂控制器、电话交换机、操作系统、编译器以及电子游戏等。
企业应用一般都涉及到持久化数据。数据必须持久化是因为程序的多次运行都需要用到它们——实际上,有些数据需要持久化若干年。在此期间,操作这些数据的程序往往会有很多变化。这些数据的生命周期往往比最初生成它们的那些硬件、操作系统和编译器还要长。在此期间,数据本身的结构一般也会被扩展,使得它在不影响已有信息的基础上,还能表示更多新信息。即使是有根本性的变化发生,或公司安装了一套全新的软件,这些数据也必须被“迁移”到这些全新的应用上。
企业应用一般都涉及到大量数据——一个中等规模的系统往往都包含1GB以上的数据,这些数据是以百万条记录的方式存在的。巨大的数据量导致数据的管理成为系统的主要工作。早期的系统使用的是索引文件系统,如IBM的VSAM和ISAM。现代的系统往往采用数据库,绝大多数是关系型数据库。数据库的设计和演化已使其本身成为新的技术领域。
企业应用一般还涉及到很多人同时访问数据。对于很多系统来说,人数可能在100人以下,但是对于一些基于Web的系统,人数会呈指数级增长。要确保这些人都能够正确地访问数据,就一定会存在这样或那样的问题。即使人数没有那么多,要确保两个人在同时操作同一数据项时不出现错误,也是存在问题的。事务管理工具可以处理这个问题,但是它通常无法做到对应用开发者透明。
企业应用还涉及到大量操作数据的用户界面屏幕。有几百个用户界面是不足为奇的。用户使用频率的差异很大,他们也经常没什么技术背景。因此,为了不同的使用目的,数据需要很多种表现形式。系统一般都有很多批处理过程,当专注于强调用户交互的用例时,这些批处理过程很容易被忽视。
企业应用很少独立存在,通常需要与散布在企业周围的其他企业应用集成。这些各式各样的系统是在不同时期,采用不同技术构建的,甚至连协作机制都不同:COBOL数据文件、CORBA系统或是消息系统。企业经常希望能用一种统一的通信技术来集成所有系统。当然,每次这样的集成工作几乎都很难真正实现,所有留下来的就是一个个风格各异的集成环境。当商业用户需要同其业务伙伴进行应用集成时,情况就更糟糕。
即使是某个企业统一了集成技术,它们也还是会遇到业务过程中的差异以及数据中概念的不一致性。一个部分可能认为客户是当前签有协议的人;而另外一个部门可能还要将那些以前有合同,但现在已经没有了的人计算在内。再有,一个部门可能只关心产品销售而不关心服务销售。粗看起来,这些问题似乎容易解决,但是,一旦几百个记录中的每个字段都有可能存在着细微差别,问题的规模就会形成不小的挑战——就算唯一知道这些字段之间差别的员工还在公司任职(当然,也许他在你察觉到之前就早已辞职不干了)。这样,数据就必须被不停地读取、合并、然后写成各种不同语法和语义的格式。
再接下来的问题是由“业务逻辑”带来的。我认为“业务逻辑”这个词很滑稽,因为很难找出什么东西比“业务逻辑”更加没有逻辑。当我们构建一个操作系统时,总是尽可能地使得系统中的各种事物符合逻辑。而业务逻辑生来就是那样的,没有相当的行政努力,不要想改变它,当然,它们都有自己的理由。你必须面对很多奇怪的条件。而且这些条件相互作用的方式也非常怪异。比如,某个销售人员为了签下其客户几百万美元的一张单,可能会在商务谈判中与对方达成协议,将该项目的年度到账时间推迟两天,因为这样才能与该客户的账务周期相吻合。成千上万的这类“一次特殊情况”最终导致了复杂的业务“无逻辑”,使得商业软件开发那么困难。在这种情况下,必须尽量将这些业务逻辑组织成有效的方式,因为我们可以确定的是,这些“逻辑”一定会随着时间不断变化。
对于一些人来说,“企业应用”这个词指的是大型系统。但是 需要注意的是,并不是所有的企业应用都是大型的,尽管它们可能都为企业提供巨大的价值。很多人认为,由于小型系统的规模不大,可以不用太注意它们,而且在某种程度上,这种观点能够带来一定的成本节约。如果一个小型系统失败了,相对于大型系统的失败,这种失败就不会显得那么起眼了。但是,我认为这种思想没有对小型项目的累积作用给予足够的重视。试想,如果在小型项目上能够进行某些改善措施,那么一旦这些改善措施被成功运用于大型项目,它带来的效果就会非常大。实际上,最好是通过简化架构和过程,将一个大型项目简化成小型项目。
0.3 企业应用的种类
在我们讨论如何设计企业应用以及使用哪些模式之前,明确这样一个观点是非常重要的,即企业应用是多种多样的,不同的问题将导致不同的处理方法。如果有人说“总是这样做” 的时候,就应该敲响警钟了。我认为,设计中最具挑战性(也是我最感兴趣)的地方就是了解有哪些候选的设计方法以及各种不同设计方法之间的优劣比较。进行选择的控件很大,但我在这里只选三个方面。
考虑一个B2C(Business to Customer)的网上零售商:人们通过浏览器浏览,通过购物车购买商品。通过购物车购买商品。这样一个系统必须能够应付大量的客户,因此,其解决方案不但要考虑到资源利用的有效性,还要考虑到系统的可伸缩性,以便在用户规模增大时能够通过增加硬件的办法加以解决。该系统的业务逻辑可以非常简单:获取订单,进行简单的价格计算和发货计算,给出发货信息。我们希望任何人都能够访问该系统,因此用户界面可以选用通用的Web表现方式,以支持各种不同的浏览器。数据源包括用来存放订单的数据库,还可能包括某种与库存系统的通信交流,以便获得商品的可用性信息和发货信息。
再考虑一个租约合同自动处理系统。在某些方面,这样的系统比起前面介绍的B2C系统要简单,因为它的用户数很少(在特定时间内不会超过100个),但是它的业务逻辑却比较复杂。计算每个租约的月供,处理如提早解约和延迟付款这样的事件,签订合同时验证各种数据,这些都是非常复杂的任务,因为租约领域的许多竞争都是以过去的交易为基础稍加变化而出现的。正是因为规则的随意性很大,才使得像这样一个复杂领域具有挑战性。
这样的系统在用户界面(UI)上也很复杂。这就要求HTML界面要能提供更丰富的功能和更复杂的屏幕,而这些要求往往是HTML界面目前无法达到的,需要更常规的胖客户界面。用户交互的复杂性还会带来事务行为的复杂性:签订租约可能要耗时1~2个小时,这期间用户要处于一个逻辑事务中。一个复杂的数据库设计方案中可能也会涉及到200多个表以及一些有关资产评估和计价的软件包。
第三个例子是一家小型公司使用的简单的“开支跟踪系统”。这个系统的用户很少,功能简单,通过HTML表现方式可以很容易实现,涉及的数据源表项也不多。尽管如此,开发这样的系统也不是没有挑战。一方面你必须快速地开发出它,另一方面你又必须为它以后可能的发展考虑;也许以后会为它增加赔偿校验的功能,也许它会被集成到工资系统中,也许还要增加关于税务的功能,也许要为公司的CFO生成汇总报表,也许会被集成到一个航空订票Web Service中,等等。如果在这个系统的开发中,也试图使用前面两个例子中的一些架构,可能会影响开发进度。如果一个系统会带来业务效益(如所有的企业应用应该的那样),则系统进度延误同样也是开销。如果现在不做决策又有可能影响系统未来的发展。但是,如果现在就考虑了这些灵活性但是考虑不得当,额外的复杂性又可能会影响到系统的发展,进一步延误系统部署,减少系统的效益。虽然这类系统很小,但是一个企业中往往有很多这样的系统,这些系统的架构不良性累积起来,后果将会非常可怕。
这三个企业应用的例子都有难点,而且难点各不相同。当然,也不可能有一个适合于三者的通用架构。选择架构时,必须很清楚地了解面临的问题,在理解的基础上再来选择合适的设计。本书中也没有一个通用的解决方案。实际上,很多模式仅仅是一些可选方案罢了。即使你选择了某种模式,也需要进一步根据面临的问题来修改模式。在构建企业应用时,你不思考是不行的。所有书本知识只是给你提供信息,作为你做决定的基础。
模式是这样,工具也同样如此。在系统开发时应该选取尽可能少的工具,同时也要注意,不同的工具擅长处理的方面也不同,切记不要用错了工具,否则只会事倍功半。
0.4 关于性能的考虑
很多架构的设计决策和性能有关。对于大多数与性能相关的问题,我的办法是首先建立系统,调试运行,然后通过基于测量的严格的优化过程来提高性能。但是,有一些架构上的决策对性能的影响,可能是后期优化难以弥补的。而且即使这种影响可以在后期很容易地弥补,参与这个项目的人们任然会从一开始就担心这些决策。
在这样的一本书中讨论性能通常很困难。这是因为“眼见为实”:所有那些关于性能的条条框框,不在你的具体系统中配置运行一下,是很难有说服力的。我也经常看到一些设计方案因为性能方面的考虑而被接受或拒绝,但是一旦有人在真实的设置环境中做一些测量,就会证明这些考虑是错误的。
本书将提出一些这方面的建议,包括尽量减少远程调用(它在很长时间内都被认为是优化性能的好建议)。尽管如此,还是建议读者在运用这些原则之前,在你的应用中具体试一试。同样,本书中的样例代码也有一些地方为了提高可读性而牺牲了效率。在你的系统中,需要自行决定是否进行优化。在做性能优化后,一定要与优化前进行测量对比,以确定真的得到了优化,否则,你可能只是破坏了代码的可读性。
还有一个很重要的推论:配置上的重大变化会使得某些性能优化失效。因此,在升级虚拟机、硬件、数据库或其他东西到新的版本时,必须重新确认性能优化工作的有效性。很多情况下,配置变更都会对性能优化有影响,有时候你真的会发现,以前为了提升性能做的优化,在新环境下居然影响性能。
关于性能的另一个问题是很多术语的使用不一致。最明显的例子就是“可伸缩性”(scalability),它可能有6-7种含义。下面我使用其中一些术语。
响应时间是系统完成一次外部请求处理所需要的时间。这些外部请求可能是用户交互行为,例如按下一个按钮,或是服务器API调用。
响应性不同于请求处理,它是系统响应请求的速度有多快。这个指标在许多系统里非常重要,因为对于一些系统而言,如果其响应性太慢,用户将难以忍受——尽管其响应时间可能不慢。如果在请求处理期间,系统一直处于等待状态,则系统的响应性和响应时间是相同的。然而,如果能够在处理真正完成之前就给用户一些信息表明系统已经接到请求,则响应性就会好一些。例如,在文件拷贝过程中,为用户提供一个“进度条”,将会提高用户界面的响应性,但并不会提高响应时间。
等待时间是获得系统任何形式响应的最小时间,即使应该做的工作并不存在。通常它是远程系统中的大问题。假设我们让程序什么都不做,只是调用返回即可,则如果在本机上运行程序,一般都会立即得到响应。但是,如果在远程计算机上运行程序,情况就不一样,往往需要数秒的时间才能得到响应。因为从发出请求到得到响应的数秒时间主要用于排除使信息在线路上传输的困难。作为应用开发者,我经常对等待时间无能为力。这也是为什么要尽量避免远程调用的原因。
吞吐率是给定时间内能够处理多大的请求量。如果考察的是文件拷贝,则吞吐率可以用每秒字节量来表示。对于企业应用来说,吞吐率通常用每秒事务数(tps)来度量。这种方法的一个问题是指标依赖于事务的复杂程度。对于特定系统的测试,应该选取普通的事务集合。
在这里,性能或指吞吐率,或者指响应时间,由用户自己决定。当通过某种优化技术后,使得系统的吞吐率提高了,但是响应时间下降了,这时就不好说系统的性能提高了,最好用更准确的术语表示。从用户角度而言,响应性往往比响应时间更重要,因此,为了提高响应性而损失一些响应时间或者吞吐率是值得的。
负载是关于系统当前负荷的表述,也许可以用当前有多少用户与系统相连来表示。负载有时也作为其他指标(如响应时间)的背景。因此,我们可以说:在10个用户的情况下,请求响应时间是0.5秒,在20个用户的情况下,请求响应时间是2秒。
负载敏感度是指响应时间随负载变化的程度。假设:系统A在10~20个用户的情况下,请求响应时间都是0.5秒;系统B在10个用户的情况下,请求响应时间是0.2秒,在20个用户的情况下,请求响应时间上升到2秒。此时,系统A的负载敏感度比系统B低;我们还可以使用术语衰减(degradation),称系统B衰减得比系统A快。
效率是性能除以资源。如果一个双CPU系统的性能是30tps,另一个系统有4个同样的CPU,性能是40tps,则前者效率高于后者。
系统的容量是指最大有效负载或吞吐率的指标。它可以是一个绝对最大值或性能衰减至低于一个可接受的阈值之前的临界点。
可伸缩性度量的是向系统中增加资源(通常是硬件)对系统性能的影响。一个可伸缩性的系统允许在增加了硬件后,能够有性能上的合理提高。例如,为了使吞吐率提高一倍,要增加多少服务器等。垂直可伸缩性或称垂直延展,通常指提高单个服务器的性能,例如增加内存。水平可伸缩性或称水平延展,通常指增加服务器的数目。
问题是,设计决策对所有性能指标的作用并不相同。比如,某个服务器上运行着两个软件系统:Swordfish的容量是20tps,而Camel的容量是40tps。哪一个的性能更高?哪一个的可伸缩性好?仅凭这些数据,我们无法回答关于可伸缩性的问题,我们只能说Camel系统在单片机上的效率更高。假设又增加了一台服务器后,我们发现:Swordfish的容量是35tps,Camel的容量是50tps。尽管Camel的容量仍然大于Swordfish,但是后者在可伸缩性上却显得比前者更好。假设我们继续增加服务器数目后发现:Swordfish每增加一台服务器提高15tps,Camel每增加一台服务器提高10tps。在获得了这些数据后,我们才可以说,Swordfish的水平可伸缩性比Camel好,尽管Camel在5个服务器以下会有更好的效率。
当构建企业应用系统时,关注硬件的可伸缩性往往比关注容量或效率更重要。如果需要,可伸缩性可以给予你获得更好性能的选择,可伸缩性也可以更容易实现。有时,设计人员费了九牛二虎之力才提高了少许容量,其开销还不如多买一些硬件。换句话说,假设Camel的费用比Swordfish高,高出的部分正好可以买几台服务器,那么选择Swordfish可能更合算,尽管你目前只需要40tps。现在人们经常抱怨软件对硬件的依赖性越来越大,有时为了运行某些软件就不得不对硬件进行升级,就像我一样,为了用最新版本的Word,就必须不断地升级笔记本电脑。但是总的来说,购买新硬件还是比修改旧软件来得便宜。同样,增加更多的服务器也比增加更多的程序员来得便宜——只要你的系统有足够的可伸缩性。
0.5 模式
模式的概念早就有了。我在这里不想把这段历史重新演绎一遍。只是想简单谈谈我对模式和它们为什么是描述设计的重要手段的一些看法。
模式没有统一的定义。可能最好的起点是Christopher Alexander给出的定义(这也是许多模式狂热者的灵感来源):“每一个模式描述了一个在我们周围不断重复发生的问题以及该问题解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”[Alexander et al.]。尽管Alexander是建筑家,他谈论的是建筑模式,但其定义也能很好地适用于软件业。模式的核心就是特定的解决方案,它有效而且有足够的通用性,能解决重复出现的问题,模式的另一种视角是把它看成一组建议,而创造模式的艺术则是将很多建议分解开来,形成相互独立的组,在此基础上可以相对独立地讨论它们。
模式的关键点是它们源于实践。必须观察人们的工作过程,发现其中好的设计,并找出“这些解决方案的核心”。这并不是一个简单的过程,但是一旦发现了某个模式,他将是非常有价值的。对于我来说,价值之一是能够撰写这样一本参考书。你不必通读本书的全部内容,也不必通读所有有关于模式的书。你只需要了解到这些模式都是干什么的,它们解决什么问题,它们是如何解决问题的,就足够了。这样,一旦碰到类似问题,就可以从书中找出相应的模式。那时,再深入了解相应的模式也不迟。
一旦需要使用模式,就必须知道如何将它运用于当前的问题。使用模式的关键之一是不能盲目使用,这也是模式工具为什么都那么惨的原因。我认为模式是一种“半生不熟品”,为了用好它,还必须在自己的项目中把剩下的那一半“火候”补上。我本人每次在使用模式时,都会东改一点西改一点。因此你会多次看到同一解决方案,但没有一次是完全相同的。
每个模式相对独立,但又不彼此孤立。有时候它们相互影响,如影随形。例如,如果在设计中使用了领域模型,那么经常还会用到类表继承。模式的边界本来也是模糊的,我在本书中也尽量让它们各自独立。如果有人说“使用工作单元”,你就可以直接去看工作单元这个模式如何使用,而不必阅读全书。
如果你是一个有经验的企业应用设计师,也许会对大多数模式都很熟悉。希望本书不会给你带来太大的失望。(实际上我在前言里面已经提醒过了。)模式不是什么新鲜概念。因此,撰写模式书籍的作者们也不会声称我们“发明”了某某模式,而是说我们“发现”了某某模式。我们的职责是记录通用的解决方案,找出其核心,并把最终的模式记录下来。对于一个高级设计师,模式的价值并不在于它给予你一些新东西,而在于它能帮助你更好地交流。如果你和你的同事都明白什么是远程外观,你就可以这样非常简洁地交流大量信息:“这个类是一个远程外观模式。”也可以对新人说:“用数据传输对象模式来解决这个问题。”他们就可以查找本书来搞清楚如何做。模式为设计提供了一套词汇,这也是为什么模式的名字这么重要的原因。
本书的大多数模式是用来解决企业应用的,基本模式一章(见第18章)则更通用一些。我把它们包含进来的原因是:在前面的讨论中,我引用了这些通用的模式。
0.5.1 模式的结构
每个作者都必须选择表达模式的形式。一些人采用的表达基于模式的一些经典教材如[Alexander et al.]、[Gang of Four]或[POSA]。另一些人用他们自己的方式。我在这个问题上也斟酌了很久。一方面我不想象GOF一样太精炼,另一方面我还要引用他们的东西。这就形成了本书的模式结构。
第一部分是模式的名字。模式名非常重要,因为模式的目的之一就是为设计者们交流提供一组词汇。因此,如果我告诉你Web服务器是用前端控制器和转换试图构建的,而你又了解这些模式,那么你对我的Web服务器的架构就会非常清楚了。
接下来的两部分是相关的:意图和概要。意图用一两句话总结模式;概要是模式的一种可视化表示,通常是(但不总是)一个UML图。这主要是想给模式一个简单的概况,以帮助记忆。如果你对模式已经“心知肚明”,只是不知道它的名字,那么模式的意图和概要这两部分就能为你提供足够的信息。
接下来的部分描述了模式的动机。这可能不是该模式所能解决的唯一问题,但却是我认为最具代表性的问题。
“运行机制”部分描述了解决方案。在这一部分,我会讨论一些实现问题以及我遇到的变化情况。我会尽可能独立于平台来讨论——也有一个部分是针对平台来讨论的,如果不感兴趣可以跳过这部分。为了便于解释,我用了一些UML图来辅助说明。
“使用动机”部分描述了模式何时被使用。这部分讨论是使我选择该模式而不是其他模式的权衡考虑。本书中很多模式都可以相互替代,例如页面控制器和前端控制器可以相互替代。很少有什么模式是非它不可的。因此,每当我选择了一种模式之后,我总是问自己“你什么时候不用它?”这个问题也经常驱使我选择其他方案。
“进一步阅读”部分给出了与该模式相关的其他读物。它并不完善。我只选择我认为有助于理解模式的参考文献,所以我去掉了对本书内容没有价值的任何讨论,当然其中也可能会遗漏一些我不知道的模式。我也没有提到一些我认为可能读者无法找到的参考文献,再就是一些不太稳定的Web链接。
我喜欢为模式增加一个或几个例子。每个例子都非常简单,它们是用Java语言或C#语言编写的。我之所以选择两种语言,是因为它们可能是目前绝大多数专业程序员都能读懂的语言。必须注意,例子本身不是模式。当你使用模式时,不要想当然地认为它会和例子一样,也不要把例子看成某种形式的宏替换。我把例子编得尽量简单以突出其中模式相关的部分。当然,省略的部分并不是不重要,只是它们一般都特定于具体环境,这也是为什么模式在使用时一般都必须做适当调整的原因。
为了尽量使例子简单但是又能够突出核心意思,我主要选择那些简单而又明确的例子,而不是那些来自于系统中的复杂例子。当然,在简单和过分之间掌握平衡是不容易的,但是我们必须记住:过分强调具体应用环境反而会增加模式的复杂性,使得模式的核心内容不易理解。
这就是为什么我在选择例子时选取的是一些相互独立的例子而不是相互关联的例子的原因。独立的例子有助于对模式的理解。但是在如何将这些模式联合在一起使用上却支持不多。相互关联的例子则相反,它体现了模式间是如何相互作用的,但是对其中每个模式的理解却依赖于对其他所有模式的理解。理论上,是可以构造出既相互关联又相互独立的例子,但这是一项非常艰巨的工作——至少对于我来说是这样。因此,我选择了相互独立的例子。
例子中的代码本身也主要用来增强对思想的理解。因此,在其他一些方面考虑可能不够——特别是错误处理,在这方面,我没有花费很多笔墨,因为到目前为止,我还没有得出错误处理方面的模式。在此,那些代码纯粹用来说明模式,而并不是用来显示如何对任何特定的业务问题进行建模。
正是由于这些原因,我没有把这些代码放到我的网站上供大家下载。为了让那些基本的思想在应用设置下有所意义,本书的每个样例代码都充满着太多的“脚手架”来简化它们。
并不是每个模式中都包含上面所述的各个部分。如果我不能想出很好的例子或动机等内容,我就会把相应部分省略。
0.5.2 模式的局限性
正如我在前言中所述,对于企业应用开发而言,本书介绍的模式并不全面。我对本书的要求,不在于它是否全面,而在于它是否有用。模式这个领域太大了,单凭一个人的头脑是无法做到面面俱到的,更不用说是一本书了。
本书中所列的模式都是我在具体领域中遇到的,但这并不表明我已经理解了每一个模式以及它们之间的关系。本书的内容只是反映了我在写书时的理解,在编写本书的过程中,我对相关内容的理解也不断发展和加深,当然,在本书发表之后,我仍然希望本人对模式的理解还能够继续发展。对于软件开发而言,有一点是可以肯定的,那是软件开发永远不会停止。
当你使用模式时请记住:它们只是开始,而不是结束。任何作者去囊括项目开发中的所有变化和技术是不可能的。我编写本书的目的也只是作为一个开始,希望它能够把我自己的和我所了解的经验和教训传递给读者,你们可以在此基础上继续努力。请大家记住:所有模式都是不完备的,你们都有责任在自己的系统中完善它们,你们也会在这个过程中得到乐趣。
第1章 分层
在分解复杂的软件系统时,软件设计者用得最多的技术之一就是分层。在计算机本身的架构中,可以看到:到处都有分层的例子:不同的层从包含了操作系统调用的程序设计语言,到设备驱动程序和CPU指令集,再到芯片内部的各种逻辑门。网络互联中, FTP层架构在TCP之上, TCP架构在IP之上, IP又架构在以太网之上。
当用分层的观点来考虑系统时,可以将各个子系统想像成按照“多层蛋糕”的形式来组织,每一层都依托在其下层之上。在这种组织方式下,上层使用了下层定义的各种服务,而下层对上层一无所知。另外,每一层对自己的上层隐藏其下层的细节。因此,第4层使用第3层的服务,第3层使用第2层的服务,第4层无需知道第2层的细节。(当然,并非所有的分层架构都这么隔绝,但绝大多数是不透明的,或至少是几乎不透明的。)
将系统按照层次分解有很多重要的好处:
• 在无需过多了解其他层次的基础上,可以将某一层作为一个有机整体来理解。例如,无需知道以太网的工作细节,你照样可以在TCP上构建FTP服务。
• 可以替换某层的具体实现,只要前后提供的服务相同即可。例如, FTP服务无论是在以太网、 PPP上、还是网络运营商使用的任何网络上都无需改变,而且与提供传输电缆的网络
运营商无关。
• 可以将层次间的依赖性减到最低。假设网络运营商改变了物理传输系统,但只要IP层不变,FTP服务就可以不改变。
• 分层有利于标准化工作。 TCP和IP就是关于它们各自层次如何工作的标准。
• 一旦构建好了某一层次,就可以用它为很多上层服务提供支持。因此, TCP/IP同时被FTP、telnet、 SSH和HTTP使用。否则,所有这些高层协议都必须编写它们各自的底层协议。分层是一种重要的技术,但也有缺陷:
• 层次并不能封装所有东西。有时它会为我们带来级联修改。最经典的例子就是在一个分层设计的企业应用中,如果要增加一个在用户界面上显示的数据域,就必须在数据库中增加相应的字段,还必须在用户界面和数据库之间的每一层做相应的修改。
• 过多的层次会影响性能。在每一层,一般都会从一种表现形式转换到另一种。不过底层功能的封装通常带来比代价更大的效率提升。例如,可以优化事务控制层,提高其他各层的效率。
然而,分层架构中最困难的问题是决定建立哪些层次以及每一层的职责是什么。
1.1 企业应用中层次的演化
我虽然没有从事过早期批处理系统时期的任何工作,但我认为当时的软件工作人员不会太
关注层次的概念,只要编写操作某些文件( ISAM、 VSAM等)格式的程序,这就是当时的应用。
它不需要层次。
20世纪90年代,随着客户/服务器系统的出现,分层的概念更明显了。这样的系统是一种两
个层次的系统:客户端包括用户界面和其他应用代码,服务器端通常是关系型数据库。常见的
客户端工具如VB、 PowerBuilder和Delphi。这些工具使得构建数据密集型应用非常容易。因为
它们的用户界面控件通常都是SQL感知的。因此,可以通过将控件拖拽到“设计区域”来建立
界面,然后再使用属性表单把控件连接到后台数据库。
如果应用仅仅包括关系数据的简单显示和修改,那么这种客户/服务器系统的工作方式非常
合适。问题来自领域逻辑:如业务规则、验证、计算等。通常,人们会把它们写在客户端,但
是这样很笨拙,并且往往把领域逻辑直接嵌入到用户界面。随着领域逻辑的不断复杂化,这些
代码将越来越难以使用。而且,这样做很容易产生冗余代码,这意味着简单的变化都会导致要
在很多界面中寻找相似代码。
另外一种办法是把这些领域逻辑放到数据库端,作为存储过程。但是,存储过程只提供有
限的结构化机制,这将再次导致笨拙的代码。而且,很多人喜欢关系型数据库的原因之一是
SQL是一个标准,允许他们更换数据库厂商。尽管真正更换数据库厂商的用户寥寥无几,但还
是有很多人希望拥有这种选择,并且没有太大的附加代价。由于存储过程都是数据库厂商私有
的,因此普通用户被剥夺了这种选择权。
在客户/服务器方式逐渐大众化的同时,面向对象方式开始崛起。面向对象为领域逻辑的问
题找到了答案:转到三层架构的系统。在这种方式下,在表现层实现用户界面,在领域层实现
领域逻辑,在数据源层存取数据。这种方式使你可以将复杂的领域逻辑从界面代码中抽取出来,
单独放到中间层,用对象加以建模和组织。
尽管有这些优势,但一开始面向对象的进展并不大。当时的实际情况是:大多数系统并不
特别复杂,或者至少在构建之初没有那么复杂。因此,当系统比较简单时,相对于三层架构的
优势,强有力的客户/服务器工具的竞争力非常大。但客户/服务器工具很难甚至无法应用于三层
架构系统的配置。
我认为真正巨大的冲击来自Web的兴起。人们忽然想在Web浏览器上部署这些客户/服务器
应用。然而,如果所有的领域逻辑都是写在“胖”客户中,则所有这些都必须在Web界面中重写。
对于设计良好的三层系统来说,只需要增加一个新的表现层,就可以了。另外, Java的出现使得
面向对象语言无所顾忌地向当时的主流技术发起冲击。用于构建Web页面的工具对SQL的绑定也
没有那么紧密了,这也使得它们比较容易适应三层结构。
当人们讨论分层时,常常不容易区分layer和tier。这两个词汇经常被用作同义词,但是很多
人还是认为tier意味着物理上的分离。客户/服务器系统常常被称为“two-tier system”,其分离是
物理上的分离:客户端是一台台式机,而服务器端是一台服务器。我使用layer,旨在强调无需
把不同的层次放在不同的计算机上运行。独立出来的领域逻辑层,既可以运行在台式计算机上,
也可以运行在数据库服务器上。在这种情形下,有两个节点,但是有三个层次。如果数据库也
在本地,还可以在一台笔记本电脑上运行三层软件,当然,仍旧存在三个截然不同的层次。
1.2 三个基本层次
本书主要就三个基本层次的架构展开讨论:表现层、领域层和数据源层(这里的命名取自
文献[Brown et al.])。表1-1总结了这些层次。
表1-1 三个基本层次
层 次 职 责
表现层 提提供服务,显示信息(例如在Windows或HTML页面中,处理
用户请求(鼠标点击,键盘敲击等), HTTP请求,命令行调用,
批处理API)
领域层 提逻辑,系统中真正的核心
数据源层 提与数据库、消息系统、事务管理器及其他软件包通信
表现逻辑处理用户与软件间的交互。可能简单到只是命令行或基于文本的菜单系统,但是
当前的客户界面往往是功能完善的胖客户图形界面,或者是基于HTML的浏览器界面(本书中的
“胖客户”是指Windows/Swing/fat-client用户界面,不包括HTML浏览器)。表现层的主要职责是
向用户显示信息并把从用户那里获取的信息解释成领域层或数据源层上的各种动作。
数据源逻辑主要关注与其他系统的交互,这些系统将代表应用完成相关的任务。它们可以
是事务监控器、其他应用、消息系统等。对于大多数企业应用来说,最主要的数据源逻辑就是
数据库,它的主要责任是存储持久数据。
最后一部分就是领域逻辑,也称为业务逻辑。它就是应用必须做的所有领域相关工作:包
括根据输入数据或已有数据进行计算,对从表现层输入的数据进行验证,以及根据从表现层接
收的命令来确定应该调度哪些数据源逻辑。
有时,层次组织成领域层对表现层完全隐藏了数据源层。但更多的时候,是表现层直接对
数据存储进行操作。虽然这样做并不纯粹,但是在实践中往往运行良好。表现层可能解释来自
用户的命令,通过数据源层将相关数据从数据库中提取出来,然后让领域逻辑层在向用户显示
相关数据之前先处理这些相关数据。
一个单独的企业应用,可能在上述的三个层次上都包含多个软件包。如果某个应用不仅要
支持用户通过胖客户机界面访问,还要支持用户通过命令行形式访问,则它需要两个表现层:
一个支持胖客户机界面,另一个支持命令行。对于不同的数据库,通常也需要多个数据源组件,
特别是在与已有的软件包通信时。即便是领域逻辑,也有可能被分割成相互独立的不同部分,
特定的数据源包只能由特定的领域包使用。
到目前为止,我们一直都在讨论用户。这很自然地会引出一个问题:如果驱动软件的不是
人,情况又怎么样呢?比如说驱动者可能是时髦的Web Service或是一个老土但实用的批处理程
序。对于后者,用户将是一个客户程序。这样,很明显,表现层就有可能与数据源层出现某些
相似之处,因为它们都是系统与外界的接口。这就是Alistair Cockburn的Hexagonal Architecture
模式[wiki]背后的逻辑,它将任何系统都视为由到外部系统的接口所围绕的一个核心。在
Hexagonal Architecture中,所有外部的东西都被视为外部接口。因此,从这种意义上说,它是一
种对称视图,而不是本书中的非对称分层视图。
然而,我认为这种非对称性是有益的。因为,为别人提供服务的接口与使用别人服务的接
口存在较大的差别,需要明确区分。这就是表现层和数据源层相对于核心的本质差别。表现层
是系统对外提供服务的外部接口,不管外面是复杂的人类还是一个简单的远端程序。数据源层
是系统使用外部服务的接口。这样区分的好处是:客户的不同将改变你对服务的看法。
对每个企业应用,尽管我们能够区分出其中的主要的表现层、领域层和数据源层,但是具
体如何分离要取决于应用的复杂程度。 从数据库中读取数据并把它显示在Web页面上的简单脚本,
可能全部在一个过程中。我将仍然尽量保持三层架构的风格,不过在这里可能只是把每个层的
行为放到三个不同的子程序中。一旦系统再复杂一点,就可以将三个层次的工作分解成不同的
类。如果复杂度继续增加,则把类分配到不同的包中。我的总体建议就是根据不同的问题,选
择一种适合的分离方式,但是切记一定要进行某种形式的分离—至少在子程序级别。
伴随着分离,还有一条关于依赖性的普遍原则:领域层和数据源层绝对不要依赖于表现层。
也就是说,在领域层和数据源层的代码中,不要出现调用表现层代码的情况。这条规则将简化
在相同的基础上替换表现层的代价,也使得表现层的修改所带来的连锁反应尽可能小。领域层
与数据源层的关系更复杂,其取决于数据源层的架构模式。
使用领域逻辑时,其中一个最困难的部分就是区分什么是领域逻辑,什么是其他逻辑。一
种不太正规的测试办法就是:假想向系统中增加一个完全不同的新层,例如为Web应用增加一个
命令行界面层。如果在这个过程中,发现需要重复实现某些功能,则说明可能有一些本应该在
领域层实现的逻辑,现在在表现层实现了。类似地,你也可以假想一下,将后台数据库更换成
XML文件格式,看看情况又会如何?
举一个例子。我所知道的一个系统有一张产品列表,其中,当月销售量比上月销售量大10%
的产品需要用红色显示。为实现这个功能,开发者在表现层逻辑中比较当月和上月的销售量,
然后将差别大于10%的产品显示为红色。
这样做的麻烦就是将领域逻辑放到了表现层中。为了进行适当的分离,需要在领域层中定
义一个方法,用来指示该产品的销售量是否较上月有较大提高。该方法完成销售量的比较,返
回一个布尔值。表现层则只需要简单地调用一下这个布尔方法,如果返回值为真,则用红色突
出显示这个产品。这样,该过程就分解成两部分:确定需不需要突出显示,选择如何突出显示。
当然,我担心这样也许有些太教条主义了。 Alan Knight在审阅本书时评论说:他自己“很
头大,将部分领域逻辑混入表现层到底是滑向地狱的第一步呢,还是只有少数纯粹主义者才会
抱怨的小问题?”我们之所以担心,正是因为这种做法两者兼备。
1.3 为各层选择运行环境
本书绝大多数篇幅讨论的都是逻辑层次—将系统中各部分分离,以降低不同部分之间的
耦合程度。即使是都运行在同一台计算机上,不同层次间的分离也是非常重要的。当然,系统
物理结构的不同会有所影响。
对于大多数信息系统来说,主要的决定就是在哪里运行处理工作,是在客户机上,还是在
台式机上,又或是在服务器上?
通常,最简单的情况是将所有东西都运行在服务器上。在这种情况下,一个使用Web浏览器
的HTML前端是一个好方法。这样做最大的好处是所有的东西都在有限的环境内,很容易修改维
护。无需考虑将它们分发到不同的客户端并维护与服务器的同步等问题。也不必考虑与客户机
上其他软件的兼容性问题。
在客户机上运行应用程序的好处是系统的响应性好,或者在网络断开的情况下也能正常工
作。任何运行在服务器上的逻辑在响应客户请求时,都需要一个来回的通信开销。如果用户仅
仅是为了试试系统的即时反馈,这个通信来回也无法避免。它还需要在网络连接保持的状态下
运行。当然,网络的分布可能会无所不在,但是至少在我写本书的时候, 31000英尺高的地方还
没有。也许在不久的将来,就会到处都有了,但有不少人需要立即工作,不必等待网络连接。
断接操作带来特别的挑战性,我不想在本书中过多讨论。
有了这些约束,我们就可以逐层分析了。数据源层一般都是运行在服务器上。例外情况是:
当需要断接操作时,可以将服务器的功能复制到一台功能强大的客户机上。在这种情况下,在
离线的客户机上对数据源的任何修改,都需要同步到服务器上。正如我前面提到的那样,关于
这方面的讨论,我想留到以后某个时候或留给另一位作者。
在何处运行表现层主要取决于用户界面的种类。如果运行的是一个胖客户,则意味着表现
层运行在客户端。如果运行的是一个Web界面,则意味着表现层运行在服务器端。当然,也有例
外—例如,客户软件(如Unix中的X servers)的远程操作在台式机上运行了一个Web服务器—当
然,这是极少数情况。
如果要建立一个B2C系统,就没什么选择了。无论是谁都可能访问你的服务器,你也不想因
为客户用的是TRS-80系统,就把他拒之门外。在这种情况下,可以在服务器上完成所有工作,
并提供一个HTML界面给浏览器。这样做的缺点就是每个操作都必须要一个来回的通信开销,可
能会影响响应时间。可以通过可下载的applet或浏览器脚本来缓解问题,但是它们同时会带来浏
览器兼容性等其他问题。使用的HTML越纯粹,事情就会变得越简单。
即使你们的每一台台式机都是由你们公司的信息系统部门手工精心搭建的,这种简单性仍
然非常诱人。因为即使是非常简单的胖客户系统,也会遇到维护客户端一致性以及避免各种软
件不兼容等问题。
人们希望有胖客户表现层的主要原因是:有些任务对于用户而言太复杂了,为了有一个可
用的应用系统,它们的需要超出了Web GUI的能力。当然,人们已经在逐渐习惯于采用使Web前
端更可用的各种方法,这些方法减少了对胖客户方式的需求。因此,我非常赞同只要有可能就
用Web表现方式,只有在必需的情况下才使用胖客户方式。
剩下来的就是领域逻辑了。领域逻辑可以全都运行于服务器端,也可以全都运行于客户端,
也可以一分为二。再有,全部运行在服务器端有助于系统维护,向客户端转移是为了响应时间
及断接使用的需要。
如果你必须在客户端运行某种逻辑,可以考虑将所有逻辑都运行在客户端—这样至少保
证了相关的东西都在一起。这样,胖客户端和Web服务器联合部署在客户机上,对响应性的改善
不会太大,但是它可以作为一种处理断接操作的办法。在这种情况下,可以通过不同的模块将
领域逻辑与表现层分开,可以使用事务脚本或领域模型。将所有的领域逻辑放在客户端的问题
是升级和维护代价高。
将领域逻辑分割在客户端和服务器端,应该是最差的选择,因为这样做无法确定任意一块
逻辑到底在哪。采用这种方式的主要原因是:只有一小部分领域逻辑需要在客户端完成。其中
的诀窍就是将这一小部分独立出来成为自包含的模块,使得它不依赖于系统的任意其他部分。
这样,就可以在客户端或服务器端运行它了。这将需要采用一些烦人的小技巧,但它的确是一
个解决问题的好办法。
一旦选择了处理节点,接下来就应该尽可能使所有代码保持在单一进程内完成(可能是在
同一个节点上,也可能拷贝在集群中的多个节点上)。除非不得已,否则不要把层次放在多个进
程中完成。因为那样做不但损失性能,而且增加复杂性,因为必须增加类似下面的模式,如远
程外观和数据传输对象。
以下因素被Jens Coldewey称为复杂性增压器( complexity booster):分布、显式多线程、范
型差异(例如对象/关系)、多平台开发以及极限性能要求(如每秒100个事务以上)。所有这些因
素都会带来很大的代价。当然,有时我们无法回避它们,但是要切记:这里罗列的每一项都会
为开发和运行维护阶段带来开销。
——选自:《企业应用架构模式》 [Patterns of Enterprise Application Architecture] [英] 福勒 著;王怀民,周斌 译