SOA服务设计原则-转载
这一部分是有关整个 SOA 系统的指南,代表了在建立系统时需要进行决策的各个方面。您将向设计人员和实现人员提供哪些规则和指导方针?您的 SOA 基础结构将提供何种功能?我们将给出一系列建议设计原则,但每个都是设计过程中的一种折衷做法。您的企业可能有具体的要求,而需要选择与我们提供的常规建议不同的选项。我们提出设计原则的目的在于标识需要进行决策的方面;而此类决策则是架构设计人员的责任。我们并不认为所提出的设计原则非常全面;在您的企业中实现 SOA 时,很有可能会采用其他原则,我们非常希望您能将这些设计原则反馈给我们。
SOA 要求一致性
有很多可用于创建、发布、发现和调用服务的候选技术。SOA 应提供一个参考体系结构,以指定服务提供者和使用者将使用的特定机制;我们应以在 SAO 所有参与者间实现一致性为目标。此类一致性可以减少开发、集成和维护工作。
如果需要使用参考体系结构之外的元素,我们推荐使用补充性方法。例如,假如我们为服务发布和发现选择的机制是 UDDI,但某个特定的开发团队已在使用一个基于其他存储库技术的开发流程,此时该如何处理呢?我们将选择投入精力将该团队的服务同时发布到两个存储库。这样,现有的服务使用者就可以使用其熟悉(但可能并不标准)的存储库了。而运行于公共 SOA 基础结构上的使用者则可以为所有服务使用标准存储库——例如 UDDI。
SOA 简化开发
我们希望任何企业级的 SOA 基础结构都具有可伸缩性和弹性;还应包含行业级的企业服务总线(Enterprise Service Bus,ESB)和安全技术。或者,换种说法,以 SOA 为目标的服务和流程的开发人员可利用成熟的中间件,依赖 SOA 基础结构提供问题的解决方案,如身份验证、消息转换和可靠消息交付。
这些中间件功能的提供应以一个重要的原则为基础:服务和流程开发人员应远离中间件实现的复杂细节。我们的理想目标是,在我们的 SOA 环境中工作的开发人员应只需要业务领域的相关知识和基本的编程技巧。
我们可以通过各种方式实现此目标,如下所述:
总之,在定义面向服务的体系结构及其基础结构时,我们必须特别注意开发人员的需求。当为开发人员提供指南,以告知他们应如何开发或使用服务时,我们应该寻找可促进这些指导方针遵循的机制。一个总的原则是“只要可方便完成所需的工作,就说明方法是正确的。”换句话说,遵循相关指南应当为阻力最小的方法。SOA 内的控制对其成功甚为关键。从开发人员的角度而言,他们有责任了解 SOA 基础结构和指南,并积极使用指南,而不要尝试进行规避。
服务具有标准的、经过正式定义的可由计算机处理的接口
了解了工具和代码生成在 SOA 实现中可扮演重要角色之后,我们现在要强调使用可由计算机处理的接口的重要性。当使用定义良好的可由计算机处理的语言描述了接口时,实际上就为各种工具支持功能提供了支持。我们希望改善分离状况,因此我们强烈建议使用 WSDL 之类正式定义的开放标准语言,而不要使用专用格式。
可由计算机处理的方法的概念应该从服务接口描述(如 WSDL)扩展到所有其他形式的声明信息或元数据。只有同时强调声明技术和可由计算机处理的元数据,才能将其相关的复杂性从业务应用程序开发人员转移到基于标准的中间件中。新兴的 WS-Policy 之类的技术在支持此方法方面充当着重要的角色。
服务应设计为可重用
服务设计人员应该记住,他们所开发的任何服务都可能成为可重用资产。设计人员不应只关注服务的最初使用者的需求,而应该进行更为广泛的业务分析,以确定更全面的需求。我们建议,设计人员应考虑服务可能的发展方向:
文本其余部分所讨论的很多设计原则都与确保服务的可伸缩性和可维护性密切相关。需要提醒一下:可能会由于考虑了潜在的重用而采用不恰当的设计方法对服务进行设计,从而导致实现“过当”。我们鼓励将最初的重点放在服务接口设计上,以确保其支持可伸缩性;我们的设计原则可帮助做到这一点。然后生成一个该接口的战术型实现,要求足以满足目前已知的需求。假如该接口设计良好,应该可以在出现相关需求时替代伸缩性更好的实现。
我们曾说过,服务是其接口采用某种一致认可的格式发布的服务操作的逻辑分组,那么我们接下来将讨论适用于整个服务的设计原则。在下面的服务操作设计原则中,我们将讨论各个操作的设计。
命名服务时应以最大化易用性为目标
我们在选择服务、操作、数据类型和参数的名称时有一个指导原则:希望最大化服务的易用性。我们希望帮助流程开发人员标识实现业务流程所需的服务和操作。因此,我们强烈建议使用服务使用者专业领域内有意义的名称,优先选用业务概念而不是技术概念。我们的建议就是:应使用名词对服务进行命名;而应使用动词对操作进行命名。(举例:查询物料基本信息服务)
服务应具有精心选择的粒度
粒度 一词在 SOA 相关讨论中有多种不同的用法。在本文的服务设计讨论中,我们考虑的是服务本身的粒度,即服务应该包含的操作数量。没有可用于确定服务粒度的简单启发式方法。我们将提供两个在设计服务时应该考虑的因素的示例加以说明:
这表明,在选择服务粒度时,我们可能需要在多个因素间进行折衷,如可维护性、可操作性和易用性。任何给定的 SOA 都应向服务设计人员提供指南,以便确定此类折衷方案。
服务应是内聚而完整的
既然认识到了在确定服务粒度时需要考虑周全,那么在确定哪些操作应组成服务时有什么注意事项呢?我们认为有两个对象设计概念很有用:内聚性和完整性。我们可将这些概念应用于服务接口。
我们希望创建功能内聚的接口,一组操作由于其功能相关而聚合到一起。我们发现,当评估内聚程度时,从服务使用者角度看待服务很有用。通过使用者的视角,我们会将重点放在服务的功能上。将此方法与使用以下内聚标准进行对比:
使用名词-动词对服务和操作进行命名的规则可以帮助我们将重点放在服务接口的功能内聚性上。我们可以问这样一个问题“这个动词是否是该名词所进行的操作?”
我们的第二个对象设计概念是完整性概念。在为已知使用者创建服务时,完整性的问题尤为值得注意。在这种情况下,我们通常会将重点放在满足所知的使用者需求上。请务必记住,服务应该为可重用的,因此需要考虑将来的使用者的可能需求。举个简单的例子,假如有个名为 CellPhone 的服务提供 Create、Update、Query、Delete 和 Deactivate 等操作。我们完全可以想象会需要对弃用的手机进行重新激活,因此应决定是否也应提供对称的 Activate 方法。
通过判断,我们应该应用完整性规则。如果不知道使用者需求,则可能很难提供正确的功能,因此就有可能存在将开发和测试工作浪费在提供将不会使用的操作上的风险。
服务应对实现细节进行封装
另一个对象设计原则(封装)也适用于设计服务接口。我们封装服务实现的细节——所用的算法和资源——的动机在于增加服务使用者和提供者之间的分离,从而为将来扩展提供灵活性。
服务适应多种调用模式
WebSphere? 等提供的 Web 服务技术允许进行更高层次的封装。服务使用者通过使用各种调用模式,可以使用完全相同的代码技术来调用 Web 服务,如以下这些模式:
不过,虽然 Web 服务基础结构可以封装调用的细节,从而简化代码,但服务设计也应对调用方式加以考虑。对比一下本地调用和远程调用。与清单 3 所示内容类似的服务设计可以提供有价值的业务功能,但却不适合在很多 SOA 环境中部署。
服务具有无状态接口
我们在服务应设计为可重用中提到,应该将服务设计为可伸缩且可部署到高可用性基础结构中。此总体原则的一个推论就是,服务不应为有状态型的。即,它们不应依赖于使用者和提供者间长期存在的关系,操作调用也不应隐式地依赖于前一个调用。我们强烈建议,服务应设计为可避免维护会话上下文的需求。
服务应使用状态事务建模
前面给出了一个总的建议,以避免依赖对话状态,但我们应当记住,有用的计算机系统通常将为有状态的;通常反映了业务对象的生命周期。例如,考虑购物中的一个订单的生命周期:创建订单。从用户的角度来看,创建了一个空的购物车。用户将随后向订单添加物品,即将其放入购物车中。最后提交订单,然后订单将被传送给配送部门。
操作表示业务动作。
我们已经指出,总的原则是,我们应该优先对服务和操作使用业务领域的名称,使用动词作为操作名称。对于操作,我们将这个建议进一步深化:应当使用具体的业务含义而不是泛型操作对操作进行定义。例如,不要使用泛泛的 UpdateCustomerDetails 操作,而要创建 ChangeCustomerAddress、RecordCustomerMarriage 和 AddAlternativeCustomerContactNumber 之类的操作。(由于体现业务动作后服务的粒度根据细化,要根据实际情况使用。数据集成类服务不需要体现业务动作。)此方法具有以下好处:
操作应采用粗粒度参数
在讨论操作参数时,同样要面对粒度的问题。请比较清单 11 和清单 12 中所示的 CreateNewCustomer 操作的两个接口。
清单 11. 采用细粒度参数的 CreateNewCustomer 操作接口
??? int CreateNewCustomer(String familyName,
??? ??? String givenName,
??? ??? String initials,
??? ??? int age
??? ??? String address1
??? ??? String address2
??? ??? String postcode
??? ??? // ...
??? )
清单 12. 采用单个粗粒度参数的 CreateNewCustomer 操作接口
??? int CreateNewCustomer( CustomerDetails newDetails)
???
清单 11 显示了一个具有很多细粒度参数的操作。而在清单 12 中的操作则采用结构化类型作为单个粗粒度参数。我们之所以建议使用粗粒度参数,有两个原因。首先,它们提供了创建灵活操作的机会,支持在不干扰现有使用者的情况下提供新版本的操作。其次,具有大量类型相似的参数的操作易于在从第三代语言代码进行调用时出现转换错误。相反,当数据放置在所使用的结构化类型的显式方法(如 setGivenName() 和 setInitials())中时,此方法出错的几率更小。
操作设计应考虑并发性
传统的事务型编程模型(如 Entity Enterprise Java Bean (Entity Bean) 所支持的编程模型)允许实现数据库更新,因此其数据库锁定方式如清单 13 中所示。(注,即考虑服务的事务完整性。)
清单 13. 事务型编程模型
??? ??? Begin Transaction
??? ??? Retrieve data from database - locking record??
??? ??? Modify values
??? ??? Update database record with modified values
??? ??? Commit Transaction - unlocking record
请注意,数据库锁定从第 1 行检索时一直保持到第 5 行的提交操作。这样以一定的延迟确保了正确的并发行为。如果我们希望设计一个提供数据库更新功能的服务,则可以提供与清单 8 中的第 2 行和第 4 行的检索和写入操作对应的操作。不过,我们强烈建议,不要在高度分离且可能异步的 SOA 基础结构中的连续调用间保持锁定。我们建议采用乐观锁定策略,将并发控制的责任委派给相应的应用程序逻辑。
乐观锁定策略中的更新请求可以解释为“以下是基于记录 XYZ 的 V 版本的一些记录更新。请仅在从我读取该记录后没有人进行修改的情况下进行更改。”
考虑到管理并发更新的相对复杂性,我们提出一个相关的建议:尽可能使用无状态语义。例如,与实现等效的“Retrieve record”-“Write record”两个操作(使用者会在检索和写入操作间使值递增)相比,可能实现具有良好并发行为的单个操作“Increment balance by X”更为容易。