`
jsx112
  • 浏览: 306920 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

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 之类的技术在支持此方法方面充当着重要的角色。

服务应设计为可重用

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

  • 设计必须能适应不断增加的吞吐量;如果服务在使用服务的数量增加的情况下仍可成功运行,那么使用率也会成级数递增。
  • 如果使用服务的数量增加,则数据量和并发数据访问模式可能会与最初投入使用时的情况大为不同。
  • 我们必须对服务请求的未来增长进行预计;新使用者可能需要其他的功能,或者需要对现有功能进行更改


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

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

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

我们在选择服务、操作、数据类型和参数的名称时有一个指导原则:希望最大化服务的易用性。我们希望帮助流程开发人员标识实现业务流程所需的服务和操作。因此,我们强烈建议使用服务使用者专业领域内有意义的名称,优先选用业务概念而不是技术概念。我们的建议就是:应使用名词对服务进行命名;而应使用动词对操作进行命名。(举例:查询物料基本信息服务)

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

粒度 一词在 SOA 相关讨论中有多种不同的用法。在本文的服务设计讨论中,我们考虑的是服务本身的粒度,即服务应该包含的操作数量。没有可用于确定服务粒度的简单启发式方法。我们将提供两个在设计服务时应该考虑的因素的示例加以说明:

  • 服务将通常作为测试和发布的单位。如果粒度过粗,而将大量操作分组到单个服务中,则可能将增加服务的使用者。因此,如果我们对服务的某些方面进行更改(可能仅为了其中一些使用者的利益),则必须重新发布整个服务,从而可能影响所有使用者。
  • 服务使用者所面临的一个挑战就是找到正确的操作。通常,使用者需要浏览服务列表,然后在标识了合适的服务后浏览服务操作列表。我们认为,服务粒度的两个极端——提供仅有几个方法的很多服务,或数十或数百个操作均集中在几个服务中——都将对易用性造成影响。


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

服务应是内聚而完整的

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

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

  • 我们可以考虑基于功能实现的内聚性进行决策。是否应由于操作使用相同的算法分组到一起,或者由于均是使用相同主机上的 CICS 事务实现的而将其分组到一起?这些是实现细节,不应影响接口设计。
  • 可以使用时间内聚性原则,即,将在短时间内一起使用的操作分组到一起,例如,RetrieveCustomerDetails、 CheckCreditRating、CreateLoanFacility 和 TransferFunds 操作都可能在金融业务流程中依次出现。不过,时间内聚性并不意味着这些操作应该由同一个服务提供,CheckCreditRating 和 TransferFunds 就缺乏功能内聚性。


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

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

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

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

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

服务适应多种调用模式

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

  • 使用 SOAP over HTTP 的传统同步调用
  • 使用 SOAP over JMS 的基于消息的异步调用
  • 使用 Java 过程调用的本地调用


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

服务具有无状态接口

我们在服务应设计为可重用中提到,应该将服务设计为可伸缩且可部署到高可用性基础结构中。此总体原则的一个推论就是,服务不应为有状态型的。即,它们不应依赖于使用者和提供者间长期存在的关系,操作调用也不应隐式地依赖于前一个调用。我们强烈建议,服务应设计为可避免维护会话上下文的需求。

服务应使用状态事务建模

前面给出了一个总的建议,以避免依赖对话状态,但我们应当记住,有用的计算机系统通常将为有状态的;通常反映了业务对象的生命周期。例如,考虑购物中的一个订单的生命周期:创建订单。从用户的角度来看,创建了一个空的购物车。用户将随后向订单添加物品,即将其放入购物车中。最后提交订单,然后订单将被传送给配送部门。

操作表示业务动作。

我们已经指出,总的原则是,我们应该优先对服务和操作使用业务领域的名称,使用动词作为操作名称。对于操作,我们将这个建议进一步深化:应当使用具体的业务含义而不是泛型操作对操作进行定义。例如,不要使用泛泛的 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 版本的一些记录更新。请仅在从我读取该记录后没有人进行修改的情况下进行更改。”

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

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics