Как зарегистрировать несколько реализаций одного интерфейса Asp.Net ядром?

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

public interface IService
{
}

public class ServiceA : IService
{
}

public class ServiceB : IService
{    
}

public class ServiceC : IService
{    
}

обычно другие контейнеры IOC, такие как Unity позволяют регистрировать конкретные реализации некоторых Key что отличает их.

In Asp.Net Core как зарегистрировать эти службы и разрешить их во время выполнения на основе некоторого ключа?

Я не вижу Add способ служба key или name параметр, который обычно используется для различения бетона реализация.

    public void ConfigureServices(IServiceCollection services)
    {            
         // How do I register services here of the same interface            
    }


    public MyController:Controller
    {
       public void DoSomeThing(string key)
       { 
          // How do get service based on key
       }
    }

шаблон завод единственный вариант здесь?

обновление 1
Я прошел через статью здесь это показывает, как использовать шаблон фабрики для получения экземпляров службы, когда у нас есть несколько конкретных реализаций. Однако это еще не полное решение. когда я позову _serviceProvider.GetService() метод я не могу вводить данные в конструктор. Например, рассмотрим этот пример

public class ServiceA : IService
{
     private string _efConnectionString;
     ServiceA(string efconnectionString)
     {
       _efConnecttionString = efConnectionString;
     } 
}

public class ServiceB : IService
{    
   private string _mongoConnectionString;
   public ServiceB(string mongoConnectionString)
   {
      _mongoConnectionString = mongoConnectionString;
   }
}

public class ServiceC : IService
{    
    private string _someOtherConnectionString
    public ServiceC(string someOtherConnectionString)
    {
      _someOtherConnectionString = someOtherConnectionString;
    }
}

как _serviceProvider.GetService() ввести соответствующую строку подключения? В Unity или любом другом МОК мы можем сделать это во время регистрации типа. Я могу использовать конкурентные возвращается однако для этого мне потребуется ввести все настройки, я не могу ввести определенную connectionstring в службу.

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

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

поэтому я думаю, что по умолчанию DI в ASP.NET ядро отсутствует 2 вещи
1 > Регистрация экземпляров с помощью ключа
2 > Inject статические данные в конструктор во время регистрации

14 ответов


Я сделал простой обходной путь, используя Func когда я оказался в этой ситуации.

services.AddTransient<ServiceA>();
services.AddTransient<ServiceB>();
services.AddTransient<ServiceC>();

services.AddTransient<Func<string, IService>>(serviceProvider => key =>
{
    switch(key)
    {
        case "A":
            return serviceProvider.GetService<ServiceA>();
        case "B":
            return serviceProvider.GetService<ServiceB>();
        case "C":
            return serviceProvider.GetService<ServiceC>();
        default:
            throw new KeyNotFoundException(); // or maybe return null, up to you
    }
});

и использовать его из любого класса, зарегистрированного в DI, как:

public class Consumer
{
    private readonly Func<string, IService> _serviceAccessor;

    public Consumer(Func<string, IService> serviceAccessor)
    {
        _serviceAccessor = serviceAccesor;
    }

    public void UseServiceA()
    {
        //use _serviceAccessor field to resolve desired type
        _serviceAccessor("A").DoIServiceOperation();
    }
}

обновление

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

но вы можете использовать любой пользовательский тип разрешения в качестве ключа, так как вы обычно не хотите огромный N-case переключите гниющий код. Зависит от масштаба приложения.


другой вариант-использовать метод расширения GetServices С Microsoft.Extensions.DependencyInjection.

зарегистрируйте свои услуги как:

services.AddSingleton<IService, ServiceA>();
services.AddSingleton<IService, ServiceB>();
services.AddSingleton<IService, ServiceC>();

затем разрешите с небольшим количеством Linq:

var services = serviceProvider.GetServices<IService>();
var serviceB = services.First(o => o.GetType() == typeof(ServiceB));

или

var serviceZ = services.First(o => o.Name.Equals("Z"));

(предполагая, что IService имеет строковое свойство с именем "Name")

обязательно using Microsoft.Extensions.DependencyInjection;

обновление

AspNet 2.1 источник:GetServices


