我正在尝试将一个可怕的WCF服务重构为更易于管理的东西。 在撰写本文时,该服务通过构造函数大约需要9个依赖项,这使得单元测试非常困难。
服务通过状态机处理本地状态,对参数进行验证,引发故障异常,执行实际操作并通过发布/子通道触发发布事件。所有其他服务电话的代码非常相似。
我意识到我可以通过Aspect-Oriented Programming或WCF行为以不同的方式完成其中的几个(参数验证,发布/订阅通知),但我的直觉告诉我一般方法是错误的 - 这感觉太“程序化”。
我的目标是将实际操作的执行与发布/子通知之类的内容分开,甚至可能将错误处理分开。
我想知道像DDD或CQRS这样的首字母缩略词或其他技巧是否可以帮到这里?遗憾的是,我不熟悉定义之外的那些概念。
这是一个这样的WCF操作的简化示例:
public void DoSomething(DoSomethingData data)
{
if (!_stateMachine.CanFire(MyEvents.StartProcessing))
{
throw new FaultException(...);
}
if (!ValidateArgument(data))
{
throw new FaultException(...);
}
var transitionResult =
_stateMachine.Fire(MyEvents.StartProcessing);
if (!transitionResult.Accepted)
{
throw new FaultException(...);
}
try
{
// does the actual something
DoSomethingInternal(data);
_publicationChannel.StatusUpdate(new Info
{
Status = transitionResult.NewState
});
}
catch (FaultException<MyError> faultException)
{
if (faultException.Detail.ErrorType ==
MyErrorTypes.EngineIsOffline)
{
TryFireEvent(MyServiceEvent.Error,
faultException.Detail);
}
throw;
}
}
答案 0 :(得分:44)
你所拥有的是伪装命令的一个很好的例子。你在这里做的很好,你的服务方法已经只需要一个参数DoSomethingData
。这个是你的命令信息。
这里缺少的是对命令处理程序的一般抽象:
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
通过一些重构,您的服务方法将如下所示:
// Vanilla dependency.
ICommandHandler<DoSomethingData> doSomethingHandler;
public void DoSomething(DoSomethingData data)
{
this.doSomethingHandler.Handle(data);
}
当然,您需要ICommandHandler<DoSomethingData>
的实施。在您的情况下,它将如下所示:
public class DoSomethingHandler : ICommandHandler<DoSomethingData>
{
public void Handle(DoSomethingData command)
{
// does the actual something
DoSomethingInternal(command);
}
}
现在您可能想知道,您实施的那些跨领域问题如参数验证,可以触发,发布通道状态更新和错误处理等。好吧,是的,它们都是跨领域的问题,你的WCF服务类和你的业务逻辑(DoSomethingHandler
)都不应该关注它。
有几种方法可以应用面向方面编程。有些人喜欢使用像PostSharp这样的代码编织工具。这些工具的缺点是它们使单元测试变得更加困难,因为您编织了所有横切关注点。
第二种方式是使用拦截。使用动态代理生成和一些反思。然而,我更喜欢这种变化,那就是应用装饰器。关于这一点的好处是,根据我的经验,这是应用交叉问题的最简洁方法。
让我们来看看你的验证装饰器:
public class WcfValidationCommandHandlerDecorator<T> : ICommandHandler<T>
{
private IValidator<T> validator;
private ICommandHandler<T> wrapped;
public ValidationCommandHandlerDecorator(IValidator<T> validator,
ICommandHandler<T> wrapped)
{
this.validator = validator;
this.wrapped = wrapped;
}
public void Handle(T command)
{
if (!this.validator.ValidateArgument(command))
{
throw new FaultException(...);
}
// Command is valid. Let's call the real handler.
this.wrapped.Handle(command);
}
}
由于这个WcfValidationCommandHandlerDecorator<T>
是泛型类型,我们可以将它包装在每个命令处理程序中。例如:
var handler = new WcfValidationCommandHandlerDecorator<DoSomethingData>(
new DoSomethingHandler(),
new DoSomethingValidator());
您可以轻松创建一个处理任何抛出异常的装饰器:
public class WcfExceptionHandlerCommandHandlerDecorator<T> : ICommandHandler<T>
{
private ICommandHandler<T> wrapped;
public ValidationCommandHandlerDecorator(ICommandHandler<T> wrapped)
{
this.wrapped = wrapped;
}
public void Handle(T command)
{
try
{
// does the actual something
this.wrapped.Handle(command);
_publicationChannel.StatusUpdate(new Info
{
Status = transitionResult.NewState
});
}
catch (FaultException<MyError> faultException)
{
if (faultException.Detail.ErrorType == MyErrorTypes.EngineIsOffline)
{
TryFireEvent(MyServiceEvent.Error, faultException.Detail);
}
throw;
}
}
}
你有没有看到我如何在这个装饰器中包装你的代码?我们可以再次使用这个装饰器来包装原始的:
var handler =
new WcfValidationCommandHandlerDecorator<DoSomethingData>(
new WcfExceptionHandlerCommandHandlerDecorator<DoSomethingData>(
new DoSomethingHandler()),
new DoSomethingValidator());
当然这一切似乎都是非常多的代码,如果你只有一个WCF服务方法,那么这可能是一种过度杀伤力。但是,如果你有十几个人,它会变得非常有趣。如果你有数百个?好吧..如果你没有使用这样的技术,我不想成为维护代码库的开发人员。
因此,经过几分钟的重构后,您最终会得到依赖于ICommandHandler<TCommand>
接口的WCF服务类。所有横切关注的问题都将放在装饰器中,当然,一切都由您的DI库连接在一起。我想你知道一些; - )
当你这样做时,可能有一件事你可以改进,因为你所有的WCF服务类开始看起来都很相似:
// Vanilla dependency.
ICommandHandler<FooData> handler;
public void Foo(FooData data)
{
this.handler.Handle(data);
}
开始编写新命令和新处理程序将变得无聊。您仍然需要维护您的WCF服务。
您可以做的是使用单个方法创建一个具有单个类的WCF服务,如下所示:
[ServiceKnownType("GetKnownTypes")]
public class CommandService
{
[OperationContract]
public void Execute(object command)
{
Type commandHandlerType = typeof(ICommandHandler<>)
.MakeGenericType(command.GetType());
dynamic commandHandler = Bootstrapper.GetInstance(commandHandlerType);
commandHandler.Handle((dynamic)command);
}
public static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
{
// create and return a list of all command types
// dynamically using reflection that this service
// must accept.
}
}
现在你所拥有的只是一个WCF服务,只有一个永远不会改变的方法。 ServiceKnownTypeAttribute
指向GetKnownTypes
。 WCF将在启动时调用此方法以查看它必须接受的类型。当您根据应用程序元数据返回列表时,它允许您向系统添加和删除命令,而无需更改WCF服务中的单行。
您可能偶尔会添加新的WCF特定装饰器,这些装饰器通常应放在WCF服务中。其他装饰器可能更通用,可能放在业务层本身。例如,它们可能会被您的MVC应用程序重用。
您的问题与CQRS有关,但我的回答与此无关。嗯......没有什么是夸大其词的。 CQRS广泛使用这种模式,但CQRS更进一步。 CQRS是关于协作域的,它会强制您对命令进行排队并异步处理它们。另一方面,我的答案就是应用SOLID设计原则。 SOLID到处都是好的。不仅在协作域中。
如果您想了解更多相关信息,请阅读我关于应用command handlers的文章。之后,继续阅读my article about applying this principle to WCF services。我的回答是这些文章的摘要。
祝你好运。