企业应用架构模式-30天阅读计划

构建计算机系统并非易事。随着系统复杂性的增大,构建相应软件的难度将呈指数增大。同其他行业一样,我们只有在不断的学习中进步,从成功经验中学习,从失败教训中学习,才有望克服这些困难。这本书的内容就是这样一些“学习”经验。只有通过模式的总结和学习,才能更有效地与他人进行交流。—选自《企业应用架构模式》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界面目前无法达到的,需要更常规的胖客户界面。用户交互的复杂性还会带来事务行为的复杂性:签订租约可能要耗时12个小时,这期间用户要处于一个逻辑事务中。一个复杂的数据库设计方案中可能也会涉及到200多个表以及一些有关资产评估和计价的软件包。第三个例子是一家小型公司使用的简单的“开支跟踪系统”。这个系统的用户很少,功能简单,通过HTML表现方式可以很容易实现,涉及的数据源表项也不多。尽管如此,开发这样的系统也不是没有挑战。一方面你必须快速地开发出它,另一方面你又必须为它以后可能的发展考虑;也许以后会为它增加赔偿校验的功能,也许它会被集成到工资系统中,也许还要增加关于税务的功能,也许要为公司的CFO生成汇总报表,也许会被集成到一个航空订票Web Service中,等等。如果在这个系统的开发中,也试图使用前面两个例子中的一些架构,可能会影响开发进度。如果一个系统会带来业务效益(如所有的企业应用应该的那样),则系统进度延误同样也是开销。如果现在不做决策又有可能影响系统未来的发展。但是,如果现在就考虑了这些灵活性但是考虑不得当,额外的复杂性又可能会影响到系统的发展,进一步延误系统部署,减少系统的效益。虽然这类系统很小,但是一个企业中往往有很多这样的系统,这些系统的架构不良性累积起来,后果将会非常可怕。这三个企业应用的例子都有难点,而且难点各不相同。当然,也不可能有一个适合于三者的通用架构。选择架构时,必须很清楚地了解面临的问题,在理解的基础上再来选择合适的设计。本书中也没有一个通用的解决方案。实际上,很多模式仅仅是一些可选方案罢了。即使你选择了某种模式,也需要进一步根据面临的问题来修改模式。在构建企业应用时,你不思考是不行的。所有书本知识只是给你提供信息,作为你做决定的基础。模式是这样,工具也同样如此。在系统开发时应该选取尽可能少的工具,同时也要注意,不同的工具擅长处理的方面也不同,切记不要用错了工具,否则只会事倍功半。0.4 关于性能的考虑很多架构的设计决策和性能有关。对于大多数与性能相关的问题,我的办法是首先建立系统,调试运行,然后通过基于测量的严格的优化过程来提高性能。但是,有一些架构上的决策对性能的影响,可能是后期优化难以弥补的。而且即使这种影响可以在后期很容易地弥补,参与这个项目的人们任然会从一开始就担心这些决策。在这样的一本书中讨论性能通常很困难。这是因为“眼见为实”:所有那些关于性能的条条框框,不在你的具体系统中配置运行一下,是很难有说服力的。我也经常看到一些设计方案因为性能方面的考虑而被接受或拒绝,但是一旦有人在真实的设置环境中做一些测量,就会证明这些考虑是错误的。本书将提出一些这方面的建议,包括尽量减少远程调用(它在很长时间内都被认为是优化性能的好建议)。尽管如此,还是建议读者在运用这些原则之前,在你的应用中具体试一试。同样,本书中的样例代码也有一些地方为了提高可读性而牺牲了效率。在你的系统中,需要自行决定是否进行优化。在做性能优化后,一定要与优化前进行测量对比,以确定真的得到了优化,否则,你可能只是破坏了代码的可读性。还有一个很重要的推论:配置上的重大变化会使得某些性能优化失效。因此,在升级虚拟机、硬件、数据库或其他东西到新的版本时,必须重新确认性能优化工作的有效性。很多情况下,配置变更都会对性能优化有影响,有时候你真的会发现,以前为了提升性能做的优化,在新环境下居然影响性能。关于性能的另一个问题是很多术语的使用不一致。最明显的例子就是“可伸缩性”(scalability),它可能有6-7种含义。下面我使用其中一些术语。响应时间是系统完成一次外部请求处理所需要的时间。这些外部请求可能是用户交互行为,例如按下一个按钮,或是服务器API调用。响应性不同于请求处理,它是系统响应请求的速度有多快。这个指标在许多系统里非常重要,因为对于一些系统而言,如果其响应性太慢,用户将难以忍受——尽管其响应时间可能不慢。如果在请求处理期间,系统一直处于等待状态,则系统的响应性和响应时间是相同的。然而,如果能够在处理真正完成之前就给用户一些信息表明系统已经接到请求,则响应性就会好一些。例如,在文件拷贝过程中,为用户提供一个“进度条”,将会提高用户界面的响应性,但并不会提高响应时间。等待时间是获得系统任何形式响应的最小时间,即使应该做的工作并不存在。通常它是远程系统中的大问题。假设我们让程序什么都不做,只是调用返回即可,则如果在本机上运行程序,一般都会立即得到响应。但是,如果在远程计算机上运行程序,情况就不一样,往往需要数秒的时间才能得到响应。因为从发出请求到得到响应的数秒时间主要用于排除使信息在线路上传输的困难。作为应用开发者,我经常对等待时间无能为力。这也是为什么要尽量避免远程调用的原因。吞吐率是给定时间内能够处理多大的请求量。如果考察的是文件拷贝,则吞吐率可以用每秒字节量来表示。对于企业应用来说,吞吐率通常用每秒事务数(tps)来度量。这种方法的一个问题是指标依赖于事务的复杂程度。对于特定系统的测试,应该选取普通的事务集合。在这里,性能或指吞吐率,或者指响应时间,由用户自己决定。当通过某种优化技术后,使得系统的吞吐率提高了,但是响应时间下降了,这时就不好说系统的性能提高了,最好用更准确的术语表示。从用户角度而言,响应性往往比响应时间更重要,因此,为了提高响应性而损失一些响应时间或者吞吐率是值得的。负载是关于系统当前负荷的表述,也许可以用当前有多少用户与系统相连来表示。负载有时也作为其他指标(如响应时间)的背景。因此,我们可以说:在10个用户的情况下,请求响应时间是0.5秒,在20个用户的情况下,请求响应时间是2秒。负载敏感度是指响应时间随负载变化的程度。假设:系统A在1020个用户的情况下,请求响应时间都是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 模式的局限性正如我在前言中所述,对于企业应用开发而言,本书介绍的模式并不全面。我对本书的要求,不在于它是否全面,而在于它是否有用。模式这个领域太大了,单凭一个人的头脑是无法做到面面俱到的,更不用说是一本书了。本书中所列的模式都是我在具体领域中遇到的,但这并不表明我已经理解了每一个模式以及它们之间的关系。本书的内容只是反映了我在写书时的理解,在编写本书的过程中,我对相关内容的理解也不断发展和加深,当然,在本书发表之后,我仍然希望本人对模式的理解还能够继续发展。对于软件开发而言,有一点是可以肯定的,那是软件开发永远不会停止。当你使用模式时请记住:它们只是开始,而不是结束。任何作者去囊括项目开发中的所有变化和技术是不可能的。我编写本书的目的也只是作为一个开始,希望它能够把我自己的和我所了解的经验和教训传递给读者,你们可以在此基础上继续努力。请大家记住:所有模式都是不完备的,你们都有责任在自己的系统中完善它们,你们也会在这个过程中得到乐趣。——选自:《企业应用架构模式》 [Patterns of Enterprise Application Architecture] [英] 福勒 著;王怀民,周斌 译

