您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
SOA 实现:服务设计原则
 
  2030  次浏览      15
 2018-9-11
 
编辑推荐:
本文来自于infoq.com,本文的主要目的是强调在面向服务的体系结构中服务设计的重要性。

引言

面向服务的体系结构(Service-Oriented Architecture,SOA)提供了支持业务灵活性的 IT 灵活性远景。在本文中,我们将重点讨论 IT 灵活性的两个特定方面:流程实现的分离和简化。如何说明和实现各个服务对 IT 灵活性的这些方面有很大的影响,因此也对业务灵活性有很大的影响。我们此处的目标是提供支持 SOA 远景的服务说明和实现指南。本文的论述结构如下:

1.首先,我们将描述将在其中说明和实现服务和服务操作的上下文环境。我们将考虑 SOA 基础结构的职责和服务的职责。

2.接下来,我们将讨论适用于整个服务(而不是各个操作)规范的设计原则。

3.最后,我们将说明适用于各个服务操作的设计原则。

我们在文中所给出的设计原则旨在通过改善流程实现的分离和简化来提高 IT 灵活性,因此,我们将通过对这些理念进行更为深入的分析来完成我们的介绍。

分离

SOA 原则非常强调将服务使用者和服务提供者分离开来,关于此类分离实际的含义,有很多不正式但非常有用的约定。分离背后的一个基础概念就是,对服务提供者的修改不应要求在服务使用者中进行相应的修改。例如,将当前在特定操作系统上运行的服务重新部署到另一个平台的决定完全可能不要求对服务使用者进行更改。一个主要的 SOA 指导原则就是,要减少使用者和提供者之间的依赖关系。

分离应用于技术层面,强调 Web 服务和异步消息交付之类的技术,以允许使用者独立于服务提供者选择实现和可用性选项。我们还可以通过各种方式让 SOA 基础结构实现技术分离,如:

服务应该设计为与要部署到其中的 SOA 基础结构兼容,特别是,服务应确保避免不必要的耦合。举个相反的例子,有状态服务接口将倾向于通过将使用者与特定提供者实例关联来增加使用者和提供者间的耦合。

分离的概念也同样适用于非技术的业务层次。服务使用者应该尽可能与服务提供者实现的业务逻辑细节分离。为了实现此类分离,同样也需要进行细心的设计。在下面的服务设计原则部分,我们将讨论将服务接口表述为有意义的业务操作(而不是细粒度的原语方法)的好处。

流程实现

在 SOA 中,我们通过对各个服务进行编排(通过编程方式或使用基于业务流程执行语言——Business Process Execution Language,BPEL——的工具)来实现业务流程。如果要实现 SOA 远景,则必须简化创建和修改流程的任务——即,服务编排任务。

业务流程编排的目标在于实际而有效地实现所需的业务逻辑。流程实现人员所面临的问题包括:

选择恰当的服务操作

确定使用正确参数调用服务的顺序

处理各种可能的响应,包括错误响应

应当清楚,服务设计的质量对编排简化有很大的影响。有关服务、操作和参数的名称和数量以及文档质量和服务操作之间的相互依赖程度——所有设计问题——的决策都可能给编排带来很大的影响。

SOA 设计原则

这一部分是有关整个 SOA 系统的指南,代表了在建立系统时需要进行决策的各个方面。您将向设计人员和实现人员提供哪些规则和指导方针?您的 SOA 基础结构将提供何种功能?我们将给出一系列建议设计原则,但每个都是设计过程中的一种折衷做法。您的企业可能有具体的要求,而需要选择与我们提供的常规建议不同的选项。我们提出设计原则的目的在于标识需要进行决策的方面;而此类决策则是架构设计人员的责任。我们并不认为所提出的设计原则非常全面;在您的企业中实现 SOA 时,很有可能会采用其他原则,我们非常希望您能将这些设计原则反馈给我们。

SOA 要求一致性