он не поддерживается Microsoft.Extensions.DependencyInjection.

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

это совсем не трудно:

  1. добавьте зависимость к StructureMap в свой project.json:

    "Structuremap.Microsoft.DependencyInjection" : "1.0.1",
    
  2. впрысните его в ASP.NET трубопровод внутри ConfigureServices и зарегистрировать свои классы (см. docs)

    public IServiceProvider ConfigureServices(IServiceCollection services) // returns IServiceProvider !
    {
        // Add framework services.
        services.AddMvc();
        services.AddWhatever();
    
        //using StructureMap;
        var container = new Container();
        container.Configure(config =>
        {
            // Register stuff in container, using the StructureMap APIs...
            config.For<IPet>().Add(new Cat("CatA")).Named("A");
            config.For<IPet>().Add(new Cat("CatB")).Named("B");
            config.For<IPet>().Use("A"); // Optionally set a default
            config.Populate(services);
        });
    
        return container.GetInstance<IServiceProvider>();
    }
    
  3. затем, чтобы получить именованный экземпляр, вам придется просить IContainer

    public class HomeController : Controller
    {
        public HomeController(IContainer injectedContainer)
        {
            var myPet = injectedContainer.GetInstance<IPet>("B");
            string name = myPet.Name; // Returns "CatB"
    

вот и все.

для примера, чтобы построить, нужно

    public interface IPet
    {
        string Name { get; set; }
    }

    public class Cat : IPet
    {
        public Cat(string name)
        {
            Name = name;
        }

        public string Name {get; set; }
    }

я столкнулся с той же проблемой и хочу поделиться, как я ее решил и почему.

как вы упомянули, есть две проблемы. первый:

In Asp.Net Core как зарегистрировать эти службы и разрешить их в время выполнения на основе некоторого ключа?

Итак, какие у нас есть варианты? Люди предлагают два:

  • используйте пользовательскую фабрику (например,_myFactory.GetServiceByKey(key))

  • используйте другой двигатель DI (как _unityContainer.Resolve<IService>(key))

шаблон завод единственный вариант здесь?

на самом деле оба варианта являются фабриками, потому что каждый контейнер IoC также является фабрикой (очень настраиваемой и сложной). И мне кажется, что другие варианты также являются вариациями Заводского шаблона.

так какой вариант лучше? Здесь я согласен с @Sock, который предложил использовать custom factory, и именно поэтому.

Во-Первых, Я всегда старайтесь избегать добавления новых зависимостей, когда они действительно не нужны. В этом я с вами согласен. Более того, использование двух фреймворков DI хуже, чем создание пользовательской абстракции фабрики. Во втором случае вам нужно добавить новую зависимость пакета (например, Unity), но в зависимости от нового заводского интерфейса здесь меньше зла. Основная идея ASP.NET Core DI, я считаю, это простота. Он поддерживает минимальный набор функций, следующих принцип KISS. Если вам нужно немного больше функция затем DIY или использовать соответствующий Plungin который реализует желаемую функцию (открытый закрытый принцип).

во-вторых, часто нам нужно вводить много именованных зависимостей для одной службы. В случае Unity вам может потребоваться указать имена для параметров конструктора (используя InjectionConstructor). Эта регистрация использует отражение и какая-то умная логика угадать аргументы для конструктора. Это также может привести к ошибкам выполнения, если регистрация не соответствует аргументы конструктора. С другой стороны, при использовании собственной фабрики у вас есть полный контроль над тем, как предоставить параметры конструктора. Он более удобочитаем и разрешен во время компиляции. принцип KISS снова.

второй вопрос:

как _serviceProvider.GetService () вводит соответствующее соединение струна?

во-первых, я согласен с вами, что в зависимости от новой вещи, как IOptions (и поэтому на упаковке Microsoft.Extensions.Options.ConfigurationExtensions) не очень хорошая идея. Я видел, как некоторые обсуждали IOptions где были разные мнения о его преимуществе. Опять же, я стараюсь избегать добавления новых зависимостей, когда они действительно не нужны. Это действительно необходимо? Думаю, нет. В противном случае каждая реализация должна была бы зависеть от нее без какой-либо явной необходимости, исходящей из этой реализации (для меня это выглядит как нарушение ISP, где я тоже согласен с вами). Это также верно в зависимости от фабрики, но в в этом случае can избежать.

ASP.NET Core DI обеспечивает очень приятную перегрузку для этой цели:

var mongoConnection = //...
var efConnection = //...
var otherConnection = //...
services.AddTransient<IMyFactory>(
             s => new MyFactoryImpl(
                 mongoConnection, efConnection, otherConnection, 
                 s.GetService<ISomeDependency1>(), s.GetService<ISomeDependency2>())));

вы правы, встроенный ASP.NET основной контейнер не имеет концепции регистрации нескольких служб, а затем извлечения определенного, как вы предполагаете, фабрика является единственным реальным решением в этом случае.

кроме того, вы можете переключиться на сторонний контейнер, такой как Unity или StructureMap, который предоставляет необходимое вам решение (описано здесь: https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing-the-default-services-container).


по-видимому, вы можете просто ввести IEnumerable вашего интерфейса службы! А затем найдите экземпляр, который вы хотите использовать LINQ.

мой пример для сервиса AWS SNS, но вы можете сделать то же самое для любого введенного сервиса.

запуск

foreach (string snsRegion in Configuration["SNSRegions"].Split(',', StringSplitOptions.RemoveEmptyEntries))
{
    services.AddAWSService<IAmazonSimpleNotificationService>(
        string.IsNullOrEmpty(snsRegion) ? null :
        new AWSOptions()
        {
            Region = RegionEndpoint.GetBySystemName(snsRegion)
        }
    );
}

services.AddSingleton<ISNSFactory, SNSFactory>();

services.Configure<SNSConfig>(Configuration);

SNSConfig

public class SNSConfig
{
    public string SNSDefaultRegion { get; set; }
    public string SNSSMSRegion { get; set; }
}

appsettings.в JSON

  "SNSRegions": "ap-south-1,us-west-2",
  "SNSDefaultRegion": "ap-south-1",
  "SNSSMSRegion": "us-west-2",

фабрика SNS

public class SNSFactory : ISNSFactory
{
    private readonly SNSConfig _snsConfig;
    private readonly IEnumerable<IAmazonSimpleNotificationService> _snsServices;

    public SNSFactory(
        IOptions<SNSConfig> snsConfig,
        IEnumerable<IAmazonSimpleNotificationService> snsServices
        )
    {
        _snsConfig = snsConfig.Value;
        _snsServices = snsServices;
    }

    public IAmazonSimpleNotificationService ForDefault()
    {
        return GetSNS(_snsConfig.SNSDefaultRegion);
    }

    public IAmazonSimpleNotificationService ForSMS()
    {
        return GetSNS(_snsConfig.SNSSMSRegion);
    }

    private IAmazonSimpleNotificationService GetSNS(string region)
    {
        return GetSNS(RegionEndpoint.GetBySystemName(region));
    }

    private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region)
    {
        IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region);

        if (service == null)
        {
            throw new Exception($"No SNS service registered for region: {region}");
        }

        return service;
    }
}

