4.8 实现基于事件的异步协作方式

4.8实现基于事件的异步协作方式
 
前面讨论了一些与请求/响应模式相关的技术。那么基干事件的异步通信呢?
 
4.8.1技术选择
 
主要有两个部分需要考虑:微服务发布事件机制和消费者接收事件机制。
 
传统上来说,像RabbitMQ这样的消息代理能够处理上述两个方面的问题。生产者 (producer)使用API向代理发布事件,代理也可以向消费者提供订阅服务,并且在事件发 生时通知消费者。这种代理甚至可以跟踪消费者的状态,比如标记哪些消息是该消费者已 经消费过的。这种系统通常具有较好的可伸缩性和弹性,但这么做也是有代价的。它会增 加开发流程的复杂度,因为你需要一个额外的系统(即消息代理)才能开发及测试服务。 你也需要额外的机器和专业知识来保持这些基础设施的正常运行。但一旦做好了,它会是 实现松耦合、事件驱动架构的一种非常有效的方法。通常来说我很喜欢这种方式。
 
不过需要注意的是,消息代理仅仅是中间件世界中的一小部分而已。队列本身是很合理、 很有用的东西。但是中间件厂商通常倾向于把很多的软件打包进去,比如像企业级服务总 线这样的东西。谨记这个原则:尽量让中间件保持简单,而把业务逻辑放在自己的服务中。
 
另一种方法是使用HTTP来传播事件。ATOM是一个符合REST规范的协议,可以通过它 提供资源聚合(feed)的发布服务,而且有很多现成的客户端库可以用来消费该聚合。这 样当客户服务发生改变时,只需简单地向该聚合发布一个事件即可。消费者会轮询该聚合 以查看变化。另一方面,现成的ATOM规范和与之相关的库用起来非常方便,而且HTTP 能够很好地处理伸缩性。但正如前面所提到的,HTTP不擅长处理低延迟的场景,而且使 用ATOM的话,用户还需要自己追踪消息是否送达及管理轮询等工作。
 
我见过很多人花费大量时间,在ATOM上实现越来越多任何一个还不错的消息代理都能 够提供的功能。举个例子,消费者竞争模式(Competing Consumer pattern)描述了一种 使用多个工作者实例同时消费消息的方法,工作者实例的数量可以增加,而且它们应该 可以独立于彼此正常工作。但是有一种场景需要避免,即多个工作者处理了同一条消息,从而造成浪费。如果使用消息代理,一个标准的队列就可以很好地处理这种场景。而使 用ATOM的话,就需要自己在所有的工作者之间维护一个共享的状态来减少上述情况的 发生。
 
如果你已经有了一个好的、具有弹性的消息代理的话,就用它来处理事件的汀阅和发布 吧。但如果没有的话,你可以看一看ATOM。但要注意沉没成本的陷阱,比如当你发现有 越来越多的消息代理可以满足需求时,就应该在某个时间点做出相应的调整。
 
关干异步协议使用什么样的消息格式,其实需要考虑的因素和使用同步通信时没什么区 别。如果你现在正在使用JSON作为请求和响应的格式,那么可以继续使用。
 
4.8.2异步架构的复杂性
 
这些异步的东西看起来挺有趣的,对吧?事件驱动的系统看起来耦合非常低,而且伸缩性 很好。但是这种编程风格也会带来一定的复杂性,这种复杂性并不仅仅包括对消息的发布 汀阅操作。举个例子,考虑一个非常耗时的异步请求/响应,需要考虑响应返回时需要怎 么处理。该响应是否回到发送请求的那个节点?如果是的话,节点服务停止了怎么办?如 果不是,是否需要把信息事先存储到某个其他地方,以便干做相应处理?如果API设计得 好的话,短生命周期的异步操作还是比较容易管理的,但尽管如此,对于习惯了进程间同 步调用的程序员来说,使用异步模式也需要思维上的转换。
 