有很多可用于创建、发布、发现和调用服务的候选技术。SOA 应提供一个参考体系结构,以指定服务提供者和使用者将使用的特定机制;我们应以在 SAO 所有参与者间实现一致性为目标。此类一致性可以减少开发、集成和维护工作。

如果需要使用参考体系结构之外的元素,我们推荐使用补充性方法。例如,假如我们为服务发布和发现选择的机制是 UDDI,但某个特定的开发团队已在使用一个基于其他存储库技术的开发流程,此时该如何处理呢?我们将选择投入精力将该团队的服务同时发布到两个存储库。这样,现有的服务使用者就可以使用其熟悉(但可能并不标准)的存储库了。而运行于公共 SOA 基础结构上的使用者则可以为所有服务使用标准存储库——例如 UDDI。

SOA 简化开发

我们希望任何企业级的 SOA 基础结构都具有可伸缩性和弹性;还应包含行业级的企业服务总线(Enterprise Service Bus,ESB)和安全技术。或者,换种说法,以 SOA 为目标的服务和流程的开发人员可利用成熟的中间件,依赖 SOA 基础结构提供问题的解决方案,如身份验证、消息转换和可靠消息交付。

这些中间件功能的提供应以一个重要的原则为基础:服务和流程开发人员应远离中间件实现的复杂细节。我们的理想目标是,在我们的 SOA 环境中工作的开发人员应只需要业务领域的相关知识和基本的编程技巧。

我们可以通过各种方式实现此目标,如下所述:

声明技术: J2EE 编程模型就是使用声明技术提供应用程序逻辑和中间件配置分离的一个例子。例如,应用程序组装人员通过在部署描述符(而不是代码)中添加相应条目来应用 EJB 方法角色的安全授权;然后部署人员会将这些角色映射到用户和组。请注意,部署人员无需编写任何授权代码。

抽象: 在某些情况下,SOA 基础结构中可以提供 API,以用于特定的用途。例如,SOA 基础结构可以提供错误报告和审核机制。在设计此类 API 时应非常小心,要注意其易用性。我们应优先考虑声明技术,而不是对这些机制进行编程配置。同样,在标准 API 可用时(例如 Java 日志 API),我们应通过这些标准 API 公开 SOA 基础结构功能,而不是采用自己开发编写的方式。

代码生成: 在无法避免代码复杂性的地方,可以使用代码生成技术。例如,Web 服务描述语言(Web Services Definition Language,WSDL)就可以为开发人员隐藏 SOAP、HTTP 和 JMS 的复杂细节。这是通过组合用 WSDL 表示的可由计算机处理的接口定义和可从 WSDL 生成相关调用代码的语言特定实现的工具来实现的。

工具:在不可避免 SOA 基础结构的细节进入开发人员代码的情况下,我们可以通过使用合适的工具扩展开发环境来减少开发人员工作的复杂性。IBM Rational? Software Development Platform 产品所提供的基于 Eclipse 的环境可使用自定义插件、代码片段和用户指南轻松地进行扩展。

模型驱动的开发:模型驱动的开发技术可以被视为前面两种方法的特定复杂组合,同时利用了工具和代码生成功能来简化开发体验。开发人员生成统一建模语言(Unified Modeling Language,UML)模型,此类模型可转换为相应的代码,其中包含利用 SOA 基础结构所必需的代码。

总之,在定义面向服务的体系结构及其基础结构时,我们必须特别注意开发人员的需求。当为开发人员提供指南,以告知他们应如何开发或使用服务时,我们应该寻找可促进这些指导方针遵循的机制。一个总的原则是“只要可方便完成所需的工作,就说明方法是正确的。”换句话说,遵循相关指南应当为阻力最小的方法。SOA 内的控制对其成功甚为关键。

从开发人员的角度而言,他们有责任了解 SOA 基础结构和指南,并积极使用指南,而不要尝试进行规避。

服务具有标准的、经过正式定义的可由计算机处理的接口

了解了工具和代码生成在 SOA 实现中可扮演重要角色之后,我们现在要强调使用可由计算机处理的接口的重要性。当使用定义良好的可由计算机处理的语言描述了接口时,实际上就为各种工具支持功能提供了支持。我们希望改善分离状况,因此我们强烈建议使用 WSDL 之类正式定义的开放标准语言,而不要使用专用格式。

