WCF分布式开发步步为赢(6):WCF服务契约继承与分解设计

时间:2021-03-20 15:30:20

上一节我们学习了WCF分布式开发步步为赢(5)服务契约与操作重载部分。今天我们来继续学习WCF服务契约继承和服务分解设计相关的知识点。WCF服务契约继承有何优势和缺点?实际项目里契约设计有什么原则和依据?面向对象的设计经验有何值得借鉴的地方?这里我们会一一给出详细的介绍。本文首先介绍的是WCF服务中契约继承的一些概念、例子代码分析,其次来讲解服务契约的设计问题。首先介绍的也是进行服务设计的必要性,服务设计的原则,示例代码分析。最后是全文的总结部分。结构如下:【1】OO面向对象设计原则,【2】服务契约继承,【3】服务契约分解概念,【4】服务契约分解原则,【5】服务契约分解代码分析,【6】总结。

【1】面向对象设计原则OO:

    这里我们有必要先回顾一下面向对象的经典的设计原则。这些设计原则对我们WCF服务契约的设计来说有重要的参考价值。服务契约实际利用了接口来定义实现,语法类似,WCF框架也是基于现有的语言体系,对此扩展了编程模型,比如增加了属性设置机制等。如果你曾经接触过OO面向对象的这些概念,那么这些设计原则理解起来不会困难。很多编程书籍里都会有介绍,设计模式相关书籍里会有比较详细的介绍。这里介绍几个主要的概念,为下文的继承和设计WCF服务契约部分作铺垫:

 

<1>单一职责原则(SRP): 一个类应该仅有一个引起它变化的原因。

 

<2>开放封闭原则(OCP): 类模块应该是可扩展的,但是不可修改(对扩展开放,对更改封闭)。

 

<3>Liskov 替换原则(LSP): 子类必须能够替换它们的基类。

 

<4> 依赖倒置原则(DIP): 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。 抽象不应该依赖于实现细节,实现细节应该依赖于抽象。

 

<5>接口隔离原则(ISP): 不应该强迫客户程序依赖于它们不用的方法。

【2】服务契约继承:

服务契约的定义和接口定义类似,接口可以继承与多个接口。但是WCF契约属性是不支持继承的。由于WCF框架自身的问题,不支持契约属性的继承,因此这给我们服务契约属性的声明和使用却有不少限制。在使用契约继承属性的过程中腰注意服务端契约的属性继承问题,此外就是客户端添加服务引用后,无法还原服务端契约层级的关系,所有的操作契约由一个契约类封装。因此实际编程我们要兼顾到两个方面的情况。

【2.1】服务端契约层级:

接口支持继承。但ServiceContract特性不支持继承的,我们查看其实现代码可以知道Inherited = false,即不支持继承,部分代码如下:

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
    public sealed class ServiceContractAttribute : Attribute
    {
        
    }

因此在定义多层服务契约接口的时候,我们必须在每层接口上标记ServiceContract属性,以支持WCF服务契约属性。

示例代码如下:


   //契约属性不支持继承,如果需要继承契约属性,接口标志契约属性,定义一个交通工具基接口契约
    [ServiceContract(Namespace = "http://www.cnblogs.com/frank_xl/")]
    interface IVehicle
    {     }
    //接口继承关系不支持ServiceContract继承
    [ServiceContract(Namespace = "http://www.cnblogs.com/frank_xl/")]
    interface ITruck : IVehicle
    {
    }

货车服务类能够实现整个WCF契约层级接口,我们这里实现了Run跑和Carry运输货物的契约,代码如下:


    //货车服务类实现货车服务契约
    public class WCFServiceTruck : ITruck
    {
        //实现接口定义的方法
        public string Run()
        {
            Console.WriteLine("Hello! ,This an inherite demo");
            return "Hello! Truck is running  ";
        }
        //实现接口定义的方法
        public string Carry()
        {
            Console.WriteLine("Hello! ,This an inherite demo");
            return "Hello! Truck is carrying  ";
        }     }

宿主可以为契约层级最底层的接口公开一个单独的终结点,配置文件设置代码如下:


      <service behaviorConfiguration="WCFService.WCFServiceBehavior" name="WCFService.WCFServiceTruck">
        <endpoint
           address="http://localhost:9003/WCFServiceTruck"
           binding="wsHttpBinding"
           contract="WCFService.ITruck">
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:9003/"/>
          </baseAddresses>
        </host>
      </service>

【2.2】客户端契约层级:

客户端添加服务端数据引用,导入一个服务终结点的元数据时,反序列化生成的客户端契约将不再维持原来的层级关系。一个单独的契约,名称为终结点公布的契约名。含了层级中继承相关所有接口定义的操作契约。OperationContract特性中的Action与ResponseAction属性,可以保留原来定义每个操作的契约名。我们要想恢复服务端契约继承的层级关系,客户端可以手工修改代理以及导入契约的定义,恢复契约层级。手动恢复方式其实带给我们很多灵活性,但是也增加了工作量和复杂度。实际项目里一般接触不多,这里就不详细介绍,需要的话可以查阅相关的资料。

【2.2】服务契约分解概念:

下面我们继续讲解服务契约设计的一些概念知识。其实服务契约的设计在WCF分布式应用项目中属于比较重要的部分。服务契约的设计和实现相对来多比较复杂,除了注意已有的设计原则之外还要注意WCF契约相关的特性。面向服务分析与设计的属于一个较新的领域。实际的服务分析和设计我们还是借助于已有的经验和原则,来指我们更好地设计服务契约。这也是本节给出一个面向对象重要设计原则的原因。