现在来看一个大家可以引以为戒的故事。2006年,我在一家银行帮客户构建定价系统,系 统需要根据市场事件来决定投资组合中的哪些项需要重新定价。一旦确定了需要做的事情 之后,就把它们全都放到一个消息队列中。当时我们使用一个网格来创建定价工作者池, 这样就可以根据需求来调整定价集群的规模。这些工作者使用消费者竞争模式,每个工作 者都不停地处理这些消息,直到没有消息可处理为止。
 
系统运行起来了,我们感觉很棒。但是在某一次发布之后,我们遇到了一个很令人讨厌的 问题。我们的工作者不停地崩溃,不停地崩溃,不停地崩溃。
 
最终我们发现了问题所在。代码中存在一个bug,某一种定价请求会导致工作者崩溃。我 们当时使用了事务处理队列:当工作者崩溃之后,这个请求上的锁会超时,然后该请求 就会被放回到队列中。另一个工作者会重新尝试处理该请求,然后它也会崩溃。这就 是Martin Fowler提到的灾难性故障转移(catastrophic failover)的一个典型例子(http:// martinfowler.com/bliki/CatastrophicFailover.litml)。
 
除了代码中的bug外,我们还忘了设置一个作业最大重试次数。所以后面不但修复了 bug 本身,还设置了这个最大重试次数。但是我们也意识到需要有一种方式来查看甚至是重发 这些有问题的消息。所以最后实现了一个消息医院(或者叫死信队列),所有失败的消息 都会被发送到这里。我们还创建了一个界面来显示这些消息,如果需要的话还可以触发一 个重试。如果你只熟悉点到点的同步通信,就很难快速发现这个问题。
事件驱动架构和异步编程会带来一定的复杂性,所以我通常会很谨慎地选用这种技术。你 需要确保各个流程有很好的监控机制,并考虑使用关联ID,这种机制可以帮助你对跨进程的请求进行追踪,第8章会详细讨论这个话题。
 
强烈推荐你读一读《企业集成模式》这本书,其中详细讨论了很多不同的编程模式。
 
4.9服务即状态机
 
不管你选择做一个REST忍者,还是坚持使用像SOAP这样的基于RPC的机制,服务即状 态机的概念都很强大。前面提到过(可能已经提的太多了)服务应该根据限界上下文进行 划分。我们的客户微服务应该拥有与这个上下文中行为相关的所有逻辑。
 
当消费者想要对客户做修改时,它会向客户服务发送一个合适的请求。客户服务根据自己 的逻辑决定是否接受该请求。客户服务控制了所有与客户生命周期相关的事件。我们想要 避免简单地对CRUD进行封装的贫血服务。如果出现了在客户服务之外与其进行相关的修 改的情况,那么你就失去了内聚性。
 
把关键领域的生命周期显式建模出来非常有用。我们不但可以在唯一的一个地方处理状态 冲突(比如,尝试更新已经被移除的用户),而且可以在这些状态变化的基础上封装一些 行为。
 
我仍然认为基于HTTP的REST相比其他集成技术更合理,但不管你选的是什么,都要记 住上面的原则。
 
4.10响应式扩展
 
响应式扩展(Reactive extensions,Rx)提供了一种机制,在此之上,你可以把多个调用的 结果组装起来并在此基础上执行操作。调用本身可以是阻塞或者非阻塞的。Rx改变了传 统的流程。以往我们会获取一些数据,然后基干此进行操作,现在你可以做的是简单地对 操作的结果进行观察,结果会根据相关数据的改变自动更新。一些Rx的实现允许你对这 些被观察者应用某种函数变换,比如在RxJava中就可以使用类似map或者filter这样的经 典函数。
 
很多Rx实现都在分布式系统中找到了归宿。因为调用的细节被屏蔽了,所以事情也更容 易处理。我可以简单地对下游服务调用的结果进行观察,而不需要关心它是阻塞的还是非 阻塞的,唯一需要做的就是等待结果并做出响应。其漂亮之处在于,我可以把多个不同的 调用组合起来,这样就可以更容易地对下游服务的并发调用做处理。
 
当你需要做一些基于多个服务调用的操作时,尝试一下适合你所选用技术栈的响应式扩 展。你会惊讶地发现它让你的代码变得非常简单。
 
4.11微服务世界中的DRY和代码重用的危险
 
开发人员对DRY这个缩写非常熟悉,目P Don’t Repeat Yourself。虽然从字面上看DRY仅 仅是避免重复代码,但其更精确的含义是避免系统行为和知识的重复。一般来讲这是很合 理的建议。如果有相同的代码来做同样的事情,代码规模就会变大,从而降低可维护性。 如果你想要修改的行为在系统的很多部分都有重复实现的话,那么就很容易漏掉某些部分 的修改,从而导致bug,所以强制性地使用DRY—般来讲是合理的。
 
使用DRY可以得到重用性比较好的代码。把重复代码抽取出来,然后就可以在多个地方 进行调用。比如说可以创建一个随处可用的共享库。但是这个方法在微服务的架构中可能 是危险的。
 
我们想要避免微服务和消费者之间的过度耦合,否则对微服务任何小的改动都会引起消费 方的改动。而共享代码就有可能会导致这种耦合。比如,客户端可以通过库共享其中表示 系统核心实体的公共领域对象,而所有的服务也会使用这个库。所以当任何部分需要对库 做修改时,都会引起其他部分的重新部署。如果你的系统通过消息队列进行通信,那么你 需要过滤(由不同步的部署导致的)失效的内容,忘记这么做会引起严重的问题。
 
跨服务共用代码很有可能会引入耦合。但使用像日志库这样的公共代码就没什么问题,因 为它们对外是不可见的。Realestate.com.au使用了很多深度定制化的服务模板来快速创建 新服务。他们不会在服务之间共用代码,而是把这些代码复制到每个新的服务中,以防止 耦合的发生。
 
我的经验是:在微服务内部不要违反DRY,但在跨服务的情况下可以适当违反DRY。服 务之间引入大量的耦合会比重复代码带来更糟糕的问题。但这的确是一个值得进一步探索 的问题。
 
客户端库(这里的客户端可以指某个服务、某个客户端软件,服务之间的集成:松耦合,所以这里讲的很重要)
 
很多团队坚持在最开始的时候为服务开发一个客户端库。原因在于,这样不仅能简化对服 务的使用,还能避免不同消费者之间存在重复的与服务交互的代码。
 
这么做的问题在于,如果开发服务端API和客户端API的是同一批人,那么服务端的逻辑 就有可能泄露到客户端中。我对此很清楚,因为我以前就这么做过。潜入客户端库的逻辑 越多,内聚性就越差,然后你必须在修复一个服务端问题的同时,也需对多个客户端进行 修改。这样做也会限制技术的选择,尤其是当你强制消费方使用该客户端库时。-
 
我喜欢AWS (Amazon Web Service)使用的那种客户端库的模式。它允许你直接使用底 层的SOAP或者REST接口,但事实上所有人最终都会使用SDK (Software Development Kits,软件开发工具箱),该SDK对底层API进行了抽象。值得一提的是,这些SDK要么
是由社区提供的,要么是由API幵发团队之外的的AWS员工开发的。这种程度的分离 似乎是有效的,它避免了使用客户端库的一些问题。i亥模式效果很好,其中一部分原因 是客户端自主决定何时进行升级。如果你一定要使用客户端库,请确保使用这种正确的 方式。
 
Netflix非會强调客户端库的使用,但千万不要简单地认为其目的仅仅是避免代码重复。事 实上,Netflix使用客户端库的另一个同等重要的(如果不是更重要的)原因是,保证系统 的可靠性和可伸缩性。Netflix的客户端库会处理类似服务发现、故障模式、日志等方面 的工作,可以看到这些方面与服务本身的职责并没有什么关系。如果不使用这些共享客户 端,Netflix就很难保证客户端和服务器之间的通信能够在规模化的情况下正常工作。这些 库在Netflix中的使用大大减少了初始搭建的工作量,并提高了生产率,同时也能确保系统 能正常工作。然而,至少有一个来自Netflix的员工表示,经过一段时间之后,这种做法还 是引入了客户端和服务器之间一定程度的耦合,并产生了一些问题。
 
如果你想要使用客户端库,一定要保证其中只包含处理底层传输协议的代码,比如服务发 现和故障处理等。千万不要把与目标服务相关的逻辑放到客户端库中。想清楚你是否要坚 持使用客户端库,或者你是否允许别人使用不同的技术栈来对底层API进行调用。最后, 确保由客户端来负责何时进行客户端库的升级,这样才能保证每个服务可以独立于其他服 务进行发布!
 
4.12按引用访问
 
如何传递领域实体的相关信息是一个值得讨论的话题。很重要的一个想法是,微服务应 该包含核心领域实体(比如客户)全生命周期的相关操作。前面讨论了把与客户有关的 逻辑放在客户服务中的重要性。在这种设计下,如果想要做任何与客户相关的改动,就 必须向客户服务发起请求。它遵守了一个原则,即客户服务应该是关于客户信息的唯一 可靠来源。    、
 
想象这样一个场景,你从客户服务获取了一个客户资源,那么就能看到该资源在你发起请 求那一刻的样子。但是有可能在你发送了请求之后,其他人对该资源进行了修改,所以你 所持有的其实是该客户资源曾经的样子。你持有这个资源的时间越久,其内容失效的可能 性就越高。当然,避免不必要的数据请求可以让系统更高效。
 
有时候使用本地副本没什么问题,但在其他场景下你需要知道该副本是否已经失效。所以 当你持有一个本地副本时,请确保同时持有一个指向原始资源的引用,这样在你需要的时 候就可以对本地副本进行更新。
 
考虑这样一个例子:发货之后需要请求邮件服务来发送一封邮件。一种做法是,把客户的 邮件地址、姓名、订单详情等信息发送到邮件服务。但是邮件服务有可能会将这个请求放人队列,然后在将来的某个时间再从队列中取出来,在这个时间差中,客户和订单的信息 有可能就会发生变化。更合理的方式应该是,仅仅发送表示客户资源和订单资源的uri, 然后等邮件服务器就绪时再回过头来查询这些信息。
 
在考虑基于事件的协作时,你会发现一个很棒的对位(coiimerpoim) \使用事件时,不仅需 要知道该事件是否发生,还需要知道到底发生了什么。所以当收到一个客户资源变化的更 新事件时,我想要知道事件发生时该客户的状态。同时为了能够在处理事件时得到资源的 最新状态,也应该拥有该实体的引用以便于查询。
 
当然在使用引用时也需要做一些取舍。如果总是从客户服务去查洵给定客户的相关信息, 那么客户服务的负载就会过大。如果在获取资源的同时,可以得到资源的有效性时限(即 该资源在什么时间之前是有效的)信息的话,就可以进行相应的缓存,从而减小服务的负 载。HTTP在缓存控制方面提供了很多现成的支持,第11章会讨论其中的一些措施。
 
另一个问题是,有些服务可能不需要知道整个客户资源,所以坚持进行査询这种方式会引 人潜在的耦合。冇人提出邮件服务器应该更加简单,只需要简单地把邮件地址和客户名称 发给它就可以了。我认为这里并不存在非常明确的统一规则,原则上来说,应该在不确定 数据是否能够保持有效的情况下,谨慎地进行处理。
 
4.13版本管理
 
每次提及微服务的时候,都会有人问我如何做版本管理。大家担心服务的接口难免发生改 变,耶么如何管理这些改变呢?让我们把这个问题划分成一些更小的问题,然后看看如何 对每一个进行针对性处理。
 
4.13.1尽可能推迟
减小破坏性修改影响的最好办法就是尽量不要做这样的修改。本章讨论了很多不同的集成 技术,你可以通过选用正确的技术来做到这一点。比如数据库集成很容易引人破坏性的修 改。然而REST就好得多,W为对于内部实现的修改不太容易引起服务接口的变化。
 
另一个延迟破坏性修改的关键是鼓励客户端的正确行为,避免过早地将客户端和服务端紧 密绑定起来。考虑邮件服务这个例子,它会时不时地向客户发送邮件。假设现在它得到了 一个指令:发送“订单已发送”的邮件给ID为1234的客户。它会使用该ID获取客户信 息,然后得到类似示例4-3中的响应。
 
发送邮件需要名、姓和邮件地址等信息,但不需要电话号码。我们希望能够简单地得到所 需要的那些字段,而忽略剩余的。一些强类型语言会使用一些绑定技术,这种技术会将所 有字段进行自动绑定,无论消费者是否需要。如果我们意识到,没有人在使用电话号码这 个字段并决定要删除它,有些消费者可能就会受到不必要的影响。
 
类似地,如果想要重新构造客户对象来添加更多细节(如示例4-4所示),又会发生什么 呢?邮件服务器所需要的数据还在那里,名字也相同,但是,如果我们的代码只会去某 个指定的位置寻找名和姓的信息,就会发生错误。在这个例子中,可以使用XPath来从 中提取出想要的信息,这样字段的位置就可以更加灵活。这种读取器的实现能够忽略我 们不在乎的那些修改,Martin Fowler称其为容错性读取器(http://martinfowler.com/bliki/ TolerantReader.html)。
 
客户端尽可能灵活地消费服务响应这一点符合Postel法则(也叫作鲁棒性原则,https:// tools.ietf.org/html/rfc761)。该法则认为,系统中的每个模块都应该“宽进严出”,即对自己 发送的东西要严格,对接收的东西则要宽容。这个原则最初的上下文是网络设备之间的交 互,因为在这个场景中,所有奇怪的事情都有可能发生。在请求/响应的场景下,该原则 可以帮助我们在服务发生改变时,减少消费方的修改。
 
4.13.2及早发现破坏性修改
 
及早发现会对消费者产生破坏的修改非常重要,因为即使使用最好的技术,也难以避免破 坏性修改的出现。我强烈建议使用消费者驱动的契约来及早定位这些问题,第7章会对该 技术做详细的讲解。如果你支持多种不同的客户端库,那么最好针对最新的服务对所有的客户端运行测试。一旦意识到,你可能会对某一个消费者造成破坏,那么可以选择要么尽 量避免该破坏性修改,要么接受它,并跟维护这些服务的人员好好聊一聊。
 
4.13.3使用语义化的版本管理
 
如果一个客户端能够仅仅通过查看服务的版本号,就知道它是否能够与之进行集成,那就 太棒了!语义化版本管理(http://semver.org/)就是一种能够支持这种方式的规格说明。语 义化版本管理的每一个版本号都遵循这样的格式:MAJOR .MINOR .PATCH。其中MAJOR的改变 意味着其中包含向后不兼容的修改;MINOR的改变意味着有新功能的增加,但应该是向后 兼容的;最后,PATCH的改变代表对已有功能的缺陷修复。
 
为了更好地理解语义化版本管理,让我们来看一个简单的用例。帮助台应用能够与1.2.0 版本的客户服务一起使用。如果新功能的增加引起了客户服务的版本变成了 1.3.0,那么帮 助台应用不应该看到任何行为的变化,并且自身也不需要做任何改动。由于当前的客户端 可能会依赖于在1.2.0版本中新加入的功能,所以不能保证,现在的版本可以和1.1.0版本 的客户服务一起工作。当客户服务升级到2.0.0版本时,本地应用程序应该也需要做相应 的修改。
 
你可能会决定在服务中使用语义化版本,如果使用下一节中描述的那种共存方式,那么甚 至可以针对某个特定的接口做版本管理。
 
这个版本管理策略允许我们把很多的信息和期望打包到三个字段中。完整的规范大纲就是 简单的三个数字的变化,这个规范可以简化检查版本兼容性的流程。不幸的是,我还没有 在分布式系统中见到很多这样用的例子。
 
4.13.4不同的接口共存
 
如果已经做了可以做的所有事情来避免对接口的修改(但还是无法避免),那么下一步的 任务就是限制其影响。我们不想强迫客户端跟随服务端一起升级,因为希望微服务可以独 立于彼此进行发布。我用过的一种比较成功的方法是,在同一个服务上使新接口和老接口 同时存在。所以在发布一个破坏性修改时,可以部署一个同时包含新老接口的版本。
 
这可以帮助我们尽快发布新版本的微服务,其中包含了新的接口,同时也给了消费者时间 做迁移。一旦所有的消费者不再访问老的接口,就可以删除掉该接口及相关的代码,如图 4-5所示。
 
4.13.4不同的接口共存(同一个服务上使用不同接口)
 
如果已经做了可以做的所有事情来避免对接口的修改(但还是无法避免),那么下一步的 任务就是限制其影响。我们不想强迫客户端跟随服务端一起升级,因为希望微服务可以独 立干彼此进行发布。我用过的一种比较成功的方法是,在同一个服务上使新接口和老接口 同时存在。所以在发布一个破坏性修改时,可以部署一个同时包含新老接口的版本。
 
这可以帮助我们尽快发布新版本的微服务,其中包含了新的接口,同时也给了消费者时间 做迁移。一旦所有的消费者不再访问老的接口,就可以删除掉该接口及相关的代码,如图 4-5所示。
图4-5:某个接口的不同版本同时存在,允许消费者进行逐步的迁移
在我使用这种方法的上一个项目中,随着消费者数量的增加和破坏性修改次数的增加,情 况幵始变得有些混乱。事实上,我们同时维护了三个版本的接口,当然不推荐这样做!维 护多份代码及相关的测试完全是额外的负担。为了使其更可控,我们在内部把所有对VI 的请求进行转换处理,然后去访问V2,继而V2再去访问V3。使用这种方式后,以后应 该删除哪些代码也就比较清楚了。
 
这其实就是一个扩展/收缩模式的实例,它允许我们对破坏性修改进行平滑的过度。首先 扩张服务的能力,对新老两种方式都进行支持。然后等到老的消费者都采用了新的方式, 再通过收缩API去掉旧的功能。
 
如果采用这种不同版本接口共存的方式,你需要一种方法来对不同的请求进行路由。对于 使用HTTP的系统来说,可以在请求中添加版本信息,也可以将其添加在URI中,比如 /vl/customer/和/v2/customer/。我也很犹豫采用哪种方法。一方面,我不希望客户端的代 码对URI模板进行硬编码;但从另一方面来看,这种方法确实非常明确,请求路由也比 较容易。
 
对于RPC来说,事情会更加棘手。我以前使用过protocol buffers来把方法放到不同的命名 空间中。比如vl.createCustomer和v2.createCustomer。但是当你尝i式在网络上对相同类型的不同版本进行传输时,就会非常痛苦。
 
4.13.5同时使用多个版本的服务(有多少个不同版本,则对应有多少个服务)
 
另一种经常被提起的版本管理的方法是,同时运行不同版本的服务,然后把老用户路由到 老版本的服务,而新用户可以看到新版本的服务,如图4-6所示。当改变老用户的代价过高时,Netflix会保守地采用这种方式,尤其在某些场景下,遗留的设备会与老版本的API 强行绑定。我个人不太喜欢这个想法,也理解为什么用Netflix的很少。首先,如果我需要 修复一个服务内部的bug,需要修复两个版本,并做两次部署。而且我很可能也需要在代 码库中拉分支,这无疑会引入很多问题。其次,把用户路由到正确的服务中去也是一件比 较复杂的事情。想要实现这一点,要么寻求中间件的帮助,要么自己写很多的nginx脚本, 但这样做的话系统会难以理解和管理。最后,考虑服务中可能需要管理的持久化状态。不 同版本的服务创建的用户,都需要被存储在同一个数据库中,并且它们对于不同的服务均 可见,这可能会引入更多的复杂性。
图4-6:运行多个版本的同一个服务来支持老的接口
 
短期内同时使用两个版本的服务是合理的,尤其是当你做蓝绿部署或者金丝雀发布时(第 7章会详细讨论这些模式)。在这些情况下,不同版本的服务可能只会共存几分钟或者几个 小时,而且一般只会有两个版本。升级消费者到新版本的时间越长,就越应该考虑在同一 个微服务中暴露两套API的做法。我对于共存两个服务的这种做法,是否适用于一般的项 目保持怀疑态度。
 
4.14.1走向数字化
 
在过去几年中,很多组织开始认为,不应该对网页端和移动端区别对待,相反应该对数字 化策略做全局考虑,即如何让客户更好地使用我们的服务。这对系统架构又有什么样的影 响呢?由于很难预测用户会怎样使用我们的API,所以很多公司会倾向干把API设计得比 较细粒度化,比如使用微服务架构所暴露出来的那些API。通过把服务的功能进行不同的 组合,可以为桌面应用程序、移动端设备、可穿戴设备的客户提供不同的体验,如果客户 来到实体店,甚至还可以通过这种组合提供更加真实的体验。
 
从组合的角度来考虑用户界面,如果把我们提供的能力看成是不同的绳索,组合就是把它 们编织起来。那么如何才能将其很好地编织起来呢?
 
4.14.2 约束
 
在用户与系统之间,需要考虑不同的交互形式中存在的一些约束。比如在桌面Web应用 中,需要考虑与用户浏览器及屏幕解析度相关的约束。但移动端会带来一些新的约束。移 动应用与服务器之间不同的通信方式会产生不同的效果。移动网络的带宽可能会有一定的 限制,但并非仅有的限制。有些交互方式可能会导致电池电量消耗过快,从而导致客户的 流失。
 
在不同的平台上与应用程序的交互方式也有所不同。比如我们很难在平板上进行右击操 作,而大多数情况,在手机上应该可以使用单手进行操作,其中大部分的操作应该使用拇 指进行控制。而在带宽情况不够好的地方,可以允许人们通过短信与服务进行交互。比如 在很多发展中国家,短信作为应用程序入口的做法还是很普遍的。
 
所以,尽管我们的核心服务可能是一样的,但仍需要应对不同应用场景的约束。在考虑不 同风格的用户界面组合时,需要保证它们做到了这一点。接下来看几个用户界面的例子。
 
然后UI会创建不同的组件来处理与服务之间状态的同步等工作。使用二进制协议 作为服务之间的通信方式,对于基于Web的客户端可能会不太友好,但对于原生移动设备 来说是可接受的。
 
这种方式有一些问题。首先很难为不同的设备定制不同的响应。比如,移动商店所需要的 数据和帮助台应用所需要的数据就有可能不同。一个解决方案是,允许客户指定它想要哪 些字段,但这就需要每个服务都支持这种交互方式。
 
另一个关键的问题是:谁来创建用户界面?维护服务的人往往不是服务的使用者。举个例 子,如果UI是另一个团队创建的,我们可能会退回到以前那种分层合作模式,在这种模 式下即使很小的修改都需要多个团队的参与。
 
这种通信模式非常繁琐。与服务之间过多的交互对移动设备来说会有些吃力,而且对使用流 量套餐的用户来说也很不利!使用API入口(gateway)可以很好地缓解这一问题,在这种 模式下多个底层的调用会被聚合成为一个调用,当然它也有一定的局限性,后面会做讨论。
 
4.14.4 UI片段的组合
 
相比UI主动访问所有的API,然后再将状态同步到UI控件,另一种选择是让服务直接暴露出一部分U1,然后只需要简单地把这些片段组合在一起就可以创建出整体UI,如图4-8 所示。举个例子,推荐服务可以提供一个嵌入到其他UI控件中的推荐窗口控件。比如在 网页上就可以嵌人这样一个控件。
图4-8:服务直接提供UI组件以供组装之用
 
这种方式有一个工作得很好的变种,即将一系列粗粒度的UI部分组装起来。也就是说不 再创建小部件,而是对胖客户端应用的所有内容或一个网站的所有页面进行组装。
 
这些粗粒度的片段由服务端程序提供,而这些程序又会去调用相关的API。当片段与团队 所有权匹配得比较好时,这个模型可以很好地进行工作。比如,也许音乐商店的订单管理 团队可以对所有与订单管理相关的页面负责。
 
你仍然需要某种组装层来把这些片段拉到一起,可以使用类似服务端模板的技术轻松地做 到。或者当你从很多不同的应用拉取页面时,需要某种智能的URI路由。
 
这种方式的一个关键优势是,修改服务团队的同时可以维护这些UI片段。它允许我们快 速完成修改,但这种方式也有一些问题。
 
首先,保证用户体验的一致性很重要。用户想要一个无缝的体验,而不是在应用的不同部 分得到不同感受及设计语言。然而有一些技术可以避免这些问题,比如活样式指导(living style guides),即将HTML组件、CSS及图片等资源进行共享,从而使其具有一定程度的 一致性。
 
接下来的问题比较棘手。原生应用和胖客户无法消费服务端提供的UI组件。一种解决方法是,使用混合方式在原生应用中嵌人HTML来重用一些服务端组件,但这种方式会使用 户体验欠佳。如果你需要的是原生应用的体验,那就必须自己对API进行请求,然后在本 地创建和管理UI。但即使只考虑基干Web的UI,不同的设备也会有不同的需求。当然响 应式组件能够很好地缓解这个问题。
 
还有一个问题,我不确定是否能够用这个方法解决。有时候服务提供的能力难以嵌入到小 部件或者页面中。虽然我可能会在网站页面的一个矩形区域中使用嵌入式的推荐服务控 件,但是如果想要把这种能力动态加载到其他地方呢?比如做搜索时,我希望在键人关键 字时推荐信息可以自动刷新。类似这样,交互越多就越难把一个服务做成控件的形式,也 许最终只能通过API调用来解决问题。
 
4.14.5为前端服务的后端
 
对与后端交互比较频繁的界面及需要给不同设备提供不同内容的界面来说,一个常见的解 决方案是,使用服务端的聚合接口或API人口。该入口可以对多个后端调用进行编排,并 为不同的设备提供定制化的内容,如图4-9所示。该入口如果变得太厚,包含的逻辑太多, 就会难以维护。它们会被逐渐交由单独的团队来管理,并且因为它们变得太厚,很多功能 的修改都会导致这部分代码的修改。
图4-9:使用单块入口来处理与UI之间的交互
 
这样做会得到一个聚合所有服务的巨大的层。由于所有的东西都被放在了一起,也就失去 了不同用户界面之间的隔离性,从而限制了独立于彼此进行发布的能力。我个人比较喜欢 的模式是,保证一个这样的后端只为一个应用或者用户界面服务,如图4-10所示。
图4-10:对于前端使用专用后端
这种模式有时也叫作BFF (Backends For Frontends,为前端服务的后端)。它允许团队在专 注于给定U1的同时,也会处理与之相关的服务端组件。后端虽然嵌人在服务端,但它也 是用户界面的组成部分。一些类型的UI只需要服务端的最小化足迹(footprint)即可,而 其他一些可能需要的更多。API认证和授权层可以处在BFF和UI之叫。第9章会进一步 探索这部分内容。
 
与任何一种聚合层类似,使用这种方法的风险在于包含不该包含的逻辑。业务逻辑应该处 在服务中,而不应该泄露到这一层。这些BFF应该仅仅包含与实现某种特定的用户体验相 关的逻辑。    、
 
也就是为这些前端应用分别创建它们的后端,然后它们各自的后端调用这些后端服务。
 
 
posted @ 2019-12-05 21:30  mongotea  阅读(436)  评论(0编辑  收藏  举报