January 23, 2019 · 1 min · jiezi

BFF初探

一、当前开发遇到的常见“痛点”在前后端联调时,有些麻烦出现的频率不低而且可能会较大程度影响开发效率,其中就包括前后端对接口数据格式设计的差异。两者一是基于领域模型,一是基于用户交互,因此设计出来的数据结构经常有差异,使得前端从接口取到数据后还需要多做一层“数据规格化”(我自己的称呼…)的工作。举两个例子:命名习惯不一致,例如有这样的一个列表数组ranks:[{ id: 1, value: ‘DUCHY’},{ id: 2, value: ‘KINGDOM’},{ id: 3, value: ‘EMPIRE’}]页面的渲染组件需要value这一属性名,但接口数据使用的是name,那么就需要做一个遍历,手动修改属性名。类似的情况还有(null、’’)/([]、{})的转换等等,这都是为了数据格式所做的额外操作,与业务逻辑并没有太大关联。为复用某些接口,需要做一些接口数据额外处理:数据对象info:{countries: [ { name: ‘Austria’, cities: [ ‘Vienna’, ‘Tirol’ ] }, { name: ‘Persia’, cities: [ ‘Isfahan’, ‘Shiraz’ ] }, { name: ‘United States’, cities: [ ‘San Francisco’, ‘Mountain View’ ] }]}现在有一个场景,我只想要countries数组的第一项(或者说,在特定场景下只有第一项是有意义的),那么我如果复用这一接口拿到的数据,每次就都要做一个let specifiedCountry = countries[0]的默认赋值,在更复杂的场景下这种赋值可能嵌套更深、重复次数更多。显然,处理数据格式与处理交互时的数据变化应该分离,这样前端会有更多精力去处理交互的业务逻辑。二、对GraphQL的探索及其应用要应对这一需求,当下的GraphQL是一个不错的方案,用它可以做到指定一个请求格式,然后获取所需的数据,同时它也支持一些逻辑判断和抽象,如directive、Fragment、Variable等等,以下取这三个作为例子,演示一下对于上述例子的解决方案:对于(一)中的第一个例子:考虑GraphQL的alias解决方案:GraphQL的别名alias设计目的是在同一个Type下可以返回多个对象而不发生命名冲突,不过我们也可以用它做一下name -> value 的重命名:ranks {idvalue: name}*嵌套的别名是否可行未知,还需要做一下验证对于(一)中的第二个例子:使用variable和directive做一些逻辑处理:query Country($isFirst: Boolean!) {info(episode: $episode) { countries @include(if: $isFirst) { name cities }}} 数据模型中,第一个元素包含isFirst: true即可(这里可能还要深究一下,isFirst如何设置才能真正解决原来的问题,或者说需要别的判断方式)三、BFF的定位及node.js在BFF层能做的事情BFF的应用场景有很多,聚合后端接口,提供给第三方api都是它可以负责的工作。聚合后端接口在上文已经有了类似的操作,不过做的不是聚合几个接口而是对某个接口做了额外处理。BFF层的设计一般来说可以更好地满足产品快速迭代的需求,因为它将UI交互与部分服务都交给了一个team(可以是Frontend)负责,这样可以大大减少不同team的沟通协调成本。node.js也可以做一部分在BFF层的数据加密(放BFF层合适吗?)、请求转发(需要和nginx做一下对比)也可以做一些性能优化的工作(依然要对比以往服务端的解决方案)性能优化高并发与负载均衡:常见的情况下,高并发的性能制约包括了大量的I/O操作时CPU利用率较低,而node.js在处理I/O密集型操作时有自己的优势。在负载均衡方面,nginx有几套常用的请求分配方案,也有shared memory的解决方案,并且在保证会话一致性上有较好的表现。node.js的clientRequest对象也会维护一个header queue,可以对请求的流程做一定的控制。

