21

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

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

我意识到我可以通过面向方面的编程或 WCF 行为以不同的方式做其中的几件事(参数验证、发布/订阅通知),但我的直觉告诉我,一般方法是错误的——这感觉太“程序化” .

我的目标是将实际操作的执行与发布/订阅通知之类的事情分开,甚至可能是错误处理。

我想知道像DDDCQRS或其他技术这样的首字母缩略词是否可以在这里提供帮助?不幸的是,我对定义之外的那些概念不是很熟悉。

这是一个此类 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;
    }
}
4

1 回答 1

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); 
    }
}

现在您可能想知道,您实现的那些横切关注点(例如参数验证、can 触发、发布通道状态更新和错误处理)呢?嗯,是的,它们都是横切关注点,您的 WCF 服务类和您的业务逻辑(the 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 无处不在。不仅在协作领域。

If you want to read more about this, please read my article about applying command handlers. After that, go on and read my article about applying this principle to WCF services. My answer is a summary of those articles.

Good luck.

于 2013-02-12T12:04:55.797 回答