可由计算机处理的方法的概念应该从服务接口描述(如 WSDL)扩展到所有其他形式的声明信息或元数据。只有同时强调声明技术和可由计算机处理的元数据,才能将其相关的复杂性从业务应用程序开发人员转移到基于标准的中间件中。新兴的 WS-Policy 之类的技术在支持此方法方面充当着重要的角色。

服务应设计为可重用

服务设计人员应该记住,他们所开发的任何服务都可能成为可重用资产。设计人员不应只关注服务的最初使用者的需求,而应该进行更为广泛的业务分析,以确定更全面的需求。我们建议,设计人员应考虑服务可能的发展方向:

1.设计必须能适应不断增加的吞吐量;如果服务在使用服务的数量增加的情况下仍可成功运行,那么使用率也会成级数递增。

2.如果使用服务的数量增加,则数据量和并发数据访问模式可能会与最初投入使用时的情况大为不同。

3.我们必须对服务请求的未来增长进行预计;新使用者可能需要其他的功能,或者需要对现有功能进行更改

文本其余部分所讨论的很多设计原则都与确保服务的可伸缩性和可维护性密切相关。需要提醒一下:可能会由于考虑了潜在的重用而采用不恰当的设计方法对服务进行设计,从而导致实现“过当”。我们鼓励将最初的重点放在服务接口设计上,以确保其支持可伸缩性;我们的设计原则可帮助做到这一点。然后生成一个该接口的战术型实现,要求足以满足目前已知的需求。假如该接口设计良好,应该可以在出现相关需求时替代伸缩性更好的实现。

服务设计原则

我们曾说过,服务是其接口采用某种一致认可的格式发布的服务操作的逻辑分组,那么我们接下来将讨论适用于整个服务的设计原则。在下面的服务操作设计原则中,我们将讨论各个操作的设计。

命名服务时应以最大化易用性为目标

我们在选择服务、操作、数据类型和参数的名称时有一个指导原则:希望最大化服务的易用性。我们希望帮助流程开发人员标识实现业务流程所需的服务和操作。因此,我们强烈建议使用服务使用者专业领域内有意义的名称,优先选用业务概念而不是技术概念。

我们的建议就是:应使用名词对服务进行命名;而应使用动词对操作进行命名。

比较清单 1 和清单 2 中所示的两个服务定义。我们使用简化的伪代码来减少编程语言“簇”。

清单 1. 使用动词短语和 IT 构造的服务定义

ManageCustomerData {

InsertCustomerRecord()

UpdateCustomerRecord()

// etc ...
}

清单 2. 使用名词和动词短语及业务概念的服务定义

CustomerService {

CreateNewCustomer()

ChangeCustomerAddress()

CorrectCustomerAddress()

EnableOverdraftFacilityForCustomer()

// etc ...

}

请注意清单 1 中的定义是如何使用 IT 概念进行表述并同时为服务和操作使用动词短语的。在清单 2 中,服务表述为名词,而操作则使用具有清楚的业务含义的动词短语进行命名。我们认为第二个示例的易用性更好一些。此外,在第二个示例中,服务的业务用途非常清楚,而不单是仅仅指示其输出。因此,我们不使用“update customer record”(可以为出于任何原因进行的任何更新),而使用“enable overdraft facility”。与此类似,在客户搬迁时,我们使用“change customer address”方法更改客户地址;而在希望更正无效数据时使用“correct customer address”更正客户地址,因为这样很容易看出这两个操作采用了不同的服务逻辑。

如果采用业务概念表述服务和操作名称,则必须密切注意如何确定这些名称。这就非常需要有一个正式的术语词汇表,可以通过业务分析活动得到这个词汇表。词汇表应该有一个正式的所有者。

服务应具有精心选择的粒度

粒度 一词在 SOA 相关讨论中有多种不同的用法。在本文的服务设计讨论中,我们考虑的是服务本身的粒度,即服务应该包含的操作数量。