December 11, 2018 · 1 min · jiezi

高级 Vue 组件模式 (4)

04 使用 slot 替换 mixin目标在第三篇文章中,我们使用 mixin 来抽离了注入 toggle 依赖项的公共逻辑。在 react 中,类似的需求是通过 HOC 的方式来解决的,但是仔细想想的话,react 在早些的版本也是支持 mixin 特性的,只不过后来将它标注为了 deprecated。mixin 虽然作为分发可复用功能的常用手段,但是它是一把双刃剑,除了它所带来的便利性之外,它还有以下缺点:混入的 mixin 可能包含隐式的依赖项,这在某些情况下可能不是调用者所期望的多个 mixin 可能会造成命名冲突问题,且混入结果取决于混入顺序使用不当容易使项目的复杂度呈现滚雪球式的增长所以是否有除了 mixin 以外的替代方案呢?答案当时也是有的,那就是使用 vue 中提供的作用域插槽特性。实现这里关于作用域插槽的知识同样不赘述了,不熟悉的读者可以去官方文档了解。我们可以在 toggle 组件模板中的 slot 标签上将所有与其上下文相关的方法及属性传递给它,如下:<div class=“toggle”> <slot :status=“status” :toggle=“toggle”></slot></div>这样,我们可以通过 slot-scope 特性将这些方法和属性取出来,如下:<template slot-scope="{status, toggle}"> <custom-button :on=“status.on” :toggle=“toggle”></custom-button> <custom-status-indicator :on=“status.on”></custom-status-indicator></template>当然,相比上一篇文章,我们需要对 custom-button 和 custom-status-indicator 组件做一些简单的更改,只需要将混入 mixin 的逻辑去掉,并分别声明相应的 props 属性即可。成果通过作用域插槽,我们有效地避免了第三方组件由于混入 toggleMixin 而可能造成的命名冲突以及隐式依赖等问题。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-4总结mixin 虽好,但是一定不要滥用,作为组件开发者,可以享受它带来的便利性,但是它对于组件调用者来说,可能会造成一些不可预料的问题,通过作用域插槽,我们可以将这种问题发生的程度降到最小,同时解决 mixin 需要解决的问题。目录github gist

