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

Виндзор - вытягивание Транзитные объекты из контейнера

Как я могу вытаскивать объекты из контейнера, которые являются временными по своей природе? Нужно ли регистрировать их в контейнере и вводить в конструктор требуемого класса? Внедрение всего в конструктор не очень хорошо. Также для одного класса я не хочу создавать TypedFactory и вставлять factory в необходимый класс.

Другая мысль, которая пришла ко мне, была "новой" на нужной основе. Но я также вставляю компонент Logger (через свойство) во все мои классы. Поэтому, если я их обновляю, мне придется вручную создать экземпляр Logger в этих классах. Как я могу продолжать использовать контейнер для ВСЕХ моих классов?

Ввод логгера:. Большинство моих классов имеют свойство Logger, за исключением случаев, когда существует цепочка наследования (в этом случае только этот базовый класс имеет это свойство, и все классы-получатели используют это). Когда они создаются через контейнер Windsor, они получат мою реализацию ILogger, введенную в них.

//Install QueueMonitor as Singleton
Container.Register(Component.For<QueueMonitor>().LifestyleSingleton());
//Install DataProcessor as Trnsient
Container.Register(Component.For<DataProcessor>().LifestyleTransient());

Container.Register(Component.For<Data>().LifestyleScoped());

public class QueueMonitor
{
    private dataProcessor;

    public ILogger Logger { get; set; }

    public void OnDataReceived(Data data)
    {
        //pull the dataProcessor from factory    
        dataProcessor.ProcessData(data);
    }
}

public class DataProcessor
{
    public ILogger Logger { get; set; }

    public Record[] ProcessData(Data data)
    {
        //Data can have multiple Records
        //Loop through the data and create new set of Records
        //Is this the correct way to create new records?
        //How do I use container here and avoid "new" 
        Record record = new Record(/*using the data */);
        ...

        //return a list of Records    
    }
}


public class Record
{
    public ILogger Logger { get; set; }

    private _recordNumber;
    private _recordOwner;

    public string GetDescription()
    {
        Logger.LogDebug("log something");
        // return the custom description
    }
}

Вопросы:

  • Как создать новый объект Record без использования "нового"?

  • QueueMonitor Singleton, тогда как Data - "Область". Как я могу вставить Data в метод OnDataReceived()?

4b9b3361

Ответ 1

Из приведенных вами примеров трудно быть очень конкретным, но в целом, когда вы ILogger экземпляры ILogger в большинство служб, вам следует задать себе две вещи:

  1. Я слишком много вхожу?
  2. Я нарушаю твердые принципы?

1. Я слишком много вхожу

Вы слишком много регистрируетесь, когда у вас много такого кода:

try
{
   // some operations here.
}
catch (Exception ex)
{
    this.logger.Log(ex);
    throw;
}

Написание такого кода происходит из-за потери информации об ошибках. Дублирование таких видов блоков try-catch повсеместно не помогает. Хуже того, я часто вижу, как разработчики регистрируются и продолжают (они удаляют последний оператор throw). Это действительно плохо (и пахнет как старое поведение VB ON ERROR RESUME NEXT), потому что в большинстве ситуаций у вас просто недостаточно информации, чтобы определить, безопасно ли продолжать. Часто в коде имеется ошибка или сбой во внешнем ресурсе, например в базе данных, что приводит к сбою операции. Продолжение означает, что пользователь часто получает представление о том, что операция прошла успешно, а это не так. Спросите себя: что хуже, показывая пользователю общее сообщение об ошибке, в котором говорится, что что-то пошло не так, и попросите его повторить попытку, или молча пропускаете ошибку и позволяете пользователю думать, что его запрос был успешно обработан? Подумайте, что будет чувствовать пользователь, если через две недели узнает, что его заказ так и не был отправлен. Вероятно, вы потеряете клиента. Или, что еще хуже, регистрация MRSA у пациента молчаливо завершается неудачей, в результате чего пациент не подвергается карантину во время кормления, что приводит к загрязнению других пациентов, что приводит к высокой стоимости или даже смерти

Большинство этих видов строк try-catch-log должны быть удалены, и вы должны просто позволить исключению пузыриться в стеке вызовов.

Вы не должны войти? Вы обязательно должны! Но если вы можете, определите один блок try-catch в верхней части приложения. В ASP.NET вы можете реализовать событие Application_Error, зарегистрировать HttpModule или определить пользовательскую страницу ошибок, которая ведет журнал. С WinForms решение отличается, но концепция остается неизменной: определите одну единственную вершину, которая наиболее универсальна.