public interface ISNSFactory
{
    IAmazonSimpleNotificationService ForDefault();

    IAmazonSimpleNotificationService ForSMS();
}

теперь вы можете получить услугу SNS для региона, который вы хотите в своем обычае сервис или контроллер

public class SmsSender : ISmsSender
{
    private readonly IAmazonSimpleNotificationService _sns;

    public SmsSender(ISNSFactory snsFactory)
    {
        _sns = snsFactory.ForSMS();
    }

    .......
 }

public class DeviceController : Controller
{
    private readonly IAmazonSimpleNotificationService _sns;

    public DeviceController(ISNSFactory snsFactory)
    {
        _sns = snsFactory.ForDefault();
    }

     .........
}

хотя кажется, что @Miguel A. Arilla четко указал на это, и я проголосовал за него, я создал поверх его полезного решения другое решение, которое выглядит аккуратно, но требует гораздо больше работы.

это определенно зависит от вышеуказанного решения. Поэтому в основном я создал что-то похожее на Func<string, IService>> и я назвал его IServiceAccessor в качестве интерфейса, а затем мне пришлось добавить еще несколько пристроек к IServiceCollection такие как:

public static IServiceCollection AddSingleton<TService, TImplementation, TServiceAccessor>(
            this IServiceCollection services,
            string instanceName
        )
            where TService : class
            where TImplementation : class, TService
            where TServiceAccessor : class, IServiceAccessor<TService>
        {
            services.AddSingleton<TService, TImplementation>();
            services.AddSingleton<TServiceAccessor>();
            var provider = services.BuildServiceProvider();
            var implementationInstance = provider.GetServices<TService>().Last();
            var accessor = provider.GetServices<TServiceAccessor>().First();

            var serviceDescriptors = services.Where(d => d.ServiceType == typeof(TServiceAccessor));
            while (serviceDescriptors.Any())
            {
                services.Remove(serviceDescriptors.First());
            }

            accessor.SetService(implementationInstance, instanceName);
            services.AddSingleton<TServiceAccessor>(prvd => accessor);
            return services;
        }

