重构;程序性的“;WCF服务

本文关键字:WCF 服务 程序性 重构 | 更新日期: 2023-09-27 17:58:57

我正在尝试将一个可怕的WCF服务重构为更易于管理的服务。在撰写本文时,该服务通过构造函数获得了大约9个依赖项,这使得单元测试变得非常困难。

该服务通过状态机处理本地状态,对参数进行验证,抛出故障异常,执行实际操作,并通过发布/子通道触发发布事件。此代码与所有其他服务调用非常相似。

我意识到我可以通过面向Aspect-Oriented编程或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;
    }
}

重构;程序性的“;WCF服务

这里有一个伪装命令的好例子。您在这里所做的很好的一点是,您的服务方法已经接受了一个单独的参数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); 
    }
}

现在,您可能想知道,您实现的那些交叉关注点(如参数验证、can fire、发布通道状态更新和错误处理)如何处理。是的,它们都是跨领域的关注点,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服务方法,那么这可能有些过头了。但如果你有十几个左右,它就会变得非常有趣。如果你有几百个?好如果你不使用这样的技术,我不想成为维护代码库的开发人员。

因此,经过几分钟的重构,您最终得到的WCF服务类仅依赖于ICommandHandler<TCommand>接口。所有的交叉关注点都将放在decorator中,当然,所有的东西都由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设计原则。固体在任何地方都是好的。不仅仅是在协作领域。

如果你想了解更多关于这方面的信息,请阅读我关于应用命令处理程序的文章。在那之后,继续阅读我关于将这一原则应用于WCF服务的文章。我的回答是对这些文章的总结。

祝你好运。