21

巨大なWCFサービスをより管理しやすいものにリファクタリングしようとしています。これを書いている時点では、サービスはコンストラクターを介して約9つの依存関係を取ります。そのため、単体テストは非常に困難です。

このサービスは、ステートマシンを介してローカル状態を処理し、パラメーターの検証を行い、障害例外をスローし、実際の操作を実行し、pub/subチャネルを介してパブリケーションイベントを発生させます。このコードは、他のすべてのサービス呼び出しと非常によく似ています。

おそらくアスペクト指向プログラミングやWCFの動作を介して、これらのいくつかのこと(引数の検証、pub / sub通知)を異なる方法で実行できることを認識していますが、私の直感では、一般的なアプローチが間違っていると言っています-これはあまりにも「手続き型」だと感じます。

私の目標は、実際の操作の実行をpub / sub通知、場合によってはエラー処理などから分離することです。

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

あなたが持っているのは、変装したコマンドの素晴らしい例です。ここで行っていることの良い点は、サービス メソッドが既に 1 つの引数を受け取っていることです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 サービス クラスとビジネス ロジック ( DoSomethingHandler) の両方は、それについて心配する必要はありません。

アスペクト指向プログラミングを適用するには、いくつかの方法があります。PostSharp などのコード ウィービング ツールを使用するのが好きな人もいます。これらのツールの欠点は、分野横断的な懸念事項をすべて織り込むため、単体テストが非常に難しくなることです。

2 番目の方法は、インターセプトを使用することです。動的プロキシ生成といくつかのリフレクションを使用します。しかし、私がもっと気に入っているバリエーションがあり、それはデコレータを適用することです。これの良いところは、これが私の経験では横断的関心事を適用する最もクリーンな方法だということです。

検証用のデコレータを見てみましょう。

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

もちろん、これは非常に多くのコードのように思えます。1 つの WCF サービス メソッドしかない場合は、おそらくやり過ぎです。しかし、ダースほどあると、本当に面白くなり始めます。あなたが何百も持っているなら?ええと..このような手法を使用していない場合、私はそのコードベースを維持する開発者になりたくありません。

したがって、数分間のリファクタリングの後、インターフェイスに依存するだけの WCF サービス クラスが完成しICommandHandler<TCommand>ます。すべての分野横断的な問題はデコレータに配置され、もちろんすべてが DI ライブラリによって接続されます。私はあなたがいくつか知っていると思います;-)

これを行うと、おそらく改善できる点が 1 つあります。すべての 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.
    }
}

これで、変更されることのない 1 つのメソッドを持つ WCF サービスだけになりました。へのServiceKnownTypeAttributeポイントGetKnownTypes。WCF は、起動時にこのメソッドを呼び出して、受け入れる必要がある型を確認します。アプリケーション メタデータに基づいてリストを返すと、WCF サービスで 1 行も変更することなく、システムにコマンドを追加および削除できます。

おそらく、新しい WCF 固有のデコレータを時々追加することになるでしょう。通常、それらは WCF サービスに配置する必要があります。他のデコレータはおそらくより一般的であり、ビジネス層自体に配置される可能性があります。たとえば、MVC アプリケーションで再利用される可能性があります。

あなたの質問はCQRSに関するものでしたが、私の答えはそれとは何の関係もありません. うーん...何も言い過ぎではありません。CQRS はこのパターンを広範に使用しますが、CQRS はさらに一歩進んでいます。CQRS は、コマンドをキューに入れ、非同期に処理することを強制する共同ドメインに関するものです。一方、私の答えは、SOLID設計原則を適用することです。SOLIDはどこでもいい。共同領域だけではありません。

これについて詳しく知りたい場合は、コマンド ハンドラーの適用に関する私の記事をお読みください。その後、この原則の WCF サービスへの適用に関する私の記事を読んでください。私の答えは、それらの記事の要約です。

幸運を。

于 2013-02-12T12:04:55.797 に答える