Виндзор - вытащить временные объекты из контейнера - PullRequest
57 голосов
/ 27 марта 2012

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

Еще одна мысль, которая пришла ко мне, была "новая" их по мере необходимости. Но я также внедряю компонент 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
    }
}

Вопросы:

  1. Как мне создать новый Record объект без использования «new»?

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

1 Ответ

246 голосов
/ 29 марта 2012

Из примеров, которые вы даете, трудно быть очень конкретным, но в целом, когда вы вводите 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 и других случаев использования, и это приводит к значительному дублированию кода и в конечном итоге к кошмару на обслуживание (я был там).

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

  1. Принцип единой ответственности - классы должны нести одну ответственность.Однако класс, содержащий метод MoveCustomer, не только содержит основную бизнес-логику, но и измеряет время, необходимое для выполнения операции.Другими словами, он имеет несколько обязанностей .
  2. Принцип Open-Closed (OCP) - он предписывает дизайн приложения, который предотвращает необходимость внесения радикальных измененийпо всей базе кода;или, в словаре OCP, класс должен быть открыт для расширения, но закрыт для модификации.Если вам нужно добавить обработку исключений (третью ответственность) в сценарий использования 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());

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

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).).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...