没有可用于确定服务粒度的简单启发式方法。我们将提供两个在设计服务时应该考虑的因素的示例加以说明:

1.服务将通常作为测试和发布的单位。如果粒度过粗,而将大量操作分组到单个服务中,则可能将增加服务的使用者。因此,如果我们对服务的某些方面进行更改(可能仅为了其中一些使用者的利益),则必须重新发布整个服务,从而可能影响所有使用者。

2.服务使用者所面临的一个挑战就是找到正确的操作。通常,使用者需要浏览服务列表,然后在标识了合适的服务后浏览服务操作列表。我们认为,服务粒度的两个极端——提供仅有几个方法的很多服务,或数十或数百个操作均集中在几个服务中——都将对易用性造成影响。

这表明,在选择服务粒度时,我们可能需要在多个因素间进行折衷,如可维护性、可操作性和易用性。任何给定的 SOA 都应向服务设计人员提供指南,以便确定此类折衷方案。

服务应是内聚而完整的

既然认识到了在确定服务粒度时需要考虑周全,那么在确定哪些操作应组成服务时有什么注意事项呢?我们认为有两个对象设计概念很有用:内聚性和完整性。我们可将这些概念应用于服务接口。

我们希望创建功能内聚的接口,一组操作由于其功能相关而聚合到一起。我们发现,当评估内聚程度时,从服务使用者角度看待服务很有用。通过使用者的视角,我们会将重点放在服务的功能上。将此方法与使用以下内聚标准进行对比:

我们可以考虑基于功能实现的内聚性进行决策。是否应由于操作使用相同的算法分组到一起,或者由于均是使用相同主机上的 CICS 事务实现的而将其分组到一起?这些是实现细节,不应影响接口设计。

可以使用时间内聚性原则,即,将在短时间内一起使用的操作分组到一起,例如,RetrieveCustomerDetails、CheckCreditRating、CreateLoanFacility 和 TransferFunds 操作都可能在金融业务流程中依次出现。不过,时间内聚性并不意味着这些操作应该由同一个服务提供,CheckCreditRating 和 TransferFunds 就缺乏功能内聚性。

使用名词-动词对服务和操作进行命名的规则可以帮助我们将重点放在服务接口的功能内聚性上。我们可以问这样一个问题“这个动词是否是该名词所进行的操作?”

我们的第二个对象设计概念是完整性概念。在为已知使用者创建服务时,完整性的问题尤为值得注意。在这种情况下,我们通常会将重点放在满足所知的使用者需求上。请务必记住,服务应该为可重用的,因此需要考虑将来的使用者的可能需求。举个简单的例子,假如有个名为 CellPhone 的服务提供 Create、Update、Query、Delete 和 Deactivate 等操作。我们完全可以想象会需要对弃用的手机进行重新激活,因此应决定是否也应提供对称的 Activate 方法。

通过判断,我们应该应用完整性规则。如果不知道使用者需求,则可能很难提供正确的功能,因此就有可能存在将开发和测试工作浪费在提供将不会使用的操作上的风险。

服务应对实现细节进行封装

另一个对象设计原则(封装)也适用于设计服务接口。我们封装服务实现的细节——所用的算法和资源——的动机在于增加服务使用者和提供者之间的分离,从而为将来扩展提供灵活性。

服务适应多种调用模式

WebSphere? 等提供的 Web 服务技术允许进行更高层次的封装。服务使用者通过使用各种调用模式,可以使用完全相同的代码技术来调用 Web 服务,如以下这些模式:

1.使用 SOAP over HTTP 的传统同步调用

2.使用 SOAP over JMS 的基于消息的异步调用

3.使用 Java 过程调用的本地调用

不过,虽然 Web 服务基础结构可以封装调用的细节,从而简化代码,但服务设计也应对调用方式加以考虑。对比一下本地调用和远程调用。与清单 3 所示内容类似的服务设计可以提供有价值的业务功能,但却不适合在很多 SOA 环境中部署。