October 22, 2018 · 1 min · jiezi

高级 Vue 组件模式 (5)

05 使用 $refs 访问子组件引用目标在之前的文章中,详细阐述了子组件获取父组件所提供属性及方法的一些解决方案,如果我们想在父组件之中访问子组件的一些方法和属性怎么办呢?设想以下一个场景:当前的 custom-button 组件中,有一个 input 元素我们期望当 toggle 的开关状态为开时,显示 input 元素并自动获得焦点这里要想完成目标,需要获取某个组件或者每个元素的引用,在不同的 mvvm 框架中,都提供了相关特性来完成这一点:angularjs: 可以使用依赖注入的 $element 服务Angular: 可以使用 ViewChild、ContentChild 或者 template ref 来获取引用react: 使用 ref 属性声明获取引用的逻辑在 vue 中,获取引用的方法与 react 类似,通过声明 ref 属性来完成。实现首先,在 custom-button 组件中增加一个 input 元素,如下:<input v-if=“on” ref=“input” type=“text” placeholder=“addtional messages”>注意这里的 ref=“input”,这样在组件内部,可以通过 this.$refs.input 获得该元素的引用,为了实现目标中提及的需求,再添加一个新的方法 focus 来使 input 元素获取焦点,如下:focus() { this.$nextTick(function() { this.$refs.input.focus(); });},注意这里的 this.$nextTick,正常情况下,直接调用 input 的 focus 方法是没有问题的,然而却不行。因为 input 的渲染逻辑取决于 prop 属性 on 的状态,如果直接调用 focus 方法,这时 input 元素的渲染工作很可能还未结束,这时 this.$refs.input 所指向的引用值为 undefined,继续调用方法则会抛出异常,因此我们利用 this.$nextTick 方法,将调用的逻辑延迟至下次 DOM 更新循环之后执行。同理,在 app 组件中,为 custom-button 添加一个 ref 属性,如下:<custom-button ref=“customButton” :on=“status.on” :toggle=“toggle”></custom-button>之后修改 onToggle 方法中的逻辑以满足目标中的需求,当 toggle 组件状态为开时,调用 custom-button 组件的 focus 方法,如下:onToggle(on) { if (on) this.$refs.customButton.focus(); console.log(“toggle”, on);}成果点击按钮会发现,每当开关为开时,input 元素都会显示,并会自动获得焦点。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-5总结当期望获得子元素或者子组件的引用时,切记使用 ref 和 $refs 来解决问题。文章中所举例子的交互,在实际场景中很常见,比如:当通过一个 icon 触发搜索框时,期望自动获得焦点当表单校验失败时,期望自动获得发生错误的表单项的焦点当复杂列表的筛选器展开时,期望第一个筛选单元获得焦点这几种情况下,都可以使用该模式来高效地解决问题,而不是通过使用 DOM 中的 api 或者引入 jquery 获取相关元素再进行操作。目录github gist ...

October 22, 2018 · 1 min · jiezi

高级 Vue 组件模式 (1)

写在前头去年,曾经阅读过一系列关于高级 react 组件模式的文章,今年上半年,又抽空陆陆续续地翻译了一系列关于高级 angular 组件模式的文章,碰巧最近接手了一个公司项目,前端这块的技术栈是 vue。我对于 vue 本身还是比较熟悉的,不过大多都是一些很简单的个人项目,在构建相对比较复杂的应用中缺乏实践经验,就想着也搜搜类似题材的文章,涨涨知识。结果似乎没有找到(其实也是有一些的,只不过不是和 react 和 angular 对比来写的),不如就按照 react 和 angular 这两个系列文章的思路,使用 vue 来亲自实现一次吧。由于三个框架的设计思想、语法都有比较大的区别,所以在实现过程中,均使用更符合 vue 风格的方式去解决问题,同时也提供一些对比,供读者参考,如果观点有误,还望指正。01 实现一个 toggle 组件这个系列的文章的第一篇,都会从实现一个最简单的 toggle 组件开始。在 Vue 中,我们通过 data 来声明一个 checked 属性,这个属性所控制的状态代表组件本身的开关状态,这个状态会传递给负责渲染开关变换逻辑的 switch 组件中,关于 switch 组件,这里不做过多介绍,你把它当作一个私有组件即可,其内部实现与该篇文章没有太大的关联。同时这个组件还拥有一个 on 属性,用来初始化 checked 的状态值。通过在 switch 组件注册原生 click 事件,toggle 组件还会触发一个 toggled 事件,在 App 组件中,我们会监听这个事件,并将其回传的值打印到控制台中。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-1总结toggle组件的实现是一个很典型的利用单向数据流作为数据源的简单组件:on 是单向数据源,checked 代表组件内部的开关状态通过触发 toggle 事件,将 checked 状态的变化传递给父组件目录github gist