Иногда, однако, вы все равно хотите перехватить и записать исключение определенного типа. Система, над которой я работал в прошлом, позволяла бизнес-уровню генерировать исключения ValidationExceptions, которые будут обнаруживаться на уровне представления. Эти исключения содержали информацию для проверки для отображения пользователю. Так как эти исключения будут пойманы и обработаны на уровне представления, они не будут пузыриться до самой верхней части приложения и не попадут в универсальный код приложения. Тем не менее я хотел зарегистрировать эту информацию, просто чтобы узнать, как часто пользователь вводил неверную информацию, и чтобы выяснить, были ли проверки инициированы по правильной причине. Так что это не было регистрацией ошибок; просто вход Я написал следующий код для этого:

try
{
   // some operations here.
}
catch (ValidationException ex)
{
    this.logger.Log(ex);
    throw;
}

Выглядит знакомо? Да, выглядит точно так же, как и в предыдущем фрагменте кода, с той разницей, что я перехватывал только исключения ValidationException. Однако было еще одно отличие, которое нельзя увидеть, просто взглянув на этот фрагмент. В приложении было только одно место, содержащее этот код! Это был декоратор, который подводит меня к следующему вопросу, который вы должны задать себе:

2. Нарушаю ли я твердые принципы?

Такие вещи, как ведение журнала, аудит и безопасность, называются сквозными проблемами (или аспектами). Они называются сквозными, потому что они могут разрезать многие части вашего приложения и часто должны применяться ко многим классам в системе. Однако, когда вы обнаружите, что пишете код для их использования во многих классах системы, вы, скорее всего, нарушаете принципы SOLID. Возьмем, к примеру, следующий пример:

public void MoveCustomer(int customerId, Address newAddress)
{
    var watch = Stopwatch.StartNew();

    // Real operation

    this.logger.Log("MoveCustomer executed in " +
        watch.ElapsedMiliseconds + " ms.");
}

Здесь вы измеряете время, необходимое для выполнения операции MoveCustomer и регистрируете эту информацию. Весьма вероятно, что другие операции в системе нуждаются в такой же сквозной заботе. Вы начинаете добавлять код, подобный этому, для своего ShipOrder, CancelOrder, CancelShipping и других вариантов использования, и это приводит к значительному дублированию кода и в конечном итоге к CancelShipping на обслуживание (я был там).

Проблема с этим кодом в том, что он нарушает принципы SOLID. Принципы SOLID - это набор принципов объектно-ориентированного проектирования, которые помогут вам определить гибкое и поддерживаемое (объектно-ориентированное) программное обеспечение. MoveCustomer нарушил как минимум два из этих правил:

  1. Принцип единой ответственности - классы должны нести одну ответственность. Однако класс, содержащий метод MoveCustomer, не только содержит основную бизнес-логику, но и измеряет время, необходимое для выполнения операции. Другими словами, он имеет несколько обязанностей.
  2. Принцип Open-Closed (OCP) - он предписывает дизайн приложения, который не позволяет вам вносить быстрые изменения по всей базе кода; или, в словаре OCP, класс должен быть открыт для расширения, но закрыт для модификации. В случае, если вам нужно добавить обработку исключений (третью ответственность) в MoveCustomer использования MoveCustomer, вам (снова) придется изменить метод MoveCustomer. Но вам нужно изменить не только метод MoveCustomer, но и многие другие методы, чтобы сделать это радикальным изменением. OCP тесно связан с принципом DRY.

Решение этой проблемы состоит в том, чтобы извлечь запись в свой собственный класс и позволить этому классу обернуть исходный класс:

// The real thing
public class MoveCustomerService : IMoveCustomerService
{
    public virtual void MoveCustomer(int customerId, Address newAddress)
    {
        // Real operation
    }
}

// The decorator
public class MeasuringMoveCustomerDecorator : IMoveCustomerService
{
    private readonly IMoveCustomerService decorated;
    private readonly ILogger logger;

    public MeasuringMoveCustomerDecorator(
        IMoveCustomerService decorated, ILogger logger)
    {
        this.decorated = decorated;
        this.logger = logger;
    }

    public void MoveCustomer(int customerId, Address newAddress)
    {
        var watch = Stopwatch.StartNew();

        this.decorated.MoveCustomer(customerId, newAddress);

        this.logger.Log("MoveCustomer executed in " +
            watch.ElapsedMiliseconds + " ms.");
    }
}

Обернув декоратор вокруг реального экземпляра, вы теперь можете добавить это поведение измерения к классу без изменения какой-либо другой части системы:

IMoveCustomerService command =
    new MeasuringMoveCustomerDecorator(
        new MoveCustomerService(),
        new DatabaseLogger());

Тем не менее, предыдущий пример только решил часть проблемы (только часть SRP). При написании кода, как показано выше, вам нужно будет определить отдельные декораторы для всех операций в системе, и в итоге вы получите декораторы, такие как MeasuringShipOrderDecorator, MeasuringCancelOrderDecorator и MeasuringCancelShippingDecorator. Это снова приводит к большому количеству повторяющегося кода (нарушение принципа OCP) и все еще требует написания кода для каждой операции в системе. Здесь не хватает общей абстракции над вариантами использования в системе.