сервис доступа выглядит например:

 public interface IServiceAccessor<TService>
    {
         void Register(TService service,string name);
         TService Resolve(string name);

    }

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

    services.AddSingleton<IEncryptionService, SymmetricEncryptionService, EncyptionServiceAccessor>("Symmetric");
    services.AddSingleton<IEncryptionService, AsymmetricEncryptionService, EncyptionServiceAccessor>("Asymmetric");

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

был еще один пост на stackoverflow, но я не могу его найти, где плакат подробно объяснил почему эта функция не поддерживается и как ее обойти, в основном похоже на то, что заявил @Miguel. Это был хороший пост, хотя я не согласен с каждым пунктом, потому что я думаю, что есть ситуация, когда вам действительно нужны именованные экземпляры. Я опубликую эту ссылку здесь, как только найду ее снова.

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

Я использую следующий код в своем проекте, и до сих пор он работал хорошо.

 /// <summary>
    /// Adds the singleton.
    /// </summary>
    /// <typeparam name="TService">The type of the t service.</typeparam>
    /// <typeparam name="TImplementation">The type of the t implementation.</typeparam>
    /// <param name="services">The services.</param>
    /// <param name="instanceName">Name of the instance.</param>
    /// <returns>IServiceCollection.</returns>
    public static IServiceCollection AddSingleton<TService, TImplementation>(
        this IServiceCollection services,
        string instanceName
    )
        where TService : class
        where TImplementation : class, TService
    {
        var provider = services.BuildServiceProvider();
        var implementationInstance = provider.GetServices<TService>().LastOrDefault();
        if (implementationInstance.IsNull())
        {
            services.AddSingleton<TService, TImplementation>();
            provider = services.BuildServiceProvider();
            implementationInstance = provider.GetServices<TService>().Single();
        }
        return services.RegisterInternal(instanceName, provider, implementationInstance);
    }

    private static IServiceCollection RegisterInternal<TService>(this IServiceCollection services,
        string instanceName, ServiceProvider provider, TService implementationInstance)
        where TService : class
    {
        var accessor = provider.GetServices<IServiceAccessor<TService>>().LastOrDefault();
        if (accessor.IsNull())
        {
            services.AddSingleton<ServiceAccessor<TService>>();
            provider = services.BuildServiceProvider();
            accessor = provider.GetServices<ServiceAccessor<TService>>().Single();
        }
        else
        {
            var serviceDescriptors = services.Where(d => d.ServiceType == typeof(IServiceAccessor<TService>));
            while (serviceDescriptors.Any())
            {
                services.Remove(serviceDescriptors.First());
            }
        }
        accessor.Register(implementationInstance, instanceName);
        services.AddSingleton<TService>(prvd => implementationInstance);
        services.AddSingleton<IServiceAccessor<TService>>(prvd => accessor);
        return services;
    }

    //
    // Summary:
    //     Adds a singleton service of the type specified in TService with an instance specified
    //     in implementationInstance to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
    //
    // Parameters:
    //   services:
    //     The Microsoft.Extensions.DependencyInjection.IServiceCollection to add the service
    //     to.
    //   implementationInstance:
    //     The instance of the service.
    //   instanceName:
    //     The name of the instance.
    //
    // Returns:
    //     A reference to this instance after the operation has completed.
    public static IServiceCollection AddSingleton<TService>(
        this IServiceCollection services,
        TService implementationInstance,
        string instanceName) where TService : class
    {
        var provider = services.BuildServiceProvider();
        return RegisterInternal(services, instanceName, provider, implementationInstance);
    }

    /// <summary>
    /// Registers an interface for a class
    /// </summary>
    /// <typeparam name="TInterface">The type of the t interface.</typeparam>
    /// <param name="services">The services.</param>
    /// <returns>IServiceCollection.</returns>
    public static IServiceCollection As<TInterface>(this IServiceCollection services)
         where TInterface : class
    {
        var descriptor = services.Where(d => d.ServiceType.GetInterface(typeof(TInterface).Name) != null).FirstOrDefault();
        if (descriptor.IsNotNull())
        {
            var provider = services.BuildServiceProvider();
            var implementationInstance = (TInterface)provider?.GetServices(descriptor?.ServiceType)?.Last();
            services?.AddSingleton(implementationInstance);
        }
        return services;
    }

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