清单 3. 繁忙型服务接口

LibraryCatalogService {
// search operations elided

String getBookTitle(String isbn)

String getBookAuthor(String isbn)

Date getBookPublicationDate(String isbn)

// further operations elided
}

在本地调用时,清单 3 所示的服务接口可能能够正常工作。不过,如果服务是在远程位置向使用者提供的,则该服务在常见使用场景中的性能可能会很差。例如,在使用服务检索数据来填充显示书的目录项的屏幕时,将有必要进行多个独立的远程调用,以检索书名、作者和出版日期。进行这些调用可能会有很大的性能损失。远程服务应提供粗粒度的操作,以在单个调用中检索关于某本书的所有信息。

可远程调用的服务的这个设计原则得到了广泛的认可;我们在此处强调此原则的目的在于说明被封装的服务调用细节可能给我们如何选择设计方法带来很大的影响。我们认为,同步调用和异步调用之间的选择也可能对服务接口设计有类似的影响。

这就引入了一个重要的问题:设计服务时,什么决定了所使用的调用方式?服务设计人员是否可以自由选择本地调用和远程调用、同步调用和异步调用?我们建议 SOA 应对这方面进行说明。我们提出此建议有两个原因。首先,我们希望通过确保一致性提高易用性;编排流程时,服务最好具有可预测的特征。其次,我们希望通过将使用者与提供者分离来提高灵活性。通过鼓励进行远程调用,我们可以进行位置、平台和编程语言分离。通过鼓励进行异步调用,我们可以分离使用者和提供者的可用性特征。

如果 SOA 要具有描述性,是否应声明所有服务都应设计为允许远程、异步调用?我们建议对此描述性采用更为细粒度的方法。可能的服务类型包括提供业务相关较多的操作,如 PlaceOrder,也包括技术性侧重较多的操作,如 CheckUserInRole。SOA 完全应该对不同的服务类别进行不同的描述。我们预期将更多地调用与业务相关的操作,而技术操作完全可能采用本地调用的方式。

服务具有无状态接口

我们在服务应设计为可重用中提到,应该将服务设计为可伸缩且可部署到高可用性基础结构中。此总体原则的一个推论就是,服务不应为有状态型的。即,它们不应依赖于使用者和提供者间长期存在的关系,操作调用也不应隐式地依赖于前一个调用。为了说明这一点,我们以下面的电话转换为例:

清单 4. 有状态转换

Q:What is Dave's account balance?

A: It's £320

Q:What's his credit limit?

A:It's £2,000

此示例演示了转换的两个有状态方面。第二个问题通过使用“his”引用第一个问题。这个示例中的操作依赖于转换上下文。现在让我们考虑一下所提供的应答。请注意,响应中没有上下文信息。应当只有在询问者知道所询问的问题时,这个应答才有意义。在此示例中,要求使用者维护对话状态,以便解释所得到的应答。这两个有状态关系(连续的调用之间和请求与响应之间的关系)都与 SOA 服务设计有关。

首先,我们考虑一下依赖于前一操作建立的上下文的操作。假如这是一个与呼叫中心的交互。只要与同一个操作人员对话,对话就可以有效地结束。但我们假设呼叫被中断了,如下所示:

清单 5. 被中断的有状态转换

Q:What is Dave's account balance?

Operator 1: It's £320

An interruption occurs, and the caller talks to a different operator.

Q:What's his credit limit?

Operator 2: Who?

中断导致上下文丢失,因此第二个问题是没有意义的。就这个电话对话而言,我们可以通过重新建立上下文而抵消中断带来的后果:“我在问 Dave 的银行帐户的信息,您能告诉我他的信用额度吗?”不过,在可扩展服务调用领域,有状态对话通常更为麻烦,在此领域中,重新建立上下文可能在技术上不可行,或者可能带来很大的性能开销。

通常,构建可伸缩的可靠基础结构与允许有状态交互之间有紧密的关系。创建支持有状态的服务调用的 SOA 基础结构在技术上可行。可以使用的技术包括:

