Refaktoryzacja" proceduralna " usługa WCF

Staram się przerobić monstrualną usługę WCF na coś łatwiejszego do opanowania. W momencie pisania, usługa zajmuje około 9 zależności za pośrednictwem konstruktora, co sprawia, że testowanie jednostkowe jest bardzo trudne.

Usługa obsługuje stan lokalny za pomocą maszyny stanowej, weryfikuje parametry, wyrzuca wyjątki od błędów, wykonuje rzeczywistą operację i wywołuje zdarzenia publikacji za pośrednictwem kanału pub/sub. Ten kod jest bardzo podobny do wszystkich innych wywołań usługi.

Zdaję sobie sprawę że mogę zrobić kilka z tych rzeczy (Walidacja argumentów, powiadomienia pub/sub) inaczej, być może poprzez programowanie zorientowane na aspekt lub zachowania WCF, ale moje przeczucie mówi mi, że ogólne podejście jest złe - to wydaje się zbyt "proceduralne".

Moim celem jest oddzielenie wykonania rzeczywistej operacji od rzeczy takich jak powiadomienia pub/sub, a może nawet obsługa błędów.

Zastanawiam się, czy akronimy jak DDD lub CQRS lub inne techniki mogą pomóc tutaj? Niestety nie znam zbyt dobrze tych pojęć wykraczających poza definicję.

Oto (uproszczony) przykład jednej z takich operacji 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;
    }
}
Author: Steven, 2013-02-12

1 answers

To, co tam masz, jest świetnym przykładem komendy w przebraniu. Miłe w tym, co tutaj robisz, jest to, że twoja metoda serwisowa przyjmuje już jeden argument DoSomethingData. To to twoja wiadomość polecenia.

To, czego tu brakuje, to ogólna abstrakcja nad instrukcjami obsługi:

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

Przy odrobinie refaktoryzacji, twoja metoda serwisowa wyglądałaby tak:

// Vanilla dependency.
ICommandHandler<DoSomethingData> doSomethingHandler;

public void DoSomething(DoSomethingData data)
{
    this.doSomethingHandler.Handle(data);
}

I oczywiście potrzebujesz implementacji dla ICommandHandler<DoSomethingData>. W Twoim przypadku będzie wygląda tak:

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

Teraz możesz się zastanawiać, co z tymi przekrojowymi problemami, które zaimplementowałeś, takimi jak Walidacja argumentów, can fire, aktualizacja statusu kanału publikacji i obsługa błędów. Cóż, to wszystko przekrojowe problemy, zarówno klasa usług WCF, jak i logika biznesowa (the DoSomethingHandler) nie powinny się tym martwić.

Istnieje kilka sposobów zastosowania programowania zorientowanego na aspekt. Niektórzy lubią używać narzędzi do tkania kodu, takich jak PostSharp. Minusy z tych narzędzi jest to, że znacznie utrudniają testowanie jednostkowe, ponieważ wplatasz wszystkie swoje przekrojowe obawy.

Drugi sposób polega na przechwytywaniu. Korzystanie z dynamicznego generowania proxy i refleksji. Jest jednak odmiana tego, że lubię bardziej, a to jest poprzez zastosowanie dekoratorów. Fajną rzeczą w tym jest to, że z mojego doświadczenia wynika, że jest to najczystszy sposób zastosowania obaw dotyczących cięcia poprzecznego.

Spójrzmy na dekoratora dla Twojego Walidacja:

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

Ponieważ ten WcfValidationCommandHandlerDecorator<T> jest typem generycznym, możemy owinąć go wokół każdego programu obsługi poleceń. Na przykład:

var handler = new WcfValidationCommandHandlerDecorator<DoSomethingData>(
    new DoSomethingHandler(),
    new DoSomethingValidator());

I równie łatwo możesz stworzyć dekorator, który obsługuje wszystkie wyrzucone wyjątki:

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;
        }
    }
}
Widziałeś, jak zawinąłem Twój kod do dekoratora? Możemy ponownie użyć tego dekoratora do owinięcia oryginału: {]}
var handler = 
    new WcfValidationCommandHandlerDecorator<DoSomethingData>(
        new WcfExceptionHandlerCommandHandlerDecorator<DoSomethingData>(
            new DoSomethingHandler()),
    new DoSomethingValidator());

Oczywiście to wszystko wydaje się strasznie dużo kodu i jeśli masz tylko jedną metodę obsługi WCF niż tak, to to pewnie przesada. Ale zaczyna być naprawdę interesujące, jeśli masz kilkanaście. Jeśli masz setki? Cóż.. Nie chcę być programistą utrzymującym tę bazę kodu, jeśli nie używasz takiej techniki.

Więc po kilku minutach refaktoryzacji kończysz z klasami usług WCF, które po prostu zależą od interfejsów ICommandHandler<TCommand>. Wszystkie przekrojowe problemy zostaną umieszczone w dekoratorach i oczywiście wszystko jest połączone przez Bibliotekę DI. Myślę, że znasz kilka ;-)

Kiedy to zrobisz, prawdopodobnie jest jedna rzecz, którą możesz poprawić, ponieważ wszystkie Twoje klasy usług WCF zaczną wyglądać nudnie tak samo: {]}

// Vanilla dependency.
ICommandHandler<FooData> handler;

public void Foo(FooData data)
{
    this.handler.Handle(data);
}

Pisanie nowych poleceń i nowych programów obsługi zacznie się nudzić. Nadal będziesz mieć usługę WCF do utrzymania.

Zamiast tego możesz utworzyć usługę WCF z pojedynczą klasą z jedną metodą, jak to:
[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.
    }
}

Teraz wszystko, co masz, to usługa WCF z jedną metodą, która będzie nigdy się nie zmieniaj. ServiceKnownTypeAttribute wskazuje na GetKnownTypes. WCF wywoła tę metodę podczas uruchamiania, aby zobaczyć, jakie typy musi zaakceptować. Po zwróceniu listy opartej na metadanych aplikacji, umożliwia ona dodawanie i usuwanie poleceń do systemu, bez konieczności zmiany pojedynczej linii w usłudze WCF.

Prawdopodobnie raz na jakiś czas dodasz nowe dekoratory WCF, które zazwyczaj powinny być umieszczone w usłudze WCF. Innych dekoratorów byłoby prawdopodobnie bardziej ogólne i może być umieszczony w samej warstwie biznesowej. Mogą one być ponownie wykorzystane na przykład przez Twoją aplikację MVC.

Twoje pytanie było trochę o CQRS, ale moja odpowiedź nie ma z tym nic wspólnego. Cóż... nic nie jest przesadą. CQRS wykorzystuje ten wzór szeroko, ale CQRS idzie o krok dalej. CQRS dotyczy domen współpracujących, które zmuszają cię do kolejkowania poleceń i przetwarzania ich asynchronicznie. Moja odpowiedź z drugiej strony jest tylko o zastosowaniu SOLID zasady projektowania. SOLID is good wszędzie. Nie tylko w domenach współpracy.

Jeśli chcesz przeczytać więcej na ten temat, przeczytaj mój artykuł o stosowaniu programów obsługi poleceń . Następnie przeczytaj mój artykuł o stosowaniu tej zasady do usług WCF. Moja odpowiedź to podsumowanie tych artykułów.

Powodzenia.
 44
Author: Steven,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-12 14:49:59