Отсутствует интерфейс ICommandHandler<TCommand>.

Давайте определим этот интерфейс:

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

И давайте сохраним аргументы метода MoveCustomer в своем собственном (Parameter Object) классе с именем MoveCustomerCommand:

public class MoveCustomerCommand
{
    public int CustomerId { get; set; }
    public Address NewAddress { get; set; }
}

И давайте поместим поведение метода MoveCustomer в класс, который реализует ICommandHandler<MoveCustomerCommand>:

public class MoveCustomerCommandHandler : ICommandHandler<MoveCustomerCommand>
{
    public void Execute(MoveCustomerCommand command)
    {
        int customerId = command.CustomerId;
        Address newAddress = command.NewAddress;
        // Real operation
    }
}

Поначалу это может показаться странным, но поскольку теперь у вас есть общая абстракция для вариантов использования, вы можете переписать свой декоратор следующим образом:

public class MeasuringCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    private ILogger logger;
    private ICommandHandler<TCommand> decorated;

    public MeasuringCommandHandlerDecorator(
        ILogger logger,
        ICommandHandler<TCommand> decorated)
    {
        this.decorated = decorated;
        this.logger = logger;
    }

    public void Execute(TCommand command)
    {
        var watch = Stopwatch.StartNew();

        this.decorated.Execute(command);

        this.logger.Log(typeof(TCommand).Name + " executed in " +
            watch.ElapsedMiliseconds + " ms.");
    }
}

Этот новый MeasuringCommandHandlerDecorator<T> очень похож на MeasuringMoveCustomerDecorator, но этот класс можно использовать повторно для всех обработчиков команд в системе:

ICommandHandler<MoveCustomerCommand> handler1 =
    new MeasuringCommandHandlerDecorator<MoveCustomerCommand>(
        new MoveCustomerCommandHandler(),
        new DatabaseLogger());

ICommandHandler<ShipOrderCommand> handler2 =
    new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
        new ShipOrderCommandHandler(),
        new DatabaseLogger());

Таким образом, будет гораздо проще добавить сквозные проблемы в вашу систему. Довольно просто создать удобный метод в вашем корне композиции, который может обернуть любой созданный обработчик команд соответствующими обработчиками команд в системе. Например:

private static ICommandHandler<T> Decorate<T>(ICommandHandler<T> decoratee)
{
    return
        new MeasuringCommandHandlerDecorator<T>(
            new DatabaseLogger(),
            new ValidationCommandHandlerDecorator<T>(
                new ValidationProvider(),
                new AuthorizationCommandHandlerDecorator<T>(
                    new AuthorizationChecker(
                        new AspNetUserProvider()),
                    new TransactionCommandHandlerDecorator<T>(
                        decoratee))));
}

Этот метод может быть использован следующим образом:

ICommandHandler<MoveCustomerCommand> handler1 = 
    Decorate(new MoveCustomerCommandHandler());

ICommandHandler<ShipOrderCommand> handler2 =
    Decorate(new ShipOrderCommandHandler());

Однако если ваше приложение начинает расти, может оказаться полезным загрузить его с помощью DI-контейнера, поскольку DI-контейнер может использовать автоматическую регистрацию. Это избавляет вас от необходимости вносить изменения в корень композиции для каждой новой пары команда/обработчик, которую вы добавляете в систему. Особенно, когда ваши декораторы имеют ограничения общего типа, DI-контейнер будет чрезвычайно полезен.

В настоящее время большинство современных DI-контейнеров для .NET имеют достаточно приличную поддержку для декораторов, и особенно Autofac (пример) и Simple Injector (пример) упрощают регистрацию декораторов открытого типа. Simple Injector даже позволяет декораторам применяться условно на основе заданного предиката или сложных ограничений общего типа, позволяя декорированному классу быть введенным как фабрика и позволяя контекстному контексту вставляться в декораторы, и все это может быть действительно полезным со временем ко времени.

Unity и Castle, с другой стороны, имеют динамические средства перехвата (как, кстати, делает Autofac). Динамический перехват имеет много общего с оформлением, но он использует динамическую генерацию прокси под крышками. Это может быть более гибким, чем работа с универсальными декораторами, но вы платите цену, когда дело доходит до удобства сопровождения, потому что вы часто теряете безопасность типов и перехватчики всегда вынуждают вас зависеть от библиотеки перехвата, в то время как декораторы безопасны от типов и могут быть написано без зависимости от внешней библиотеки.

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

ОБНОВЛЕНИЕ: Я также стал соавтором книги " Принципы, практики и шаблоны внедрения зависимостей", в которой более подробно рассматриваются этот стиль программирования SOLID и конструкция, описанная выше (см. Главу 10).