1.使用 Http Cookie 维护会话上下文

2.使用有状态会话 EJB;Bean 的句柄在 SOAP Header 中传递

不过,我们必须仔细考虑最终基础结构的可伸缩性和可靠性。是否要求使用关联性?即,相同的使用者发出的连续请求是否必须交付到相同的提供者实例?要求使用关联性是一种有状态性与可伸缩性及可靠性冲突的情况。如果基础结构可以随意将请求提交到多个提供者实例中的一个,就可简化负载平衡,而各个提供者实例的可靠性要求就可缓和。

如果没有关联性需求,且允许基础结构将一个使用者的连续请求交付到不同的提供者实例,则任何会话状态必须对所有提供者实例可用。应用服务器基础结构提供会话复制机制。此类机制可以用于提供会话状态,但使用它们会有性能损失。而且,我们的 Web 开发经验表明,如果没有可靠的指南,开发人员将可以随意使用会话状态;过度使用 HTTP 会话通常是导致性能低下的常见原因。请参阅“Performance Analysis for Java Web Sites”(作者:Joines、Willenborg 和 Hygh,第 59 页—60 页,Addison-Wesley ISBN 0201844540)。

我们强烈建议,服务应设计为可避免维护会话上下文的需求。

现在,让我们考虑一下对话的其他有状态方面以及请求和响应间的关系。我们是否要采用上面的电话对话方式进行服务设计,依赖会话上下文来解释响应“What is Dave's credit limit?”——“£320”——然后我们将对 SOA 基础结构再次进行约束。

基础结构必须适应各种可能性,如某些使用者无法在临时停机的情况下保留其会话状态。

我们可以通过将服务设计为在响应中包含合适的关联信息,从而避免会话状态的需求,例如以下的响应:

清单 6. 包含关联信息的对话

Q: What is Dave's credit limit?

A: Dave's credit limit is £2000

该响应既标识人员又提供具体的数据。当包装遗留系统时,通常由适配器负责提供此类关联信息。现有同步 API 完全有可能不提供关联数据。在响应中包含关联信息之所以是很好的做法,有很多原因。首先,它简化了弹性可伸缩解决方案的构造,还能提供很大的诊断帮助,且在不可能向原始请求程序交付错误响应时非常重要。未交付的消息可能放置在错误队列上,每个此类消息的解释都要求使用上下文信息。

总之,仔细地进行服务设计可以避免对状态对话的需求,从而简化可靠的可伸缩 SOA 基础结构的实现。

服务应使用状态事务建模

前面给出了一个总的建议,以避免依赖对话状态,但我们应当记住,有用的计算机系统通常将为有状态的;通常反映了业务对象的生命周期。

例如,考虑购物中的一个订单的生命周期:创建订单。从用户的角度来看,创建了一个空的购物车。用户将随后向订单添加物品,即将其放入购物车中。最后提交订单,然后订单将被传送给配送部门。图 1 显示了对此生命周期建模的简化状态转换图。

图 1. 订单生命周期状态转换

该模型说明了一些有状态的行为。例如,我们看到,在处于 Open 状态时可以向订单添加行式项目,但在提交后就不能了。

让我们考虑以下 Order 服务的设计。我们可以采用如清单 7 所示的接口。

清单 7. Order 服务设计

