面向服务和面向对象的第二部分:设计原则的比较

日期: 2008-05-28 作者:Thomas Erl翻译:Eric 来源:TechTarget中国 英文

面向对象的设计包括一整套的基本设计原则及补充设计原则,构成并规范类内部及类之间的对象逻辑。面向对象的这些原则中,有一些已经不同程度的用于面向服务,其它的一些则被完全省略。   本文讨论了面向服务与面向对象的每一个设计原则之间的关系:   封装规则  继承规则  泛化和特化规则  抽象规则  多态性规则  开放-封闭原则(OCP)   不要重复自己原则(DRY)   单一职责原则(SRP)   委派规则  关联规则  组合规则  聚集规则   通过了解面向对象设计与面向服务设计之间的关系,我们可以找出个别面向服务原则的几个具体起源。但更重要的是,我们可以确定设计服务与设计对象之间存在哪些具体的不……

我们一直都在努力坚持原创.......请不要一声不吭,就悄悄拿走。

我原创,你原创,我们的内容世界才会更加精彩!

【所有原创内容版权均属TechTarget,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用。】

微信公众号

TechTarget微信公众号二维码

TechTarget

官方微博

TechTarget中国官方微博二维码

TechTarget中国

面向对象的设计包括一整套的基本设计原则及补充设计原则,构成并规范类内部及类之间的对象逻辑。面向对象的这些原则中,有一些已经不同程度的用于面向服务,其它的一些则被完全省略。

  本文讨论了面向服务与面向对象的每一个设计原则之间的关系:

  封装规则
  继承规则
  泛化和特化规则
  抽象规则
  多态性规则
  开放-封闭原则(OCP)
  不要重复自己原则(DRY)
  单一职责原则(SRP)
  委派规则
  关联规则
  组合规则
  聚集规则

  通过了解面向对象设计与面向服务设计之间的关系,我们可以找出个别面向服务原则的几个具体起源。但更重要的是,我们可以确定设计服务与设计对象之间存在哪些具体的不同。

  封装规则

  封装的意思就是将东西密封在容器里面。在面向对象中,封装是与信息隐藏相关联的,以及由相应的信息结构聚集成的逻辑整体。这个规则表述的意思是,一个对象应该只能通过一个公共接口访问,而且实现过程应对其他对象保持隐藏。
 
  对象就是容器

  面向对象的封装规则相比于面向服务的服务抽象规则,它也是强调对信息的刻意隐藏。

  在面向服务中,服务仍然是对逻辑和实现过程进行封装,这和对象的作用一样(因为服务和对象一样,也是容器)。但是,“封装”这个词却被更多的用来是指被容器封闭(封装)的东西。事实上,服务的封装与一个基本的SOA设计模式有关,该模式应用于判定什么样的逻辑适合成为给定服务的一部分,什么样的逻辑不适合。

  图1:虽然两个设计范式中封装规则背后的含义都相同,但是各自使用的术语略有不同。

  继承规则

  继承是面向对象实现代码复用的主要手段,通过把逻辑有机组合到类中,继而建立类之间的关系加以实现。类间关系有多种不同类型,继承是其中最正式的关系。

  两个类可以形成父子关系,子类能够自动获得(继承)父类的方法和属性。当两个类以这种方式关联,那么我们把父类称为子类的超类,而子类则是父类的低级类。子类可以实现超类能做的任何事情,而且可以进一步扩展其独特的功能(通过一个所谓“特化”的过程)。
 
  超类及其子类之间形成的具体关系往往被标示为“is-a”关系,因为无论子类以什么形式存在,它们都是超类中被定义的概念的实现。这种关系用空心的三角箭头表示,如图2所示。由于侧重于个体服务的自治性及减少服务之间的耦合,因此,在面向服务中,我们一般不提倡服务之间的继承关系。而且,由于服务间的互相实现并不正式,因此也不需要建立一种“is-a”的关系。  

  图2:通过继承,子类(左下角)代表具体类型的商业文件,可以实现一个抽象的超类(左上角),从而获得与之相同的方法和属性。而代表商业文件的实体服务(右)可能有相似的功能,但都不是通过继承获得的。

  注:对于继承获得的属性及方法,可以通过明示的继承关系而认定存在,所以在子类中要求不再重复命名,这在某些领域已经成为一种OOAD惯例。在这一章,我有意在所有的例子中显示继承获得的属性及方法,是为了能更清楚地与相对应的服务定义进行比较。

  另外还要注意,在Web服务中,可以通过WSDL 2.0中interface元素的扩展属性[REF-2] 实现向上的接口继承。

  泛化和特化规则

  一个精心设计的最高层超类(也称为抽象类或基类),其所表示的高度泛型接口要具有广泛的适用性。因此,我们需要对一系列的子类进行定义。

  在定义父类超类时即实现了泛化。而子类是对超类实现独特(特化)的改变,因此子类的定义被称为特化。泛化表示的是两个类之间为“is-a-kind-of”关系,而特化表示的是之前所说的“is-a”关系。

  在面向服务中也有与泛化和特化类似的概念,只是因为面向服务不支持继承,所以这些概念有些不同。在服务设计的上下文中,泛化和特化与粒度有直接的关系。服务特化程度越高,服务层的粒度就越大。

  合理确定每个服务的特化程度非常关键,尤其是在创建服务功能的上下文和具体的范围时。不过,根据功能及实际的要求,我们可以通过使用服务设计模式,将现有的粗粒度(更泛化)服务分解成细粒度(更特化)服务,但这并不是继承的结果。

  图3:三层体系(左)包括Invoice类——这是泛化商业文件类的特化,以及Invoice Detail和Invoice Header类——这是Invoice类的聚集。从技术上讲,我们不认为Invoice Detail类和Invoice Header类是特化的,因为它们不是基于“is-a”的关系。而另一方面,由于Invoice Detail和Invoice Header服务(右)比Invoice服务具有更高水平的服务粒度,因此可以认为是更特化的(从传统的OOAD意义上讲则不然)。(注意,聚集关系使用菱形符号表示。这将在下面的聚集规则一节中进一步阐释) 。

  注:通过创建泛化类定义的上述基类或抽象类,因为不会从中产生任何实例或对象,因此在现实中实际上并没有实现。相反,它们存在的目的是为了创建继承结构及特化的子类。在面向服务中,抽象类的使用是自愿的,具体内容详见本篇文章的结尾部分。 

  抽象规则

  在面向对象中,另一个与信息隐藏相关的规则是抽象规则。具体来说,抽象的目的就是要创建一种简化类,隐藏潜在实现的复杂性,而只暴露最必要的(抽象)方法和属性。有一些抽象类没有被实现,而是形成了父类超类,其中有大量的特化子类需要被定义和实现。因此,为了定义这一种抽象类,我们使用抽象规则来为继承规则提供支持。 

  从概念上来说,面向对象的抽象规则类似于服务抽象规则,因为两者最终都是为了简化与基本求解逻辑和实现细节有关的公共信息。但是,由于面向服务不支持继承规则,所以在面向服务中没有与抽象类相对应的概念。 

 

  图4:面向对象的抽象规则(左)主要重视对其他用户程序隐藏复杂性(在这种情况下,即潜在实现)的隐藏,而服务的抽象规则(右)也限制了人们对潜在服务细节的访问和了解。 

  多态性规则

  当多个面向对象的子类继承和保留父类的同一种方法时,可以通过建立方法名称相同的多个类实现。因为每个子类特化的方式都是独特的,所以即使方法的定义相同,各个子类的实现也会有所不同。因此,将相同的信息发送给这些子类中的任何一个,由于子类实现的差异,出现的结果也会不同。这就是所谓的多态性。 

  因为面向服务中不存在继承关系,所以多态性这种形式并不适用于个体服务。面向服务可以支持实现的、与多态性最相近的是遵循标准化服务契约(Standardized Service Contract)原则,对服务契约的一致性功能表述。这通常导致许多服务的功能有着相似或相同的名称。(在实体服务中,标准化的基于操作符的CRUD类操作的一贯使用,就是这方面的一个例子)。

  这项接口一致水平是命名惯例应用于契约设计的结果。不同服务中,有着相同名称的功能当然不能支持同一条信息。因此,我们不认为它是真正的多态性。

  图5:继承获得getStatus方法的特化子类(左),每一个都能处理相同的输入信息。另一方面,Purchase Order和Invoice服务(右)分别需要各自具体的输入信息,才能表达GetStatus功能。

  开放-封闭原则(OCP)

  OCP是一项基本的设计原则,其表述的意思是类应该允许(开放)扩展功能,但禁止(封闭)对已实现的类进行修改。这是一项重要的设计要求,有利于保护多个客户程序已经产生依赖的可复用功能。它完全适用于服务契约,而且要求在应用服务可复用性(Service Reusability)原则时必须遵循,以尽量减少后续管理的成本。

  图6:位于右上方的类因为重命名(不是覆盖)了已实现的方法,因而违反了这个原则;而位于底部的每一个服务,只是对服务契约进行了扩展,从而遵守了这个原则。

  不要重复自己(DRY)原则

  通过避免代码的重复,我们可以更有效地复用对象,最大程度的减少多余的开发工作。这个原则简单说来就是,如果存在可复用的逻辑,那么,我们就应该将其分离出来,以便于重复使用。这个原则背后的基本原理为服务正常化(Service Normalization)模式以及不可知服务模型的使用提供了基础。另外,通过避免功能的重叠,我们能在逻辑集中(Logic Centralization)模式支持的环境中避免服务设计的重复。

  图7:位于左侧的类和服务都被进一步分解,从而分离出可重复使用的逻辑,应用到单独的不可知功能的上下文中,表示为位于右下方的类和服务。 

  单一职责原则(SRP)

  求解逻辑设计的面向对象单元,都完美的围绕着同一个总目标。单一职责原则要求我们根据这个总目标限制所有单元的功能范围,使其只能随着该目标的改变而改变。

  在面向服务中,与其相对应的是服务模式的一贯使用,该模式用于建立独特的功能服务上下文。例如,实体服务,应用于与业务实体相关的处理过程。同样地,任务服务也只有唯一的目标,就是实现具体业务流程的自动化。

 

  图8:粗粒度的、具有双重目标的Client Account类和服务(左)被分解为两组行为,分别代表两种截然不同的、具有单一目标的功能上下文(右)。 

  单一职责原则与内聚的概念密切相关。类或服务在定义(并遵循)一个具体的功能上下文时,就提高了其内聚度,从而能被建设成可容纳一组高度相关的方法或能力的容器。而另一方面,具有低内聚度的类或服务,则因为定义了一个包含多重目标的上下文,而违背了这个原则。 

  注意,内聚度与服务粒度并不总是密切相关的。一个粗粒度的服务,仍然可以具有较高的内聚度。但是,由于服务的功能范围要促进大量目标或职责的实现,这自然会导致粒度变低(更广泛),而具有低内聚度的服务通常都具有较粗的粒度。

  注:在前几节中,我们借助服务可重用性(Service Reusability)原则,为将服务定位为多用途的企业资源提供了参考。在这里,“多用途”一词是指一个服务(或任何一个服务功能)在多种情况中的重复使用。因为服务本身保留了一个具体的功能上下文及功能范围,因此,我们可以说它具有单一职责。 

  委派规则

  这个简单的规则是说,如果一个对象需要使用已经存在于另一个对象的逻辑,那么它应该将执行该逻辑的职责给予(代理)给这另一个对象,而不是自己执行逻辑。应用这一原则的关键条件是不能改变被委派职责的对象的行为。 

  这个基本设计思路的一贯使用完全支持服务可重用性原则,以及逻辑集中化的广泛实现。事实上,委派规则直接对应的是服务的基本设计模式,它需要从服务的功能上下文外部引用并复用逻辑,从而保持该上下文的完整性。在面向服务中,这个原则背后的基本原理是对服务组合的需要。 

  图9:Client类自己并不执行一系列invoice数据的检索,而是通过调用相应的Invoice类将该职责委派。Client服务的GetOwing功能,也是通过调用Invoice服务的GetUnpaid功能实现职责的委派。 

  关联规则

  在OOAD中,两个类之间的关联表示的是一种关系。不同的关系之间要求必须执行委派,从而使对象能够在运行时互相调用及通信。类之间的关联有许多不同类型,其中最简单的关联确立了允许两类之间交换信息的“uses-a”关系。这两个类可以互不相关而且相互独立;只不过是一个提供功能,另一个在用而已。

  另外,那些更正式的关联类型也定义了不同类型的关系。例如,聚集和组合规则,在具有所有权含义的类之间建立了“has-a”的关系(详见下面的部分)。服务的交互和聚集这种关联类型非常相似,因为服务只需要能够相互使用对方的功能,而不需要受与所有权有关的限制。这就是为什么通常使用与表示面向对象关联关系的箭头相同(或类似)的符号标识服务交互。

  图10:Client类和Purchase Order类(上方)之间的关联,与Client服务和Purchase Order服务(下方)之间的关联类似。

  组合规则

  在面向对象和面向服务中,组合的概念比较类似,但与封装的情况一样,这个词的使用不尽相同。在OOAD中,组合是指一种在类之间建立所有权结构的关联形式。一个父类由其他的类组成,那么,这个父类与其他的类之间建立的关系就是“has-a”关系。 

  此外,组合对象的寿命与父对象有关,这意味着当父对象不再存在时,组合对象也将被破坏。组合关系由端点为实心菱形的直线标识,菱形指向负责启动组合的类。 

  在面向服务中,“组合”一词是指一堆服务的集合或聚集,但没有事先对所有权结构进行定义。因此,与OOAD组合相关的规则并不适用于面向服务。服务可以自由调用其他服务的功能,而且负责启动组合的组合控制器服务实例,其运行时间并不需要跟其他的组合成员服务实例运行的时间相等。这种程度的自由,对充分实现服务组合的潜能非常重要。 

  图11:Client类组合成一个相关的Account类,并根据account(上方)的性质,决定由三个子类中的其中一个执行。这种情况下,Account接口归Client类所有,而且不能脱离其存在。而另一方面,Account服务是被不具有所有权含义的Client服务(底部)进行简单的调用。 

  聚集规则

  聚集这一规则与组合规则类似,但两者在参与的对象之间建立的关系略有不同。发生聚集关联的类仍然能在“has-a”关系的基础上建立一个所有权结构。不过,启动聚集的对象并不需要与其他参与对象拥有相等的寿命。换句话说,被聚集的类允许于作为聚集者的父类(容器)之外独立存在。聚集关系由端点为空心菱形的直线表示,菱形指向聚集其他类的类。 

  因为聚集仍然基于“has-a”的所有权结构,所以和组合一样,它也不适用于面向服务。而正如在关联规则一节中所提到的,服务的交互更类似于“uses-a”的关系。

  图12:Client类聚集了Invoice类(上方),因为Clients有invoices,而且invoices可以独立于Clients存在。不过,Client服务可以调用Invoice服务(下方),同样也可以调用其他任何服务。

  面向服务的类设计指南

  在这篇研究面向服务和面向对象原则的文章结束之前,有必要在OOAD惯例和类设计中构建面向服务的设计准则。 

  下面的章节介绍了一套面向服务的类设计准则,这些准则在您使用UML类符号进行服务建模时将会有所帮助。 

  实现类接口

  定位为服务的类应该一直能实现接口,从而确保正式的公共契约与可能还需隐藏的多余的类细节分开表示。这直接为标准化服务契约(Standardized Service Contract),服务松耦合(Service Loose Coupling)和服务抽象(Service Abstraction)等原则提供了支持。

  限制类对接口的访问

  这条准则基本上是契约集中化(Contract Centralization)模式和服务松耦合(Service Loose Coupling)原则所定义的用户-契约耦合类型的转换。这种积极的耦合形式能够阻止服务中以Web服务形式存在的消极的耦合形式,并以同样的方式保护潜在的类实现细节。 

  不要定义接口中的公共属性

  这已经成为OOAD中一项成功的做法,为了支持面向服务,有必要在这里重复提出。服务无状态性原则(Service Statelessness principle),提倡服务以求解单元的形式存在,从而能在适当的时候恢复到无状态。从公共接口中删除属性,将通过某些方法(存取方法或其他方法)推动通信,从而能对服务中状态的变化进行控制(这正是我们所希望的)。

  谨慎的使用继承

  我们试图通过服务松耦合(Service Loose Coupling)、服务自治(Service Autonomy)、服务可组合性(Service Composability)等原则, 寻求在每一个服务中建立独立和自由,而可以将这一切实现的面向服务却不正式提倡服务间的继承。服务内部的继承(继承被服务封装的类),可视需要用于加强服务内部的逻辑结构。不过要注意一个常识,在某些情况中,粗粒度服务可能需要被分解成细粒度(更特化)服务。我们可以按照相应的服务设计模式,通过设计服务契约和逻辑所用的方法,为服务分解做好准备。由构件组成的服务逻辑,通过继承结构被紧紧约束,如果潜在的类结构相互依赖性较弱的话,它将更加难以被分解成物理上分离的服务。

  避免服务间的“has-a”关系

  服务组合,需要各组合成员有独立于父类控制器行动的自由,即使这意味着在控制器实例被破坏后它们仍然处于运行状态。此外,服务不能仅限于预先确定的所有权体系的形式;按照服务可组合性原则,在理想情况下,服务还需要具备组合的功能或是被其他的任何一个服务在已有库存中组合的功能。

  除非需要有严格的规则限制对象寿命与父对象之间的关联,“uses-a”关联,相比于组合或聚集来说,是一个“对服务更友善”的组合类的方式。 

  使用抽象类进行建模,而不是设计

  正如在泛化和特化规则一节中所阐述的,在面向服务中,抽象类的使用不是必需的。因为没有定义正式的继承关系,所以对于待设计的其他服务来说,也不需要基服务或抽象服务。

  不过,使用抽象类可以为面向服务的分析阶段提供帮助(特别是对那些熟悉OOAD的人来说)。我们可以把抽象类非正式地定义为相关类集合的基类或根类,以保证定义服务候选功能上下文、服务功能候选过程中的连贯性。这样,抽象类的使用就能被纳入服务建模过程的自定义化中。 

  使用外观类

  在这篇文章中,我们没有讨论外观类的使用,因为它们在技术上代表了一种与服务外观(Service Fa?ade)模式相对应的面向对象设计模式(与设计原则相对)。不过,值得一提的是,从服务设计的角度来看,对于将构件结构组建为独立的服务,或是面向服务的Web服务中的一部分,外观类的创建都是一个非常重要而且通用的技术。

翻译

Eric
Eric

相关推荐