因为WCF服务契约的定义借助现有的编程语言如C#,契约设计实际首先就是对服务接口的设计。我们应该如何设计服务接口?如何知道服务接口中应该定义哪些操作?每个接口又应该包含多少操作?等等都是我们必须考虑的问题。Service Contract Factoring就是要考虑服务接口的分解问题。在面向服务的应用程序中,可重用的基本单元就是服务接口。因此如何设计服务接口就是重中之重。

【4】服务契约分解原则:

这里我们设计服务接口时候即遵循单一职责和接口隔离等原则,又要考虑系统的开发成本。合理的接口是专业的、松耦合的、规则化和可重用的接口。这些优势同样有利于整个系统的松耦合和可重用等特性。总的来说,契约分解的目的就是使接口包含的更少操作。

如果我们定义了太多的细粒度服务接口,虽然它们易于实现,但集成它们的代价太高。如果我们仅定义了一个复杂的服务接口,虽然集成的成本会降低,但却接口的实现和可维护性较差。我们设计面向服务的系统时,需要平衡两个影响系统的因素,接口成本和集成成本。参见下图。

WCF分布式开发步步为赢(6):WCF服务契约继承与分解设计

系统服务的代价为实现的代价与集成的代价的综合。上图显示了最小代价与服务接口规模和数量之间的关系。设计良好的系统应该在系统集成成本和契约接口设计实现成本之间作何平衡点,达到系统整体开发成本的降低。

【5】服务契约分解代码分析:

这里我们来讲解一个简单的服务契约设计的例子。这里我们还继续使用交通车为例子进行讲解。

我们首先定义一个接口交通工具IVehicle,定义了如下:


    [ServiceContract(Namespace = "http://www.cnblogs.com/frank_xl/")]
    interface IVehicle
    {
        //操作契约,跑,开的契约
        [OperationContract]
        string Run();
        //操作契约,拉人、载人的契约
        [OperationContract]
        string Take();
        //操作契约,运输货物的契约
        [OperationContract]
        string Carry();
    }

这里的交通工具接口,分别定义了跑,拉人和载货三种操作。放在一个接口中。这就违反了接口设计中的主要的原则ISP接口隔离原则。我们不应该强迫服务继承他们不需要的操作。接口隔离原则ISP:使用多个专门的接口比使用单一的接口要好。从服务设计的角度来说:一个类对另外一个类的依赖性应当是建立在最小的接口上的。如果服务类只需要某一些方法的话,那么就应服务类可以继承相应的接口实现这些需要的方法,而不要实现不需要的方法。继承接口意味着作出承诺,服务类必须实现,也就是所谓的契约的概念。

因此,我们将服务契约分解为接口层级的方式,通过接口分解和继承,实现操作的分离。这里可以重新定义两个接口货车ITruck和小轿车ICar,分别定义自己的拉货Carry();和载人Take()的操作,当服务类需要实现拉货操作的时候就继承避免了接口设计的职责的混淆。代码如下:


//契约属性不支持继承,如果需要继承契约属性,接口标志契约属性,定义一个交通工具基接口契约
    [ServiceContract(Namespace = "http://www.cnblogs.com/frank_xl/")]
    interface IVehicle
    {
        //操作契约,跑,开的契约
        [OperationContract]
        string Run();
    }
    //接口继承关系不支持ServiceContract继承
    [ServiceContract(Namespace = "http://www.cnblogs.com/frank_xl/")]
    interface ITruck : IVehicle
    {
        //操作契约,运输货物的契约
        [OperationContract]
        string Carry();
    }
    //接口继承关系不支持ServiceContract继承
    [ServiceContract(Namespace = "http://www.cnblogs.com/frank_xl/")]
    interface ICar : IVehicle
    {
        //操作契约,拉人、载人的契约
        [OperationContract]
        string Take();
    }

【6】总结:

以上就是对WCF服务继承和分解设计相关知识的介绍,下面简要做下介绍:

<1>:本文开始讲解了OO面向对象的设计原则,作为经典的面相对象的设计经验,也是设计模式文章里的重要的知识点,对WCF服务设计有主要的参考价值,比如SRP单一职责、ISP接口隔离等原则;

<2>:我们应该避免设计过多或者过少的接口,应该考虑系统的服务接口定义实现的复杂度和系统服务集成的成本。综合起来权衡,取得一个平衡点。服务契约成员的最佳数量(根据经验总结,仅代表本人观点)应介于3到5之间。开发者在制订WCF编码规范时,应该指定一个上限值(例如20)。无论在何种情况,都不能超过该值。

另外避免定义类属性操作(Property-Like Operation)这样的定义十分类似C#的属性访问器,例如:

[OperationContract]
string GetCarNumber(); 

我们不应该干涉客户端属性访问,客户端在调用抽象操作时,不用关心具体的实现细节,只负责调用操作,而由服务去管理服务对象的状态。

<3>:WCF服务设计原则只是一些参考原则,对实际的开发工作起知道作用。包括前面叙述的面相对象的经典的设计原则和设计模式的宝贵经验。实际项目中需要结合自身的实际情况,实时调整和设计服务契约接口的设计,以期设计去更加合理和高效的服务契约。最后是本文的参考代码:Files/frank_xl/WCFServiceFactoringFrankXuLei.rar,下一节我们继续学习WCF数据契约的知识,请继续关注~。

参考资料:

1.《Programming WCF Services 》,By Juval Lowy;

2.面向对象设计模式与原则,http://hi.baidu.com/dapengchu/blog/item/0521e6d965525dee38012f5c.html