OrderService {

void addLineItemToOrder(int orderId, int productId, int quantity)

void assignOrderToPacker(int packerId)

int createOrder(int customerId) // returns id of new order

int packItemForOrder(int orderId, int quantity) // returns quantity left to ship

boolean shipOrder(int orderId) // returns whether all order is now shipped

void submitOrder(int orderId)

// ... query operations elided ...

我们要考虑此接口的易用性。(更现实一些,我们应该考虑那些具有更多方法的完整接口,如用于列出和删除行式项目的方法。)如果没有状态图供参考,即使查看我们的这个小示例,也非常难于分清方法应该按照何种顺序进行调用。因此我们认为,服务设计人员必须进行一定的工作,以简化使用者的任务。我们提供了一些可能的技术。

首先,考虑操作和参数的名称。我们上面的示例中的名称是经过细心选择的,并进行一定的努力,以推导出方法的可能调用顺序。请比较清单 8 和清单 9 中的示例,这两个代码片段几乎完全一样,只是操作和参数的名称不同。

清单 8. 不合理选择的操作和参数名称

ZettuylService {
int wibble(int wibId, int wobId, String which);
int wobble(int quibId);
boolean wrubble(int wibId);
void quibble(int widId)
void quash(int wibId)
Stuff[] getStuff(int wibId );
void quite(int wibId);
Things[] getThings(int wibId);
void hinge(int wibId, intwobId);
int henge(int wibId , Stuff someStuff)
}

清单 9. 合理选择的操作和参数名称

ExpenseService {
int approveClaimItem(int claimId, int itemId, String comment);
int createClaim(String userId);
boolean auditClaim(int claimId);
void approveClaim(claimId)
void returnClaim(claimId)
ClaimItemDetails[] getClaimItems(int claimId );
void payClaim(int claimId);
ClaimErrors[] validateClaim(int claimId);
void removeClaimItem(int claimId, int itemId);
int addClaimItem(int claimId, ClaimItemDetails details)
}

清单 8 中的名称都是难以理解的。而清单 9 中选择的名称说明了服务的目的,并可以减少很多操作应按特定顺序调用的情况。例如,createClaim() 将在 approveClaim() 前使用,而后者又将在 payClaim() 前使用。因此,正如我们在前面的命名服务时应以最大化易用性为目标所指出的,名称的选择对易用性影响非常大。

其次,前面的 Order 的状态转换图可清楚说明订单的有状态行为。该图提供了有用的说明信息,显示了订单的状态以及每个状态中相应的操作。

增加易用性的第二项技术是,要记住并非所有记录的值都可以通过服务接口定义实现最佳的交付。记录良好的 WSDL 文件很有价值,但一起提供的关系图和示例也有很大的价值。

增加易用性的另一项技术是,创建反映业务对象生命周期的状态的服务接口。在我们费用申领示例中,每笔费用申领的生命周期都包含四个状态,如图 2 中所示。

图 2. expense 对象的四个状态

这些状态之所以重要,有两个原因。第一,每个状态基本上都与不同的系统用户相关。例如,当费用申领处于构建 (building) 状态时,主要系统用户是输入费用申领详细信息的申领人,而在审核 (auditing) 状态中,则是由具有批准权利的人对费用申领进行检查。

第二,主要状态之间的转换通常反映了不同 IT 系统之间的数据流。例如,在构建 (building) 状态期间,可以在用户的工作站上使用一个瘦客户端应用程序来捕获费用申领。提交后,费用申领将传递给费用申领处理系统,而当得到批准后,费用申领将传递给另外一个系统,即支付系统。在传递过程中,我们需要提到的是,如果实现的确要将数据从一个系统传递到另一个系统,则需要尤为注意负责在系统之间进行传输的操作(在我们的示例中为 submitClaim() 和 approveClaim())。它们的实现将需要对两个系统进行更新,而这样很容易丧失两个系统中任意一个的可用性。这些方法的实现将可以通过使用异步排队机制得到改善。

由于业务对象状态常常能同时反映业务和技术两方面的内容,因此完全可以将原始 ExpenseClaimService 拆分为适应每个状态的多个服务。我们可以得到如清单 10 中所示的服务。

清单 10. 根据状态划分服务

ClaimEntryService {
createClaim(String userId);
ClaimItemDetails[] getClaimItems(int );.
ClaimErrors[] validateClaim(int claimId);
void removeClaimItem(int claimId, int itemId);
int addClaimItem(int claimId, ClaimItemDetails details)
int submitClaim(int claimId);
}

ClaimApprovalService {
int approveClaimItem(int claimId, int itemId, String comment);
void approveClaim(claimId)
void returnClaim(claimId)
ClaimItemDetails[] getClaimItems(int );.
ClaimErrors[] validateClaim(int claimId);
}

ClaimPaymentService {
void payClaim(int claimId);
}

通过这种方式,能更方便地理解每个服务。而且,将接口这样划分将可能非常适合服务(或服务集)的开发、部署、维护和使用方式。这些服务很可能针对不同的使用者,可以由独立的开发团队进行开发,可以分开部署,因而具有分离的版本周期。换句话说,通过将重点放在对象生命周期上,我们就可以建立具有恰当粒度的服务。

服务操作设计原则

前面我们讨论了服务的总体设计方面的问题,接下来就要讨论一下各个服务操作的设计了。

操作表示业务动作。

我们已经指出,总的原则是,我们应该优先对服务和操作使用业务领域的名称,使用动词作为操作名称。对于操作,我们将这个建议进一步深化:应当使用具体的业务含义而不是泛型操作对操作进行定义。例如,不要使用泛泛的 UpdateCustomerDetails 操作,而要创建 ChangeCustomerAddress、RecordCustomerMarriage 和 AddAlternativeCustomerContactNumber 之类的操作。此方法具有以下好处:

操作与具体业务场景对应。此类场景可能不仅是简单的更新数据库中的记录。例如,更改地址或婚姻状况可以要求生成正式的文档,而将要求系统记录该文档的详细信息——或扫描版本。如果使用不太具体的操作(如 UpdateCustomerDetails),则较难实现此类业务场景。

各个操作接口将非常简单,且易于理解,从而提高了易用性。

每个操作的更新单元得到了清楚的定义(在我们的示例中为地址、婚姻状况和电话号码)。在实现具有高并发性要求的系统时,我们可以基于操作的要求采用更细粒度的锁定策略,从而减少资源争用。

操作应采用粗粒度参数

在讨论操作参数时,同样要面对粒度的问题。请比较清单 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 版本的一些记录更新。请仅在从我读取该记录后没有人进行修改的情况下进行更改。”

以下是为清单 12 所示的同一个模型使用了数据库触发器和修订计数器的乐观锁定实现。该实现要求执行以下步骤:

1.向要使用乐观锁定的表中添加一个额外的整数列;该列保存修订计数器。

2.向数据库添加触发器,以便对该表中的记录的每个更新都会导致修订计数器递增。

3.所有检索操作均会返回包含修订计数器的数据项。

4.所有更新操作都必须包含从检索获得的修订计数器。

更新操作实现必须对数据库记录进行限定的更新操作,如“如果修订计数器等于...则更新记录...”如果其间对记录进行了任何修改,此更新操作将失败——在其间,如果出现了更新,则会触发更新触发器,因此会修改修订计数器。

如果由于其他使用者在其间进行了更新而导致更新失败,则将向使用者报告一个特定的错误。

请注意,此实现在更新时要求使用者提供正确的修订计数器;进行纠正的责任分散到数据库、提供者和使用者三者身上。另外,还请注意,此实现设计真的非常乐观;如果争用的概率很低,就能很好地工作。如果可能出现更新冲突,则重试的性能开销将非常大。另外,还需要一些其他可能的乐观锁定策略和详细设计,以制定合适的并发方案。

考虑到管理并发更新的相对复杂性,我们提出一个相关的建议:尽可能使用无状态语义。例如,与实现等效的“Retrieve record”-“Write record”两个操作(使用者会在检索和写入操作间使值递增)相比,可能实现具有良好并发行为的单个操作“Increment balance by X”更为容易。

结束语

我们并不认为文中包含了全部设计原则。而是希望通过这些原则能够说明,每个 SOA 都需要慎重地为其企业确定一组恰当的原则,并随后确定每个服务创建人员都能遵循这些原则。

   
2030 次浏览       15
相关文章

多维方法来开发有机的业务流程架构
SCA 应用程序开发
BPM 和 SOA 性能最佳实践
实现企业服务总线模式
相关文档

SCA介绍及应用实例
基于SOA架构的ESB平台:Infomagic
SCA架构
SOA的基本概念
相关课程

面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践