(有时候,自己也不喜欢长篇大论的纯文字性的素材,不过当你面对成堆的文档时你慢慢会适应!看到一段对于服务契约的分解与设计,不错的分享一下!)
服务契约的分解与设计
如果不考虑语法因素,我们应该如何设计服务契约?如何知道服务契约中应该定义哪些操作?每个契约又应该包含多少操作?解决这些问题与 WCF 技术并无太大关系,更多地属于抽象的面向服务分析与设计的范畴。如何将系统分解为服务,以及如何剖析契约方法,并不在本书讨论范围之内。不过,本节仍然给出了一些建议,以指导开发者更好地设计服务契约。
(1).契约分解
一个服务契约是逻辑相关的操作的组合。所谓的“逻辑相关”通常指特定的领域逻辑。我们可以将服务契约想象成实体的不同表现。一旦识别(在需求分析之后)出实体支持的所有操作,就需要将它们分配给契约。这称为服务契约的分解(Service Contract Factoring)。分解服务契约时,通常需要考虑可重用元素(Reusable Element)。在面向服务的应用程序中,一个可重用的基本单元就是服务契约。那么,系统的其他实体能否重用这些被分解出的服务契约?实体对象的哪些职责能够被分解出来,哪些职责又能被其他实体所调用?
让我们考虑一个具体而又简单的实例。假定我们希望对一个狗的服务建模。需求说明狗能叫能吃,拥有一个兽医诊所的注册号,可以对它注射疫苗。我们可以定义一个 IDog 服务契约,并让不同的服务如 PoodleService(狮子狗)和 GermanShepherdService(德国牧羊犬)实现 IDog 契约:
[ServiceContract]
interface IDog
{
[OperationContract]
void Fetch();
[OperationContract]
void Bark();
[OperationContract]
long GetVetClinicNumber();
[OperationContract]
void Vaccinate();
}
class PoodleService : IDog
{...}
class GermanShepherdService : IDog
{...}
然而,IDog 服务契约的定义并没有体现职责分离的原则。 虽然这些操作都是狗所应具有的,但是 Fetch()和 Bark()方法与 IDog 服务契约的逻辑关联性,远远强于
GetVetClinicNumber()和 Vaccinate()方法。Fetch()和 Bark()体现了狗的本性,与它的日常生活有关,属于实例化的犬类实体的职责。GetVetClinicNumber()和 Vaccinate()则体现了不同的特性,它们与兽医诊所的宠物记录有关。一个最佳方案是将 GetVetClinicNumber()和 Vaccinate()操作分解出来,形成一个单独的 IPet 契约:
[ServiceContract]
interface IDog
{
[OperationContract]
void Fetch();
[OperationContract]
void Bark();
[OperationContract]
long GetVetClinicNumber();
[OperationContract]
void Vaccinate();
}
class PoodleService : IDog
{...}
class GermanShepherdService : IDog
{...}
由于宠物的职责不依赖于犬类实体,因此其他实体(例如猫)可以重用以及实现 IPet 服务契约:
[ServiceContract]
interface ICat
{
[OperationContract]
void Purr();
[OperationContract]
void CatchMouse();
}
class PoodleService : IDog,IPet
{...}
class SiameseService : ICat,IPet
{...}
契约的分解实现了应用程序中诊所管理职责与实际服务(狗或者猫)之间的解耦。将操作分解为单独的接口,是服务设计中常见的做法,它能够降低操作之间的逻辑关系。但是,有时候在几个不相关的契约中会找到相同的操作,这些操作与它们各自的契约存在一定的逻辑关系。例如,猫和狗这两种动物都会脱毛,都能够哺育后代。从逻辑上讲,脱毛与犬吠一样,都属于狗的服务操作;同时它又与猫叫一样,属于猫的服务操作。
此时,我们将服务契约分解为契约层级的方式,而不是单独的契约:
[ServiceContract]
interface IMammal
{
[OperationContract]
void ShedFur();
[OperationContract]
void Lactate();
}
[ServiceContract]
interface IDog : IMammal
{...}
[ServiceContract]
interface ICat : IMammal
{...}
(2).分解原则
显而易见,合理的契约分解可以实现深度特化、松散耦合、精细调整以及契约的重用。这些优势有助于改善整个系统。总的来说,契约分解的目的就是使契约包含的操作尽可能少。
设计面向服务的系统时,需要平衡两个影响系统的因素(参见图 2-1)。
一个是实现服务契约的代价,一个则是将服务契约合并或集成为一个高内聚应用程序的代价。
如果我们定义了太多的细粒度服务契约,虽然它们易于实现,但集成它们的代价未免太高。另一方面,如果我们仅定义了几个复杂而又庞大的服务契约,虽然集成的代价可能会降低,但却制约了契约的实现。
实现契约的代价与服务契约的规模并非线性的关系,当契约的规模增加两倍时,复杂度会陡增至四到六倍。与之相似,集成契约的代价与服务契约的数量同样不是线性关系,因为参与的服务数与它们之间关联点的数目不是线形的。
对于任何一个系统,实现契约所付出的代价,包括设计服务以及维护服务的代价,等于上述两个因素的总和(实现的代价与集成的代价)。图 2-1 的一个区域显示了最小代价与服务契约规模和数量之间的关系。一个设计良好的系统,服务的个数与规模应该恰如其分,遵循平衡的“中庸之道”,力求达到“增之一分则太多(大),减之一分则太少(小)”的标准。
由于契约分解与使用的服务技术无关,对于职责分离以及大规模应用程序的架构设计,我们只能根据自己或他人的经验,总结出关于服务契约分解的规则和方法,与读者分享。
首先,我们应该避免设计只具有一个操作的服务契约。一个服务契约体现了实体的特征,如果服务只有一个操作,则过于单调,没有实际的意义。此时,就应该检查它是否使用了太多的参数?它的粒度是否过粗,因此需要分解为多个操作?是否需要将该操作转移到已有的服务契约中?
服务契约成员的最佳数量(根据经验总结,仅代表本人观点)应介于 3 到 5 之间。如果设计的服务契约包含了多个操作,例如 6 到 9 个,仍然可能工作良好。但是,我们需要判断这些操作会否因为过度分解而需要合并。如果服务契约定义了 12 个甚至更多的操作,毫无疑问,我们需要将这些操作分解到单独的服务契约中,或者为它们建立契约层级。开发者在制订 WCF 编码规范时,应该指定一个上限值(例如 20)。无论在何种情况,都不能超过该值。
另一个原则关于准属性操作(Property-Like Operation)的使用,例如:
[OperationContract]
long GetVetClinicNumber();
我们应该避免定义这样的操作。服务契约允许客户端在调用抽象操作时,不用关心具体的实现细节。准属性操作由于无法封装状态的管理,因此在封装性的表现上差强人意。在服务端,我们可以封装读写变量值的业务逻辑,但在理想状态下,我们却不应该干涉客户端对属性的使用。客户端应该只负责调用操作,而由服务去管理服务对象的状态。这种交互方式应该被表示为 DoSomething()样式,例如 Vaccinate()方法。服务如何实现该方法,是否需要设置诊所号,都不是客户端需要考虑的内容。
需要注意的是,这些分解原则,包括经验法则与通用规律,只能作为帮助开发者核算和评估特定设计的工具。它不能替代领域专家的意见与经验。“实践出真知”,应用这些指导原则时,需要做出合理的判断,甚至提出质问。
(3).契约查询
有时候,客户端需要通过编程方式验证一个特定的终结点(通过地址进行识别)是否支持一个特定的契约。设想有这样一个应用程序,终端用户在安装时(甚至在运行时)指定或配置应用程序,用以使用服务并与服务交互。如果服务不支持所需的契约,应用程序就会向用户发出警告,提示配置的地址是无效的,询问是否更正地址或替换地址。例如,使用的证书管理器应用程序(Credentials Manager Application)就具备这样的特征:用户需要为应用程序提供管理账户成员与角色的安全证书服务的地址。在验证了地址支持所需的服务契约之后,证书管理器只允许用户选择有效的地址。