October 21, 2018 · 1 min · jiezi

高级 Vue 组件模式 (3)

03 使用 mixin 来增强 Vue 组件目标之前一篇文章中,我们虽然将 toggle 组件划分为了 toggle-button、toggle-on 和 toggle-off 三个子组件,且一切运行良好,但是这里面其实是存在一些问题的:toggle 组件的内部状态和方法只能和这三个子组件共享,我们期望第三方的组件也可以共享这些状态和方法inject 的注入逻辑我们重复编写了三次,如果可以的话,我们更希望只声明一次(DRY原则)inject 的注入逻辑当前为硬编码,某些情况下,我们可能期望进行动态地配置如果熟悉 react 的读者这里可能马上就会想到 HOC(高阶组件) 的概念,而且这也是 react 中一个很常见的模式,该模式能够提高 react 组件的复用程度和灵活性。在 vue 中,我们是否也有一些手段或特性来提高组件的复用程度和灵活性呢?答案当然是有的,那就是 mixin。实现关于 mixin 本身的知识,这里就不做过多赘述了,不熟悉的读者可以去官方文档了解。我们通过声明一个叫作 toggleMixin 的 mixin 来抽离公共的注入逻辑,如下:export const withToggleMixin = { inject: { toggleComp: “toggleComp” }};之后,每当需要注入 toggle 组件提供的依赖项时,就混入当前 mixin,如下:mixins: [withToggleMixin]如果关于注入的逻辑,我们增加一些灵活性,比如期望自由地声明注入依赖项的 key 时,我们可以借由 HOC 的概念,声明一个高阶 mixin(可以简称 HOM ?? 皮一下,很开心),如下:export function withToggle(parentCompName = “toggleComp”) { return { inject: { [parentCompName]: “toggleComp” } };}这个 HOC mixin 可以按如下的方式使用:mixins: [withToggle(“toggle”)]这样在当前的组件中,调用 toggle 组件相关状态和方法时,就不再是 this.toggleComp,而是 this.toggle。成果通过实现 toggleMixin,我们成功将注入的逻辑抽离了出来,这样每次需要共享 toggle 组件的状态和方法时,混入该 mixin 即可。这样就解决了第三方组件无法共享其状态和方法的问题,在在线实例代码中,我实现了两个第三方组件,分别是 custom-button 和 custom-status-indicator,前者是自定义开关,使用 withToggleMixin 来混入注入逻辑,后者是自定义的状态指示器,使用 withToggle 高阶函数来混入注入逻辑。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-3总结mixin 作为一种分发 Vue 组件中可复用功能的非常灵活的方式,可以在很多场景下大展身手,尤其在一些处理公共逻辑的组件,比如通知、表单错误提示等,使用这种模式尤其有用。目录github gist ...

October 21, 2018 · 1 min · jiezi

高级 Vue 组件模式 (2)

