重构"程序"WCF服务

Iga*_*nik 21 c# wcf refactoring dependency-injection cqrs

我正在尝试将一个可怕的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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*ven 44

你所拥有的是伪装命令的一个很好的例子.你在这里做的很好,你的服务方法已经在一个参数中DoSomethingData.这您的命令消息.

你在这里缺少的是对命令处理程序的一般抽象:

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
Run Code Online (Sandbox Code Playgroud)

通过一些重构,您的服务方法将如下所示:

// Vanilla dependency.
ICommandHandler<DoSomethingData> doSomethingHandler;

public void DoSomething(DoSomethingData data)
{
    this.doSomethingHandler.Handle(data);
}
Run Code Online (Sandbox Code Playgroud)

当然,你需要一个实现ICommandHandler<DoSomethingData>.在您的情况下,它将如下所示:

public class DoSomethingHandler : ICommandHandler<DoSomethingData>
{
    public void Handle(DoSomethingData command)
    {
        // does the actual something
        DoSomethingInternal(command); 
    }
}
Run Code Online (Sandbox Code Playgroud)

现在你可能想知道,你实现的那些跨领域问题如参数验证,可以触发,发布通道状态更新和错误处理.好吧,是的,它们都是跨领域的问题,你的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

由于这WcfValidationCommandHandlerDecorator<T>是一个泛型类型,我们可以将它包装在每个命令处理程序中.例如:

var handler = new WcfValidationCommandHandlerDecorator<DoSomethingData>(
    new DoSomethingHandler(),
    new DoSomethingValidator());
Run Code Online (Sandbox Code Playgroud)

您可以轻松创建一个处理任何抛出异常的装饰器:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

你有没有看到我如何在这个装饰器中包装你的代码?我们可以再次使用这个装饰器来包装原始的:

var handler = 
    new WcfValidationCommandHandlerDecorator<DoSomethingData>(
        new WcfExceptionHandlerCommandHandlerDecorator<DoSomethingData>(
            new DoSomethingHandler()),
    new DoSomethingValidator());
Run Code Online (Sandbox Code Playgroud)

当然这一切看起来都是非常多的代码,如果你只有一个WCF服务方法,那么这可能是一种过度杀伤力.但如果你有十几个人,那它就开始变得非常有趣了.如果你有数百个?好吧..如果你没有使用这样的技术,我不想成为维护代码库的开发人员.

因此,经过几分钟的重构后,您最终会得到依赖于ICommandHandler<TCommand>接口的WCF服务类.所有横切关注的问题都将放在装饰器中,当然,一切都由您的DI库连接在一起.我想你知道几个;-)

当你这样做时,可能有一件事你可以改进,因为你所有的WCF服务类开始看起来都很相似:

// Vanilla dependency.
ICommandHandler<FooData> handler;

public void Foo(FooData data)
{
    this.handler.Handle(data);
}
Run Code Online (Sandbox Code Playgroud)

它将开始无聊地编写新命令和新处理程序.您仍将维护您的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.
    }
}
Run Code Online (Sandbox Code Playgroud)

现在你所拥有的只是一个WCF服务,它只有一个永远不会改变的方法.该ServiceKnownTypeAttribute指向GetKnownTypes.WCF将在启动时调用此方法以查看它必须接受的类型.当您根据应用程序元数据返回列表时,它允许您向系统添加和删除命令,而无需更改WCF服务中的单行.

您可能偶尔会添加新的WCF特定装饰器,这些装饰器通常应放在WCF服务中.其他装饰器可能更通用,可能放在业务层本身.例如,它们可能会被您的MVC应用程序重用.

您的问题与CQRS有关,但我的回答与此无关.嗯......没有什么是夸大其词的.CQRS广泛使用这种模式,但CQRS更进一步.CQRS是关于协作域的,它会强制您对命令进行排队并异步处理它们.另一方面,我的答案就是应用SOLID设计原则.SOLID到处都是好的.不仅在协作域中.

如果您想了解更多相关信息,请阅读我关于应用命令处理程序的文章.之后,继续阅读我关于将此原则应用于WCF服务的文章.我的回答是这些文章的摘要.

祝好运.

  • 哇,非常感谢你,史蒂文,你的回答非常详细!我考虑过实现“ICommand/ICommandHandler”模式,我只是想确保这是处理此问题的“惯用”方法。我很高兴你向我保证我是对的:) (2认同)