在WCF服务中重构God对象

时间:2015-06-25 10:32:28

标签: wcf soa servicecontract

我们在系统中遇到god object。该系统由public service向我们的客户提供,middle office serviceback office service

流程如下:用户在public service中注册了一些交易,然后来自middle office service的经理检查交易并批准或拒绝交易,最后来自back office service的经理最终确定或拒绝事务。

我正在使用transaction这个词,但实际上这些是不同类型的操作,例如CRUD on entity1CRUD on entiny2 ...不仅CRUD操作,而且许多其他操作,如approve/send/decline entity1make entity1 parent/child of entity2等等......

现在WCF服务合同只是根据系统的那些部分分开。所以我们有3份服务合同:

PublicService.cs
MiddleOfficeService.cs
BackOfficeService.cs
每个人都有大量的经营合同:

public interface IBackOfficeService
{
    [OperationContract]
    void AddEntity1(Entity1 item);

    [OperationContract]
    void DeleteEntity1(Entity1 item);

    ....

    [OperationContract]
    void SendEntity2(Entity2 item);

    ....
}

在所有3项服务中,这些运营合同的数量已经达到2000个,每个服务合同约为600个。它不仅打破了最佳实践,而且只需更新服务引用就会非常痛苦,因为它需要很长时间。而且系统每天都在增长,每次迭代都会在这些服务中添加越来越多的操作。

现在我们面临两难困境,因为我们如何将这些神服务分解为逻辑部分。有人说服务不应包含超过12~20次操作。其他人说一些不同的事情。我意识到没有黄金法则,但我希望听到一些关于此的建议。

例如,如果我只是按实体类型拆分这些服务,那么我可以在项目中获得大约50个服务端点和50个服务引用。在这种情况下,可维护性是什么?

还有一件事需要考虑。假设我选择了按实体拆分这些服务的方法。例如:

public interface IEntity1Service
{
    [OperationContract]
    void AddEntity1(Entity1 item);

    [OperationContract]
    void ApproveEntity1(Entity1 item);

    [OperationContract]
    void SendEntity1(Entity1 item);

    [OperationContract]
    void DeleteEntity1(Entity1 item);
    ....

    [OperationContract]
    void FinalizeEntity1(Entity1 item);

    [OperationContract]
    void DeclineEntity1(Entity1 item);
}

现在发生的事情是我应该在public clientback office client中添加对此服务的引用。但back office仅需FinalizeEntity1DeclineEntity1次操作。因此,这是Interface segregation principleSOLID的经典违规行为。所以我必须进一步分为3个不同的服务,如IEntity1FrontServiceIEntity1MiddleServiceIEntity1BackService

4 个答案:

答案 0 :(得分:5)

这里的挑战是重构代码而不更改代码的大部分以避免潜在的回归。

避免使用数千行的大型业务代码的一种解决方案是将接口/实现拆分为多个部分,每个部分代表一个给定的业务域。

例如,您的IPublicService界面可以编写如下(使用界面继承,每个业务域一个界面):

IPublicService.cs

[ServiceContract]
public interface IPublicService : IPublicServiceDomain1, IPublicServiceDomain2
{
}

IPublicServiceDomain1.cs

[ServiceContract]
public interface IPublicServiceDomain1
{
    [OperationContract]
    string GetEntity1(int value);
}

IPublicServiceDomain2.cs

[ServiceContract]
public interface IPublicServiceDomain2
{
    [OperationContract]
    string GetEntity2(int value);
}

现在,对于服务实现,您可以使用部分类(每个业务域的一个部分类)将其拆分为多个部分:

Service.cs

public partial class Service : IPublicService
{
}

Service.Domain1.cs

public partial class Service : IPublicServiceDomain1
{
    public string GetEntity1(int value)
    {
        // Some implementation
    }
}

Service.Domain2.cs

public partial class Service : IPublicServiceDomain2
{
    public string GetEntity2(int value)
    {
        // Some implementation
    }
}

对于服务器配置,仍然只有一个端点:

<system.serviceModel>
  <services>
    <service name="WcfServiceLibrary2.Service">
      <endpoint address="" binding="basicHttpBinding" contract="WcfServiceLibrary2.IPublicService">
        <identity>
          <dns value="localhost" />
        </identity>
      </endpoint>
      <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      <host>
        <baseAddresses>
          <add baseAddress="http://localhost:8733/Design_Time_Addresses/WcfServiceLibrary2/Service1/" />
        </baseAddresses>
      </host>
    </service>
  </services>
  <behaviors>
    <serviceBehaviors>
      <behavior>
        <serviceMetadata httpGetEnabled="True" httpsGetEnabled="True" />
        <serviceDebug includeExceptionDetailInFaults="False" />
      </behavior>
    </serviceBehaviors>
  </behaviors>
</system.serviceModel>

客户端相同:仍然是一个服务引用:

