Как использовать внедрение зависимостей для создания новых объектов во время выполнения - PullRequest
3 голосов
/ 27 марта 2020

Я работаю над преобразованием библиотеки для использования внедрения зависимостей. Я натолкнулся на этот (упрощенный) пример кода, который необходимо реорганизовать:

public class MessageQueueManager {
    public static readonly MessageQueueManager Instance =
        new MessageQueueManager();
    private readonly MessageQueue _highPriority;
    private readonly MessageQueue _lowPriority;

    private MessageQueueManager() {
        _highPriority = new MessageQueue(1);
        _lowPriority = new MessageQueue(2);
    }
}

public class MessageQueue {
    private int _priority;

    public MessageQueue(int priority) => _priority = priority;

    public void QueueMessage(Message msg) {
        _queue.Add(msg);
        MessageQueueManager.Instance.NotifyMessageQueued(this);
    }
}

public class Message {
    public Message(string text, Action onDismiss) { ... }

    private void Dismiss() {
       _onDismiss();
       MessageQueueManager.Instance.MessageDismissed(this);
    }
}

//to display a message:
MyQueue.QueueMessage(new Message(...));

Моя первая попытка - это сияющий драгоценный камень:

public class MessageQueueManager : IMessageQueueManager {
    private readonly IMessageQueue _highPriority;
    private readonly IMessageQueue _lowPriority;

    private MessageQueueManager(
        Func<IMessageQueueManager, int,
        /* several other dependencies*/ 
        IMessageQueue> queueConstructor,
        /*several other dependencies*/)
    {
        _highPriority = queueConstructor(this, 1/*, several other deps*/);
        _lowPriority = queueConstructor(this, 2/*, several other deps*/);
    }
}

public class MessageQueue : IMessageQueue {
    private readonly IMessageQueueManager _owner;
    private readonly int _priority;

    public MessageQueue(
        IMessageQueueManager owner, int priority,
        /*several other dependencies*/)
    {
        _owner = owner;
        _priority = priority;
        /*deal with several other dependencies*/
    }

    public void QueueMessage(IMessage msg)
    {
        _msg.Manager = _owner;
        _queue.Add(msg);
        _owner.NotifyMessageQueued(this);
    }
}

public class Message : IMessage {
    public IMessageQueueManager Manager {get; set;}
    public Message(string text, Action onDismiss)
    {
        //...
    }

    private void Dismiss() {
       _onDismiss();
       Manager.MessageDismissed(this);
    }
}

MyQueue.QueueMessage(new Message(...));

Итак ... я надеваю Мне это не нравится.

Обратите также внимание, что в других местах созданы свои собственные MessageQueue, которые не принадлежат непосредственно менеджеру, но все же взаимодействуют с той же системой. Два из MessageQueueManager - это просто значения по умолчанию, которые будут использовать большинство мест.

Есть ли более чистый способ справиться с этим? Имеет ли смысл использовать конструктор Message в MessageQueueManager? Каждое место, которое создает новое Сообщение, должно быть затем введено этим менеджером? (Это большая библиотека с сообщениями повсюду, и я пытаюсь делать это по несколько штук за раз, так что это будет большая задача, хотя это должно быть сделано в конце концов ...)

I Я на ранних стадиях. Я планирую в конечном итоге использовать некоторую библиотеку DI, хотя я не определился с какой, и я не знаю, как это действительно поможет в этом случае для создания новых сообщений специально. Если бы это был какой-то всеобъемлющий объект, который я мог бы передать для создания для меня сообщений и менеджеров выборки, это было бы полезно, но, очевидно, это не «правильное внедрение зависимостей», а скорее «поиск сервисов», что, по-видимому, плохо, Я думаю?

1 Ответ

4 голосов
/ 31 марта 2020

Вам нужно сделать несколько твиков:

1. Вам нужно IMessage?

Если у вас есть несколько реализаций сообщений очереди, то, в противном случае, если сообщение содержит только данные, возможно, этот интерфейс может быть опущен.

2. Отделите MessageQueueManager от Message

Если Message использует MessageQueueManager, то с внедрением зависимости вы должны начать передавать MessageQueueManager сообщениям при каждом создании. Предполагая, что у вас может быть много мест, где сообщения создаются без ведома администратора очередей, передача MessageQueueManager в качестве параметра может стать серьезным изменением. И вы можете избежать этого.

Давайте предположим, что когда сообщение может быть отклонено, оно должно быть уже в одной или нескольких очередях. В этом случае мы могли бы делегировать действие dismiss очередям , в которые было добавлено сообщение, и имеет смысл, чтобы очереди сообщений знали своего manager .

Итак, вам нужно что-то вроде этого:

public class Message : IMessage
{
    public Message(string text, Action onDismiss) { }

    // This is set by MessageQueue when the message is being added to the queue
    internal IMessageQueue Queue { get; set; }

    public void Dismiss()
    {
        //_onDismiss();

        if (Queue == null)
        {
            throw new InvalidOperationException("Message not queued.");
        }

        Queue.DismissMessage(this);
    }
}

3. MessageQueue создание

MessageQueue создание должно быть централизовано. Это делается для того, чтобы после создания экземпляра очереди вы могли установить MessageQueueManager, чтобы очередь могла обрабатывать NotifyMessageQueued и MessageDismissed.

Как то, что вы уже пробовали (внедрение queueConstructor), вам нужно, как говорили другие,

использовать фабрику.