public interface IStage<out T> : IStage { }

public interface IStage {
      void DoSomething();
}

создайте свои различные реализации

public class YourClassA : IStage<YouClassA> { 
    public void DoSomething() 
    {
        ...TODO
    }
}

public class YourClassB : IStage<YourClassB> { .....etc. }

Регистрация

services.AddTransient<IStage<YourClassA>, YourClassA>()
services.AddTransient<IStage<YourClassB>, YourClassB>()

конструктор и использование экземпляра...

public class Whatever
{
   private IStage ClassA { get; }

   public Whatever(IStage<YourClassA> yourClassA)
   {
         ClassA = yourClassA;
   }

   public void SomeWhateverMethod()
   {
        ClassA.DoSomething();
        .....
   }

заводской подход, безусловно, жизнеспособен. Другой подход заключается в использовании наследования для создания отдельных интерфейсов, наследуемых от IService, реализации унаследованных интерфейсов в реализациях IService и регистрации унаследованных интерфейсов, а не базы. Является ли добавление иерархии наследования или фабрик "правильным" шаблоном, все зависит от того, с кем вы говорите. Мне часто приходится использовать этот шаблон при работе с несколькими поставщиками баз данных в одном приложении, что использует универсальный, например IRepository<T>, как основа для доступа к данным.

пример интерфейсов и реализаций:

public interface IService 
{
}

public interface IServiceA: IService
{}

public interface IServiceB: IService
{}

public IServiceC: IService
{}

public class ServiceA: IServiceA 
{}

public class ServiceB: IServiceB
{}

public class ServiceC: IServiceC
{}

контейнер:

container.Register<IServiceA, ServiceA>();
container.Register<IServiceB, ServiceB>();
container.Register<IServiceC, ServiceC>();

хотя реализация out of the box не предлагает этого, вот пример проекта, который позволяет регистрировать именованные экземпляры, а затем вводить INamedServiceFactory в ваш код и извлекать экземпляры по имени. В отличие от других решений facory здесь, это позволит вам зарегистрировать несколько экземпляров та же реализация но настроен по-другому

https://github.com/macsux/DotNetDINamedInstances


Как насчет услуг?

Если бы у нас был интерфейс INamedService (С. Name property), мы могли бы написать расширение IServiceCollection для .GetService (string name), где расширение будет принимать этот строковый параметр и делать a .GetServices () на себе и в каждом возвращаемом экземпляре найдите экземпляр, чей INamedService.Имя соответствует данному имени.

такой:

public interface INamedService
{
    string Name { get; }
}

public static T GetService<T>(this IServiceProvider provider, string serviceName)
    where T : INamedService
{
    var candidates = provider.GetServices<T>();
    return candidates.FirstOrDefault(s => s.Name == serviceName);
}

поэтому ваш IMyService должен реализовать INamedService, но вы получите разрешение на основе ключа, которое хотите, верно?

справедливости ради, необходимость даже иметь этот интерфейс INamedService кажется уродливой, но если вы хотите пойти дальше и сделать вещи более элегантными, то [NamedServiceAttribute("A")] на реализации/классе может быть найден кодом в этом расширении, и это будет работать так же хорошо. Чтобы быть еще более справедливым, отражение медленное, поэтому оптимизация может быть в порядке, но, честно говоря, это то, что двигатель DI должен был помочь. Скорость и простота-каждый большой вклад в TCO.

в целом, нет необходимости в явной фабрике, потому что" поиск именованной службы " является такой многоразовой концепцией, а классы фабрики не масштабируются как решение. И Func кажется прекрасным, но блок коммутатора так bleh, и опять же, вы будете писать функции так же часто, как и писать фабрики. Начните простой, многоразовый, с меньшим кодом, и если это окажется не для вас, тогда идите сложным.


Я просто впрыскиваю IEnumerable

ConfigureServices при запуске.cs

Assembly.GetEntryAssembly().GetTypesAssignableFrom<IService>().ForEach((t)=>
                {
                    services.AddScoped(typeof(IService), t);
                });

Папку Услуги

public interface IService
{
    string Name { get; set; }
}

public class ServiceA : IService
{
    public string Name { get { return "A"; } }
}

public class ServiceB : IService
{    
    public string Name { get { return "B"; } }
}

public class ServiceC : IService
{    
    public string Name { get { return "C"; } }
}

MyController.cs

public class MyController
{
    private readonly IEnumerable<IService> _services;
    public MyController(IEnumerable<IService> services)
    {
        _services = services;
    }
    public void DoSomething()
    {
        var service = _services.Where(s => s.Name == "A").Single();
    }
...
}

расширения.cs