<system.serviceModel>
  <bindings>
    <basicHttpBinding>
      <binding name="BasicHttpBinding_IPublicService" />
    </basicHttpBinding>
  </bindings>
  <client>
    <endpoint address="http://localhost:8733/Design_Time_Addresses/WcfServiceLibrary2/Service1/"
      binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IPublicService"
      contract="ServiceReference1.IPublicService" name="BasicHttpBinding_IPublicService" />
  </client>
</system.serviceModel>

这允许通过将大型服务拆分为多个逻辑部分(每个部分与给定的业务域相关联)来重构服务器端。

这并不会改变您的3个服务中的每个服务仍然有600个操作的事实,因此客户端代理生成仍然需要很长时间。至少你的代码在服务器端组织得更好,而且重构会很便宜而且风险也不大。

这里没有银弹,这只是代码重组,以提高可读性/维护性。

200个服务,每个服务10个操作,20个服务,每个服务100个操作,这是另一个主题,但可以肯定的是,重构需要更多时间,,你仍然可以进行2000次操作 。除非您重构整个应用程序并减少此数量(例如,通过提供更多&#34;高级别&#34;(并非总是可能)的服务)。

答案 1 :(得分:2)

拥有太多的操作合同在给定的服务中没有意义,因为它会导致维护问题。如果说如果Add(),Delete,Update(),AddChildItem(),RemoveChildItem()等操作应该在一起,那么不要担心操作合同的数量达到30-40。因为应该在一起的东西应该来自一个界面(凝聚力)。

但是,给定服务合同中的600个操作实际上是压倒性的。您可以开始识别操作: -

  1. 这是必须在一起
  2. 并且不需要在给定的服务中一起使用。
  3. 基于此,您可以将操作拆分为不同的服务。

    如果客户端没有直接使用某些方法,那么请考虑根据BUSSINESS逻辑公开方法(“MatthiasBäßler”也建议)。

    假设您要公开MoneyTransfer功能。那么你不需要公开

    • SendEmail()
    • DebitAccount()
    • 您的Web应用程序使用的服务中的CreditAccount()等。

    因此,您可以在此处向Web应用程序公开聚合服务。在这种情况下,它可能是IAccountService,其方法就像

    1. TransferMoney()
    2. GetBalance(),
    3. 在您的实施内部,您可以创建其他服务,提供相关操作,如: -

      • SendEmail()
      • DebitAccount()
      • IAccountService所需的CreditAccount()等。 MoneyTransfer()方法。

      这样,给定服务中的方法数量将降低到可维护的水平。

答案 2 :(得分:1)

你的问题不是上帝对象问题,因为它是一个服务组合问题。由于不同的原因,上帝对象存在问题,而不是基于crud的巨大服务接口存在问题。

我当然同意你所描述的3份服务合同已达到有效无法管理的程度。与重构相关的痛苦将不成比例地高于过程中的代码,因此您采取正确的方法非常重要,因此您的问题。

不幸的是,soa中的服务可组合性是一个非常大的主题,你不太可能在这里获得大量有用的答案;虽然显然有用,但其他人的经验不太可能适用于您的情况。

我已经在SO before上写过这篇文章,所以对于它的价值,我会包括我的想法:

  

如果服务操作可以存在于某个级别,我发现它是最好的   他们有商业意义。

     

这意味着如果一个商人被告知操作   名字,他们会大致了解调用该操作的内容   做,并可以猜测它需要传递什么数据   它。

     

为此,您的操作应全部或部分完成   一些业务流程。

     

例如,以下操作签名具有商业含义:

void SolicitQuote(int brokerId, int userId, DateTime quoteRequiredBy);

int BindPolicyDocument(byte[] document, SomeType documentMetadata);

Guid BeginOnboardEmployee(string employeeName, DateTime employeeDateOfBirth);
     

如果您在考虑服务组合时使用此主体,那么   好处是你很少偏离最佳路径;   你知道每个操作的作用,你知道什么时候操作没有   需要更久。

     

另一个好处是因为业务流程公平变化   很少你不需要改变你的服务合同。

答案 3 :(得分:1)

我没有WCF的经验,但我认为上帝类和重载接口似乎是一般的OOD问题。

在设计系统时,您应该寻找行为(或业务逻辑)而不是数据结构和操作。不要看看你将如何实现它,但客户将如何使用它以及如何命名它。根据我的经验,拥有正确的方法名称通常会提供关于对象的大量线索。

对我而言,令人大开眼界的是the design of the Mark IV coffee maker,摘自&#34; UML for Java Programmers&#34;罗伯特C.马丁。对于有意义的名字,我推荐他的书&#34;清洁代码&#34;。

因此,不是建立离散操作的界面,而是:

GetClientByName(string name);
AddOrder(PartNumber p, ContactInformation i);
SendOrder(Order o);

做类似的事情:

PrepareNewOrderForApproval(PartNumber p, string clientName);

完成此操作后,您还可以重构为单独的对象。