02 编写复合组件目标我们需要实现的需求是能够使使用者通过 <toggle> 组件动态地改变包含在它内部的内容。熟悉 vue 的童鞋可能马上会想到不同的解决方案,比如使用 slot 并配合 v-if,我们这里采用另外一种方法,利用 vue 提供的 provide/inject 属性按照复合组件的思想来实现。这里简单介绍下 provide/inject 的功能,它允许某个父组件向子组件注入一个依赖项(这里的父子关系可以跨域多个层级,也就是祖先与后代),如果我们在其他 mvvm 框架对比来看的话,你可以发现其他框架也具有相同的特性,比如:angularjs: directive 中的 require 属性来声明注入逻辑Angular: 依赖注入中组件级别的注入器React: context 上下文对象想进一步了解的话,可以参考官方文档实现在 vue 中,这里我们会分别实现三个组件,依次为:toggle-button: 代表开关,用来渲染父组件的开关状态toggle-on: 根据父组件 toggle 的开关状态,渲染当状态为开时的内容toggle-off: 根据父组件 toggle 的开关状态,渲染当状态为关时的内容在上一篇文章中,我们已经实现了 toggle 组件,这里我们要做一些更改。首先,需要使用 provide 属性增加一个提供依赖的逻辑,如下:provide() { return { toggleComp: { status: this.status, toggle: this.toggle } }}这里的 status 是该组件 data 中的声明的一个可监听对象,这个对象包含一个 on 属性来代表组件的开关状态,而 toggle 则是 methods 中的一个组件方法。关于为什么这里不直接使用 on 属性来代表开关状态,而使用一个可监听对象,是因为 provide 和 inject 绑定并不是可响应的,同时官方文档也指出,这是刻意而为,所以为了享受到 vue 响应性带来的便利性,我们这里传入 status 这个可监听对象。对于其他三个组件,其内部实现逻辑十分简单,相信读者通过参考在线代码实例马上就能看懂,这里只提一下关于 inject 声明注入依赖的逻辑,如下:inject: { toggleComp: “toggleComp” }这里的 “toggleComp” 与之前的 provide 对象中声明的 key 值所对应,而 inject 对象的 key 值当前组件注入依赖项的变量名称,之后,子组件即可以通过 this.toggleComp 来访问父组件的属性与方法。成果通过复合组件的方式,我们将 toggle 组件划分为了三个更小的、职责更加单一的子组件。同时由于 toggle-on 和 toggle-off 都使用 slot 来动态地注入组件调用者在其内部包含的自定义渲染逻辑,其灵活性得到了进一步的提升,只要这三个组件是作为 toggle 组件的子组件来调用,一切都将正常运行。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-2总结通常情况下,在设计和实现职能分明的组件时,可以使用这种模式,比如 tabs 与 tab 组件,tabs 只负责 tab 的滚动、导航等逻辑,而 tab 本身仅负责内容的渲染,就如同这里的 toggle 和 toggle-button、`toggle-on、toggle-off 一样。目录github gist ...

October 21, 2018 · 1 min · jiezi

高级 Angular 组件模式 (7)

07 使用 Content Directives原文: Use Content Directives因为父组件会提供所有相关的 UI 元素(比如这里的 button),所以 toggle 组件的开发者可能无法满足组件使用者的一些附加需求,比如,在一个自定义的开关控制元素上增加 aria 属性。如果 toggle 组件能够提供一些 hooks 方法或指令给组件使用者,这些 hooks 方法或指令能够在自定义的开关元素上设置一些合理的默认值,那将是极好的。目标提供一些 hooks 方法或指令给组件使用者,使其可以与所提供的 UI 元素交互并修改它们。实现我们通过实现一个 [toggler] 指令来负责向组件使用者提供的自定义元素增加 role=“switch” 和 aria-pressed 属性。这个 [toggler] 指令拥有一个 [on] input 属性(并与 <switch> 组件共享),该属性将决定 aria-pressed 属性的值是 true 还是 false。成果stackblitz演示地址译者注到这里已经是第七篇了,也许你已经发现,Angular 中很多开发模式或者理念,都和 Directive 脱不了干系。Angular 中其本身推崇组件化开发,即把一切 UI 概念当做 Component 来看待,但仔细思考的话,这其实是有前提的,即这个 UI 概念一般是由一个或多个 html 元素组成的,比如一个按钮、一个表格等。但是在前端开发中,小于元素这个颗粒度的概念也是存在的,比如上文提及的 aira 属性便是其中之一,如果也为将这些 UI 概念抽象化为一个组件,就未免杀鸡用牛刀了,因此这里使用 Directive 才是最佳实践,其官方文章本身也有描述,Directive 即为没有模板的 Component。从组件开发者的角度来看的话,Directive 也会作为一种相对 Component 更加轻量的解决方案,因为与其提供封装良好、配置灵活、功能完备(这三点其实很难同时满足)的 Component,不如提供功能简单的 Directive,而将部分其他工作交付组件使用者来完成。比如文章中所提及的,作为组件开发者,无法预先得知组件使用者会怎样管理开关元素以及它的样式,因此提供一些 hooks 是很有必要的,而 hooks 这个概念,一般情况下,都会是相对简单的,比如生命周期 hook、调用过程 hook、自定义属性 hook 等,在这里,我们通过 Directive 为自定义开关元素增加 aria 属性来达到提供自定义属性 hook 的目标。 ...

October 9, 2018 · 1 min · jiezi