避免实现工厂中的循环依赖

问题描述 投票:0回答:1

我正在进程中使用 Azure Functions。

我的功能之一是一个端点,它可以根据请求数据做很多事情,根据所述请求数据,将调用命令类的执行方法。我想注册这些命令类,因为它们可以自己使用服务。

CommandService
本身也充当网关,目前,允许从命令类调用最初调用该函数的API,该函数不是对其的直接响应。基本上有时我可以直接回复并在函数返回的 httpresponse 中给出答案。

但是有时需要进行处理并且答案会被延迟,所以我发回一个 httpresponse 说我收到了请求,但对该请求的处理后的答案稍后会出现。处理完成后,发送答案就是我这边向最初向我发送请求的 API 发起的请求。

问题就在这里,因为尝试将

CommandService
注入命令类会导致循环依赖,但是我想使用反射来自动使用所有存在的命令填充 CommandService,这样我就可以添加新命令而无需无需担心手动创建和维护大量注册列表。

// This is a method called in Startup.Configure()
public void ConfigureCommands(IFunctionsHostBuilder builder)
{
    var commandClasses = Assembly.GetAssembly(typeof(Command)).GetTypes().Where(x => typeof(Command).IsAssignableFrom(x) && !x.IsAbstract).ToArray();

    foreach (var command in commandClasses)
        builder.Services.AddTransient(command);

    builder.Services.AddSingleton<ICommandService>(x =>
    {
        var service = x.GetRequiredService<CommandService>();
        foreach (var command in commandClasses)
            service.RegisterCommand((Command)x.GetRequiredService(command)); // This causes the circular dependency, because a command tries to inject ICommandService

        return service;
    });
}

然后在我的函数中,我解析请求数据,检查有效性等,并最终将结果传递给我的

CommandService
,然后它会找到要执行的命令。

// This is a method in CommandService
public async Task<CommandResponse> ExecuteCommand(CommandRequestData requestData)
{
    _logger.LogDebug($"Command received:\n{JsonConvert.SerializeObject(requestData)}");

    return await _commands.First(x => x.CommandName == request.name).Execute(requestData);
}

据我所知,由于在

Startup.Configure()
调用和函数调用之间没有空缺,我将如何调用我的注册/初始化?

我试过打电话给

builder.Service.BuildServiceProvider()
,但似乎目前还没有注册所有内容,所以这是一个死胡同。

我想通过将与此 API 相关的内容保留在同一个服务类中来保持简洁,那么有没有一种干净的方法可以做到这一点,或者我是否最好将与处理请求的不同服务中的 API 进行对话?

c# dependency-injection azure-functions
1个回答
0
投票

肮脏而快速的回答

更换:

var service = x.GetRequiredService<CommandService>();

与:

var logger = x.GetRequiredService<ILogger<CommandService>>();
var service = new CommandService(logger);

如果需要,解决并添加

CommandService
的其他依赖项。

完整答案

使用以下解决方案,我假设“可以根据请求数据执行许多操作的端点”是不可触及且无法更改的,因此我们必须根据命令中包含的字符串名称进行命令类型发现请求数据。

首先,我们可以只扫描一次,而不是每次需要命令时都扫描命令名称。我们可以利用内置 DI 支持解析相同依赖项的序列这一事实,因此我们可以要求 DI 提供

Command
的所有实现,并从中创建一个漂亮的字典,以命令名称作为键。

public class CommandLookup : ICommandLookup
{
    private readonly Dictionary<string, Type> _commandTypes;

    public CommandLookup(IEnumerable<Command> commands)
    {
        _commandTypes = commands.ToDictionary(x => x.CommandName, x => x.GetType());
    }

    public Type GetCommandType(string name)
    {
        return _commandTypes[name];
    }
}

public interface ICommandLookup
{
    Type GetCommandType(string name);
}

但这只会给我们命令类型,我们仍然需要初始化它。最好不要手动执行。我们可以使用

IServiceProvider
通过工厂模式来实现这一点。值得注意的是,工厂的这种实现是基于服务定位器模式的,这对于某些人来说被视为反模式。但是,考虑到本答案开头的陈述,我们就顺其自然吧。

public class CommandFactory : ICommandFactory
{
    private readonly IServiceProvider _serviceProvider;

    public CommandFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public Command CreateCommand(Type commandType)
    {
        return _serviceProvider.GetRequiredService(commandType) as Command;
    }
}

public interface ICommandFactory
{
    Command CreateCommand(Type commandType);
}

命令本身必须包含这些最少的信息、名称和执行的抽象方法:

public abstract class Command
{
    public abstract string CommandName { get; }

    public abstract Task<CommandResponse> Execute(CommandRequestData requestData);
}

命令服务将不再需要注册部分,只需要执行逻辑。命令类型使用查找来解析,命令本身使用命令工厂初始化,同时传递获得的类型。

public class CommandService : ICommandService
{
    private readonly ICommandLookup _commandLookup;
    private readonly ICommandFactory _commandFactory;
    private readonly ILogger<CommandService> _logger;

    public CommandService(
        ICommandLookup commandLookup,
        ICommandFactory commandFactory,
        ILogger<CommandService> logger)
    {
        _commandLookup = commandLookup;
        _commandFactory = commandFactory;
        _logger = logger;
    }

    public async Task<CommandResponse> ExecuteCommand(CommandRequestData requestData)
    {
        _logger.LogDebug("Command received:\n{commandData}", JsonConvert.SerializeObject(requestData));

        var commandType = _commandLookup.GetCommandType(requestData.Name);
        var command = _commandFactory.CreateCommand(commandType);

        return await command.Execute(requestData);
    }
}

最后,

IServiceCollection
内的注册部分:

public void ConfigureCommands(IFunctionsHostBuilder builder)
{
    var commandClasses = Assembly.GetAssembly(typeof(Command))
        .GetTypes()
        .Where(x => typeof(Command).IsAssignableFrom(x) && !x.IsAbstract)
        .ToArray();

    foreach (var commandType in commandClasses)
    {
        builder.Services.AddTransient(commandType);
        builder.Services.AddTransient(typeof(Command), commandType);
    }

    builder.Services.AddScoped<ICommandService, CommandService>();
    builder.Services.AddSingleton<ICommandLookup, CommandLookup>();
    builder.Services.AddTransient<ICommandFactory, CommandFactory>();
}

本节的一些解释:

  • 每个命令注册两次,第一次用于查找目的,因此我们可以通过
    Command
    类型解析所有命令,第二次用于工厂,因此使用具体类型我们只能解析我们实际想要的命令
  • 查找被注册为单例,因为我们只想扫描命令一次 - 当查找最初创建时

值得一读

值得了解的图书馆

© www.soinside.com 2019 - 2024. All rights reserved.