Эта фабрика здесь должна быть вашей собственной фабрикой согласно принципу обращения зависимостей . Вы должны воздерживаться от использования любой фабрики DI, специфицированной c factory, как ILifetimeScope в Autofa c. Это потому, что внедрение зависимостей является инструментом и должно быть необязательным. Вам нужна инверсия зависимостей больше, чем внедрение зависимостей. Так что я бы рекомендовал вам не ie вашими типами один DI framework.

public interface IMessageQueueFactory
{ 
    // Those are shotcuts to CreatePriority(N)
    IMessageQueue CreateLowPriority();
    IMessageQueue CreateHighPriority();
    IMessageQueue CreatePriority(int priority);
}

// Forgive the verbosal name here. It indicates the factory creates instance of MessageQueue.
public class MessageQueueMessageQueueFactory : IMessageQueueFactory
{
    private Lazy<IMessageQueueManager> _manager;

    // The reason for using Lazy<T> here is becuase we have circular dependency in this design:
    // MessageQueueManager -> MessageQueueMessageQueueFactory -> MessageQueueManager
    // No all DI framework can handle this, so we have to defer resolving manager instance 
    // Also because queue manager is a singleton instance in the container, so deferred resolving
    // doesn't cause much pain such as scoping issues.
    public MessageQueueMessageQueueFactory(Lazy<IMessageQueueManager> manager)
    {
        _manager = manager;
    }

    public IMessageQueue CreateHighPriority()
    {
        return CreatePriority(1);
    }

    public IMessageQueue CreateLowPriority()
    {
        return CreatePriority(2);
    }

    public IMessageQueue CreatePriority(int priority)
    {
        return new MessageQueue(priority, _manager);
    }
}

Это нормально, если вам нужно внедрить спецификацию framework c factory в MessageQueueMessageQueueFactory потому что здесь предполагается, что это адаптер, преобразующий ваше собственное создание типа в разрешение типа DI.

И MessageQueue теперь выглядит так:

public class MessageQueue : IMessageQueue
{
    private int _priority;
    private Lazy<IMessageQueueManager> _manager;

    // Use internal constructor to encourage using factory to instantiate message queue.
    internal MessageQueue(int priority, Lazy<IMessageQueueManager> manager)
    {
        _priority = priority;
        _manager = manager;
    }

    public void QueueMessage(IMessage msg)
    {
        _queue.Add(msg);
        ((Message)msg).Queue = this;
        _manager.Value.NotifyMessageQueued(this);
    }

    public void DismissMessage(IMessage msg)
    {
        // So we have a type cast here. Depends on your message queue implemenation,
        // this is ok or not.
        // For example, if this is a RabbitMQ message queue, of course the message 
        // instance must also be a RabbitMQ message, so cast is fine.
        // However, if the message queue is a base class for other message queues, this 
        // should be avoided.
        // And this also goes back to the point: Do you really need an IMessage interface?
        if (((Message)msg).Queue != this)
        {
            throw new InvalidOperationException("Message is not queued by current instance.");
        }

        _manager.Value.MessageDismissed(this);
    }
}

4. Создание очереди сообщений в MessageQueueManager

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

public class MessageQueueManager : IMessageQueueManager
{
    private readonly IMessageQueue _highPriority;
    private readonly IMessageQueue _lowPriority;

    public MessageQueueManager(IMessageQueueFactory queueFactory)
    {
        _highPriority = queueFactory.CreateHighPriority();
        _lowPriority = queueFactory.CreateLowPriority();
    }
}

Обратите внимание, что поскольку мы хотим использовать Внедрение зависимости, и MessageQueueManager теперь использует IMessageQueueFactory, поэтому нам нужно изменить его с реального одноэлементного на одноэлементный DI, удалив свойство Instance и зарегистрировав тип как singleton

5. Вам нужно обработать семейство очередей?

Скажем, если у вас есть несколько реализаций очереди, вы, вероятно, не хотите смешивать типы очередей c. Например, вы не хотите добавлять сообщение RabbitMQ в очередь MSMQ. В этом случае вы можете использовать Абстрактный заводской шаблон :

public interface IMessageQueueProvider
{ 
    IMessageQueueManager CreateManager();
    IMessageQueueFactory CreateFactory();
    IMessage CreateMessage();
}

Но это немного выходит за рамки.

Наконец

Зарегистрируйте новые типы. Я использую Microsoft Extensions Dependency Injection в качестве примера:

var services = new ServiceCollection();
services.AddSingleton<IMessageQueueManager, MessageQueueManager>();
services.AddSingleton<IMessageQueueFactory, MessageQueueMessageQueueFactory>();

// Most DI containers now should support Lazy<T>, in case if it is not, you could add
// explicit registration.
services.AddTransient(
  provider => new Lazy<IMessageQueueManager>(
    () => provider.GetRequiredService<IMessageQueueManager>()));

// This provides a way to resolve low priority queue as IMessageQueue when injected into 
// other types.
services.AddTransient(
  provider => provider.GetRequiredService<IMessageQueueFactory>().CreateLowPriority());

Вот несколько примеров:

class Examples
{
    public Examples(
        // This is the queue manager
        IMessageQueueManager manager,
        // This is the queue factory for creating queues
        IMessageQueueFactory factory,
        // This is default queue implemenation, in this case a low priority queue
        IMessageQueue queue)
    {
        // This is queue with arbitrary priority.
        var differentPriorityQueue = factory.CreatePriority(3);

        // Queue a message and dismiss.
        var msg = new Message("Text", () => { });
        queue.QueueMessage(msg);
        msg.Dismiss();
    }
}
...