在 WCF 服务中重构上帝对象
Refactoring God objects in WCF services
我们在系统中遇到了 god object
。该系统由 public service
暴露给我们的客户,middle office service
和 back office service
组成。
流程如下:用户在 public service
注册一些交易,然后 middle office service
的经理检查交易并批准或拒绝交易,最后 back office service
的经理完成或拒绝交易。
我使用的是 transaction
这个词,但实际上这些是不同类型的操作,例如 CRUD on entity1
、CRUD on entiny2
... 不仅是 CRUD
操作,而且许多其他操作,如 approve/send/decline entity1
、make 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
只需要 FinalizeEntity1
和 DeclineEntity1
操作。所以这是 SOLID
中对 Interface segregation principle
的经典违反。所以我必须将其进一步拆分为 3 个不同的服务,例如 IEntity1FrontService
、IEntity1MiddleService
、IEntity1BackService
。
你的问题与其说是神对象问题,不如说是服务组合问题。上帝对象存在问题的原因与庞大的、基于 crud 的服务接口存在问题的原因不同。
我当然同意您描述的 3 份服务合同已经达到无法有效管理的地步。与重构相关的痛苦将比如果这是进程中的代码不成比例地高,所以你采取正确的方法非常重要,因此你的问题。
不幸的是,soa 中的服务可组合性是一个很大的话题,您不太可能在这里得到大量有用的答案;尽管显然有用,但其他人的经验不太可能适用于您的情况。
我已经在 SO before 上写过这篇文章,所以对于它的价值,我将包括我的想法:
I find that it's best if service operations can exist at a level where
they have business meaning.
What this means is that if a business person was told the operation
name, they would understand roughly what calling that operation would
do, and could make a guess at what data it would require to be passed
to it.
For this to happen your operations should fulfill in full or in part
some business process.
For example, the following operation signatures have business meaning:
void SolicitQuote(int brokerId, int userId, DateTime quoteRequiredBy);
int BindPolicyDocument(byte[] document, SomeType documentMetadata);
Guid BeginOnboardEmployee(string employeeName, DateTime employeeDateOfBirth);
If you use this principal when thinking about service composition then
the benefit is that you will rarely stray far from the optimal path;
you know what each operation does and you know when an operation is no
longer needed.
An additional benefit is that because business processes change fairly
rarely you will not need to change your service contracts as much.
这里的挑战是在不更改大部分代码的情况下重构代码以避免潜在的回归。
避免包含数千行的大型业务代码的一种解决方案是将您的 interfaces/implementations 分成多个部分,每个部分代表一个给定的业务领域。
比如你的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);
}
现在,对于服务实施,您可以使用部分 classes(一个部分 class 用于每个业务领域)将其拆分为多个部分:
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 次操作的事实,因此客户端代理的生成仍需要很长时间。至少你的代码会在服务器端组织得更好,而且重构会很便宜而且风险不大。
这里没有灵丹妙药,只是为了更好地进行代码重组 readability/maintenance。
200 个服务,每个服务 10 个操作与 20 个服务,每个服务 100 个操作是另一个话题,但可以肯定的是重构需要更多时间,你仍然有 2000 个操作。除非你重构你的整个应用程序并减少这个数字(例如通过提供更多 "high-level"(并非总是可能)的服务)。
我没有使用 WCF 的经验,但我认为上帝 类 和重载接口似乎是一个普遍的 OOD 问题。
在设计系统时,您应该寻找行为(或业务逻辑)而不是数据结构和操作。不要看你打算如何实现它,而要看客户将如何使用它以及他将如何命名它。根据我的经验,方法的正确名称通常会提供很多关于对象及其耦合的线索。
让我大开眼界的是 the design of the Mark IV coffee maker,摘录自 Robert C. Martin 的 "UML for Java Programmers"。对于有意义的名字,我推荐他的书 "Clean Code".
因此,与其构建离散操作的接口,不如:
GetClientByName(string name);
AddOrder(PartNumber p, ContactInformation i);
SendOrder(Order o);
做类似的事情:
PrepareNewOrderForApproval(PartNumber p, string clientName);
完成此操作后,您还可以重构为单独的对象。
在给定服务中拥有太多运营合同没有意义,因为它会导致维护问题。话虽如此,如果像 Add()、Delete、Update()、AddChildItem()、RemoveChildItem() 等操作应该放在一起,那么不用担心操作合约的数量会增加到 30-40 个。因为应该在一起的东西应该从一个接口出来(内聚)。
但是在给定的服务合同中进行 600 次操作确实是一个压倒性的数字。您可以开始识别操作:-
- 需要在一起
- 而且在给定的服务中不需要在一起。
基于此,您可以将操作拆分为不同的服务。
如果有些方法不直接被client使用,那么考虑暴露基于BUSSINESS逻辑的方法("Matthias Bäßler"也有建议)。
假设您想公开 MoneyTransfer 功能。那么你不需要公开
- 发送邮件()
- 借记账户()
- 您的 Web 应用程序使用的服务中的 CreditAccount() 等。
所以在这里您可以只向您的 Web 应用程序公开聚合服务。在这种情况下,它可能是 IAccountService,其方法类似于
- 转账()
- GetBalance(),
在您的实现内部,您可以创建其他提供相关操作的服务,例如:-
- 发送邮件()
- 借记账户()
- IAccountService 需要 CreditAccount() 等。 MoneyTransfer() 方法。
这样,给定服务中的方法数量将下降到可维护的水平。
我们在系统中遇到了 god object
。该系统由 public service
暴露给我们的客户,middle office service
和 back office service
组成。
流程如下:用户在 public service
注册一些交易,然后 middle office service
的经理检查交易并批准或拒绝交易,最后 back office service
的经理完成或拒绝交易。
我使用的是 transaction
这个词,但实际上这些是不同类型的操作,例如 CRUD on entity1
、CRUD on entiny2
... 不仅是 CRUD
操作,而且许多其他操作,如 approve/send/decline entity1
、make 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
只需要 FinalizeEntity1
和 DeclineEntity1
操作。所以这是 SOLID
中对 Interface segregation principle
的经典违反。所以我必须将其进一步拆分为 3 个不同的服务,例如 IEntity1FrontService
、IEntity1MiddleService
、IEntity1BackService
。
你的问题与其说是神对象问题,不如说是服务组合问题。上帝对象存在问题的原因与庞大的、基于 crud 的服务接口存在问题的原因不同。
我当然同意您描述的 3 份服务合同已经达到无法有效管理的地步。与重构相关的痛苦将比如果这是进程中的代码不成比例地高,所以你采取正确的方法非常重要,因此你的问题。
不幸的是,soa 中的服务可组合性是一个很大的话题,您不太可能在这里得到大量有用的答案;尽管显然有用,但其他人的经验不太可能适用于您的情况。
我已经在 SO before 上写过这篇文章,所以对于它的价值,我将包括我的想法:
I find that it's best if service operations can exist at a level where they have business meaning.
What this means is that if a business person was told the operation name, they would understand roughly what calling that operation would do, and could make a guess at what data it would require to be passed to it.
For this to happen your operations should fulfill in full or in part some business process.
For example, the following operation signatures have business meaning:
void SolicitQuote(int brokerId, int userId, DateTime quoteRequiredBy); int BindPolicyDocument(byte[] document, SomeType documentMetadata); Guid BeginOnboardEmployee(string employeeName, DateTime employeeDateOfBirth);
If you use this principal when thinking about service composition then the benefit is that you will rarely stray far from the optimal path; you know what each operation does and you know when an operation is no longer needed.
An additional benefit is that because business processes change fairly rarely you will not need to change your service contracts as much.
这里的挑战是在不更改大部分代码的情况下重构代码以避免潜在的回归。
避免包含数千行的大型业务代码的一种解决方案是将您的 interfaces/implementations 分成多个部分,每个部分代表一个给定的业务领域。
比如你的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);
}
现在,对于服务实施,您可以使用部分 classes(一个部分 class 用于每个业务领域)将其拆分为多个部分:
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 次操作的事实,因此客户端代理的生成仍需要很长时间。至少你的代码会在服务器端组织得更好,而且重构会很便宜而且风险不大。
这里没有灵丹妙药,只是为了更好地进行代码重组 readability/maintenance。
200 个服务,每个服务 10 个操作与 20 个服务,每个服务 100 个操作是另一个话题,但可以肯定的是重构需要更多时间,你仍然有 2000 个操作。除非你重构你的整个应用程序并减少这个数字(例如通过提供更多 "high-level"(并非总是可能)的服务)。
我没有使用 WCF 的经验,但我认为上帝 类 和重载接口似乎是一个普遍的 OOD 问题。
在设计系统时,您应该寻找行为(或业务逻辑)而不是数据结构和操作。不要看你打算如何实现它,而要看客户将如何使用它以及他将如何命名它。根据我的经验,方法的正确名称通常会提供很多关于对象及其耦合的线索。
让我大开眼界的是 the design of the Mark IV coffee maker,摘录自 Robert C. Martin 的 "UML for Java Programmers"。对于有意义的名字,我推荐他的书 "Clean Code".
因此,与其构建离散操作的接口,不如:
GetClientByName(string name);
AddOrder(PartNumber p, ContactInformation i);
SendOrder(Order o);
做类似的事情:
PrepareNewOrderForApproval(PartNumber p, string clientName);
完成此操作后,您还可以重构为单独的对象。
在给定服务中拥有太多运营合同没有意义,因为它会导致维护问题。话虽如此,如果像 Add()、Delete、Update()、AddChildItem()、RemoveChildItem() 等操作应该放在一起,那么不用担心操作合约的数量会增加到 30-40 个。因为应该在一起的东西应该从一个接口出来(内聚)。
但是在给定的服务合同中进行 600 次操作确实是一个压倒性的数字。您可以开始识别操作:-
- 需要在一起
- 而且在给定的服务中不需要在一起。
基于此,您可以将操作拆分为不同的服务。
如果有些方法不直接被client使用,那么考虑暴露基于BUSSINESS逻辑的方法("Matthias Bäßler"也有建议)。
假设您想公开 MoneyTransfer 功能。那么你不需要公开
- 发送邮件()
- 借记账户()
- 您的 Web 应用程序使用的服务中的 CreditAccount() 等。
所以在这里您可以只向您的 Web 应用程序公开聚合服务。在这种情况下,它可能是 IAccountService,其方法类似于
- 转账()
- GetBalance(),
在您的实现内部,您可以创建其他提供相关操作的服务,例如:-
- 发送邮件()
- 借记账户()
- IAccountService 需要 CreditAccount() 等。 MoneyTransfer() 方法。
这样,给定服务中的方法数量将下降到可维护的水平。