    public static List<Type> GetTypesAssignableFrom<T>(this Assembly assembly)
    {
        return assembly.GetTypesAssignableFrom(typeof(T));
    }
    public static List<Type> GetTypesAssignableFrom(this Assembly assembly, Type compareType)
    {
        List<Type> ret = new List<Type>();
        foreach (var type in assembly.DefinedTypes)
        {
            if (compareType.IsAssignableFrom(type) && compareType != type)
            {
                ret.Add(type);
            }
        }
        return ret;
    }

опоздали на эту вечеринку, но вот мое решение:...

Автозагрузка.cs или программа.cs, если общий обработчик...

services.AddTransient<IMyInterface<CustomerSavedConsumer>, CustomerSavedConsumer>();
services.AddTransient<IMyInterface<ManagerSavedConsumer>, ManagerSavedConsumer>();

IMyInterface настройки интерфейса T

public interface IMyInterface<T> where T : class, IMyInterface<T>
{
    Task Consume();
}

конкретные реализации IMyInterface T

public class CustomerSavedConsumer: IMyInterface<CustomerSavedConsumer>
{
    public async Task Consume();
}

public class ManagerSavedConsumer: IMyInterface<ManagerSavedConsumer>
{
    public async Task Consume();
}

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


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

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

@Override
public void messageArrived(String type, Message message) throws Exception {
    switch(type) {
        case "A": do Something with message; break;
        case "B": do Something else with message; break;
        ...
    }
}

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

@Override
public void messageArrived(String type, Message message) throws Exception {
     messageHandler.getMessageHandler(type).handle(message);
}

это хорошо, не так ли? Так как мы можем достичь чего-то подобного?

сначала давайте определим аннотацию, а также интерфейс

@Retention(RUNTIME)
@Target({TYPE})
public @interface MessageHandler {
     String value()
}

public interface Handler {
    void handle(MqttMessage message);
}

а теперь давайте используем эту аннотацию в классе:

@MessageHandler("myTopic")
public class MyTopicHandler implements Handler {

    @override
    public void handle(MqttMessage message) {
        // do something with the message
    }
}

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

public Handler getMessageHandler(String topic) {
    if (handlerMap == null) {
        loadClassesFromClassPath(); // This method should load from classpath all the classes with the annotation MessageHandler and build the handlerMap
    }
    return handlerMap.get(topic);
}

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

надеюсь, это поможет вам немного.