9

我们在系统中遇到了一个god object。该系统包括public service向我们的客户公开,middle office service以及back 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 client现在发生的事情是我应该在和中添加对该服务的引用back office client。但back office只需要FinalizeEntity1DeclineEntity1操作。Interface segregation principle所以这是一个经典的in违反SOLID。因此,我必须将其进一步拆分为 3 个不同的服务,例如IEntity1FrontService, IEntity1MiddleService, IEntity1BackService.

4

4 回答 4

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

于 2015-06-29T16:22:04.320 回答
2

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

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

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

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

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

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

  • 发送电子邮件()
  • 借方账户()
  • 您的 Web 应用程序使用的服务中的 CreditAccount() 等。

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

  1. 划款()
  2. 获取平衡(),

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

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

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

于 2015-07-06T13:38:20.343 回答
1

您的问题与其说是上帝对象问题,不如说是服务组合问题。上帝对象存在问题的原因与基于 crud 的巨大服务接口存在问题的原因不同。

我当然同意你所描述的 3 份服务合同已经到了实际上无法管理的地步。与重构相关的痛苦将不成比例地高于如果这是进程内代码,因此采取正确的方法非常重要,因此您的问题。

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

我之前在 SO 上写过这个,所以为了它的价值,我将包括我的想法:

我发现如果服务运营可以存在于具有商业意义的水平上,那是最好的。

这意味着如果业务人员被告知操作名称,他们将大致了解调用该操作会做什么,并且可以猜测需要传递给它的数据。

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

例如,以下操作签名具有业务意义:

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

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

Guid BeginOnboardEmployee(string employeeName, DateTime employeeDateOfBirth);

如果您在考虑服务组合时使用此原则,那么好处是您很少会偏离最佳路径;您知道每个操作的作用,并且您知道何时不再需要某个操作。

另一个好处是,由于业务流程很少更改,因此您不需要太多更改服务合同。

于 2015-06-25T11:00:33.343 回答
1

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

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

让我大开眼界的是Mark IV 咖啡机的设计,摘自 Robert C. Martin 的“Java 程序员的 UML”。对于有意义的名称,我推荐他的书“清洁代码”。

因此,不要构建离散操作的接口,例如:

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

执行以下操作:

PrepareNewOrderForApproval(PartNumber p, string clientName);

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

于 2015-07-06T12:24:23.110 回答