Подтвердить что ты не робот

Рефакторинг "процедурного" сервиса WCF

Я пытаюсь реорганизовать чудовищную службу WCF во что-то более управляемое. На момент написания этой статьи служба занимает около 9 зависимостей через конструктор, что делает модульное тестирование очень сложным.

Служба обрабатывает локальное состояние через конечный автомат, выполняет проверку параметров, выдает исключения, выполняет фактическую операцию и запускает события публикации через паб/дополнительный канал. Этот код очень похож на все остальные вызовы службы.

Я понимаю, что я могу сделать несколько из этих вещей (подтверждение аргумента, паб/вспомогательные уведомления) по-другому, возможно через Аспектно-ориентированное программирование или Поведение WCF, но моя внутренность говорит мне, что общий подход ошибочен - это слишком "процедурно".

Моя цель - отделить выполнение фактической операции от таких вещей, как pub/sub notifications и, возможно, даже с обработкой ошибок.

Интересно, если аббревиатуры вроде 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;
    }
}
4b9b3361

Ответ 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); 
    }
}

Теперь вам может быть интересно, как насчет тех сквозных проблем, которые вы реализовали, таких как проверка аргументов, возможность пожара, обновление статуса канала публикации и обработка ошибок. Ну да, все они связаны с перекрестными проблемами, и ваш сервисный класс 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 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>. Все сквозные проблемы будут размещены в декораторах, и, конечно же, все будет объединено вашей библиотекой 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 хорошо повсюду. Не только в области совместной работы.

Если вы хотите узнать больше об этом, прочитайте мою статью о применении обработчиков команд. После этого продолжайте читать мою статью об использовании этого принципа для служб WCF. Мой ответ - резюме этих статей.

Удачи.