【问题标题】:Refactoring God objects in WCF services在 WCF 服务中重构上帝对象
【发布时间】:2015-09-11 21:53:37
【问题描述】:

我们在系统中遇到了god object。该系统由暴露给我们客户的public servicemiddle office serviceback office service 组成。

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

我使用的是 transaction 这个词,但实际上这些是不同类型的操作,例如 CRUD on entity1CRUD on entiny2...不仅是 CRUD 操作,还有许多其他操作,例如 approve/send/decline entity1、@987654333 @等等等等……

现在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 操作。所以这是SOLID 中对Interface segregation principle 的经典违反。因此,我必须将其进一步拆分为 3 个不同的服务,例如 IEntity1FrontServiceIEntity1MiddleServiceIEntity1BackService

【问题讨论】:

  • 重新生成客户端代理需要多长时间?
  • @ken2k,整个过程可能会持续 15 分钟。对于前 1-2-3 次尝试,它只是超时然后更新。超时需要 3-4 分钟。有时它会超时 2 次,有时只有一次。有时3次。但这不是主要的。优先级是重构。即使更新参考需要 1 秒钟,我们仍会决定重构。
  • 我可以提供一个仍然只有 3 个服务的解决方案,但是使用接口继承和部分实现将 3 个接口/实现分成多个接口/实现。不过,这并不能解决客户端代理的重新生成时间。
  • @ken2k,它已经被分成3个不同的ServiceContracts和3个不同的implementations
  • 我发布了这个想法作为答案,希望它更清楚

标签: wcf soa servicecontract


【解决方案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);

如果您在考虑服务组合时使用此原则,那么 好处是您很少会偏离最佳路径; 你知道每个操作做什么,你知道什么时候操作不 更需要。

另一个好处是,因为业务流程发生了相当大的变化 您很少不需要更改服务合同。

【讨论】:

  • 感谢您花时间回答。我会澄清一点:所有三个项目都是silverlight 项目。当然,所有暴露的操作都对业务有意义。我想听听其他人在问题中描述的情况下的一些真实生活经验和方法。无论如何感谢您的回答。
  • @GiorgiNakeuri - 明白这不是你要找的,但是我不同意你关于所有操作都具有商业意义的断言 - 从商业角度来看,AddEntity1 是什么意思?
  • 这当然是出于演示目的。我已经重命名了那些。实际上有一些有意义的名称:AddOrder、ApprooveOrder、SendOrder、LoadOrders、GetOrderByOrderNumber、DeclineAgreement、GetAgreementResource、AddAgreement 等......
【解决方案2】:

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

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

例如,您的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 个操作 .除非你重构整个应用程序并减少这个数字(例如通过提供更“高级”的服务(并非总是可能的))。

【讨论】:

  • 感谢您的回答!你能详细说明一下吗:200 services with 10 operations for each vs 20 services with 100 operations for each is another topic?其实我没有时间限制。我们正在为这些类型的重构准备迭代。
  • 这非常接近我们认为的重构方式。是的,我们将做一些接口继承,但我们将为不同的域创建单独的类,而不是仅仅将类拆分为部分的部分类。但我仍然想听听关于多达 50 个服务参考的经验 ÷)
【解决方案3】:

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

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

让我大开眼界的是the design of the Mark IV coffee maker,摘自 Robert C. Martin 的“UML for Java Programmers”。对于有意义的名字,我推荐他的书“清洁代码”。

因此,与其构建离散操作的接口,例如:

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

执行以下操作:

PrepareNewOrderForApproval(PartNumber p, string clientName);

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

【讨论】:

    【解决方案4】:

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

    但给定服务合同中的 600 次操作确实是压倒性的数字。您可以开始识别操作:-

    1. 必须在一起
    2. 在给定的服务中不需要一起。

    基于此,您可以将操作拆分为不同的服务。

    如果客户端不直接使用某些方法,则考虑根据 BUSSINESS 逻辑公开该方法(也如“Matthias Bäßler”所建议的那样)。

    假设您想公开 MoneyTransfer 功能。那么你就不需要暴露了

    • 发送电子邮件()
    • DebitAccount()
    • CreditAccount() 等在您的 Web 应用程序使用的服务中。

    因此,您可以在此处仅向您的 Web 应用程序公开一个聚合服务。在这种情况下,它可能是带有类似方法的 IAccountService

    1. TransferMoney()
    2. GetBalance(),

    在您的实现内部,您可以创建其他提供相关操作的服务,例如:-

    • 发送电子邮件()
    • DebitAccount()
    • IAccountService 需要 CreditAccount() 等。 MoneyTransfer() 方法。

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

    【讨论】:

      猜你喜欢
      • 2015-11-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-25
      相关资源
      最近更新 更多