Использование обобщений через интерфейс для замены реализаций в клиентском коде - PullRequest
0 голосов
/ 03 мая 2019

Я пытаюсь создать абстракции для очередей сообщений (как RabbitMQ), но столкнулся с проблемой.

Предположим, у меня есть следующее:

interface IMessagingQueue { }

interface IMessagingExchange
{
    Bind(IMessagingQueue queue);
}

class RabbitQueue : IMessagingQueue { }

class RabbitExchange : IMessagingExchange
{
    // FIX
    Bind(IMessagingQueue queue) { }
}

class InMemoryQueue : IMessagingQueue {  }

Это хорошо работает, потому что клиент не привязан к конкретной реализации. Пример:

class Client
{
    public Client(IMessagingExchange exchange) { }
}

Однако в игру вступает проблема, когда я отмечаю // FIX.

В этом примере InMemoryQueue может быть передано RabbitExchange (через Bind), что не имеет архитектурного смысла.

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

interface IMessagingExchange<TQueue> where TQueue : IMessagingQueue
{
    Bind(TQueue queue);
}

Однако теперь клиентский код привязан к реализации из-за введения универсального:

class RabbitExchange : IMessagingExchange<RabbitQueue> { }

class Worker
{
    public Worker(RabbitExchange exchange) { }
}

Это обеспечивает безопасность во время компиляции, но жертвует способностью менять реализации.

Я обдумываю это? Есть ли способ решить эту проблему?

1 Ответ

1 голос
/ 04 мая 2019

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

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

interface IMessagingExchange
{
    void Bind(IMessagingQueue queue);
}

interface IMessagingExchange<TQueue> : IMessagingExchange where TQueue : IMessagingQueue
{
    void Bind(TQueue queue);
}

Создание интерфейса для определения подмножества очередей, которые можно использовать с конкретным обменом. В этом случае мы определяем очереди, которые могут использоваться Rabbit Exchange:

interface IRabbitQueue : IMessagingQueue { }

Класс RabbitQueue теперь реализует этот интерфейс:

class RabbitQueue : IRabbitQueue { }

Мы добавили базовый класс для проверки типов:

abstract class ExchangeBase<TQueue> : IMessagingExchange<TQueue>  where TQueue : class, IMessagingQueue
{
    public abstract void Bind(TQueue queue);

    public void Bind(IMessagingQueue queue)
    {
        var typedQueue = queue as TQueue;
        if (typedQueue == null)
            throw new InvalidOperationException($"This exchange only supports queues that implement {typeof(TQueue).FullName}");
        Bind(typedQueue);
    }
}

Обратите внимание, что базовый класс реализует нетипизированную версию, выполняет проверку типов для вас и перенаправляет запрос на типизированную версию, которая предоставляется конкретным классом.

Класс RabbitExchange теперь наследуется от базового класса и обеспечивает логику привязки:

class RabbitExchange : ExchangeBase<IRabbitQueue>
{
    public override void Bind(IRabbitQueue queue)
    {

    }
}

Исполнение:

//Using untyped versions
IMessagingExchange exchange = new RabbitExchange();
IMessagingQueue queue = new RabbitQueue();

//This works fine
exchange.Bind(queue);

//Attempt to use the wrong queue
IMessagingQueue memoryQueue = new InMemoryQueue();
//This results in an error
exchange.Bind(memoryQueue);

Этот обмен поддерживает только те очереди, которые реализуют SomeNameSpace.IRabbitQueue

//We use a typed exchange this time
var rabbitExchange = exchange as RabbitExchange;

//This works
rabbitExchange.Bind(queue);
//This is still allowed because of the untyped interface, but causes an error because type is still checked
rabbitExchange.Bind(memoryQueue);

//Use a typed queue this time
var rabbitQueue = queue as RabbitQueue;
//This skips the base class validation because it calls the typed method in the concrete class
rabbitExchange.Bind(rabbitQueue);
...