在它可以 return 具体 class 之前调用异步方法的工厂

Factory that calls an async method before it can return concrete class

我正在为一段代码苦苦挣扎,可以使用一些指针。

我正在为前端网站创建 API。这个API可以根据租户调用各种后台系统。调用哪个系统保存在数据库中的租户条目中。

我的 API 项目由几个不同的项目组成:

我的 API 调用我的逻辑层,例如检索客户对象。在此调用中传递了一个 tenantId(取自授权身份声明)。我的逻辑做了一些检查 然后想调用租户特定的后端系统。为此,我需要执行以下操作:

数据class实现了接口IBackendConnector。

因为我的所有逻辑 classes 都可能调用租户特定数据,所以我想创建一个接受 tenantId 的工厂,并且 returns 一个 class 实现 IBackendConnector . 检索租户的数据库调用是异步的,API 调用本身是异步的。

我创建了一个名为 'DataFactory' 的抽象 class,它具有(public 和私有)属性 IBackendConnector,我们将其命名为 Connector。它还有一个需要 tenantid 的构造函数,它保存为 私人 属性。每个逻辑 class 都实现了这个 DataFactory。当我需要调用后端时,我可以简单地调用一些漂亮干净的代码:

Connector.MethodIWhishToCall();

在public属性的getter里我检查private里有没有值属性,如果没有,就需要检索tenant设置,并创建正确的数据 class,所以我们只创建 class 在我们需要的时候。这是我失败的地方,因为我无法在此 属性 getter 中调用异步方法(没有死锁)。

我对这段代码做了很多改动,我对我当前使用的解决方案不满意,这是一个抽象 class,在每个逻辑 class 中实现(这需要它),其中有一个静态异步方法。

public abstract class ExternalDataFactory
{
    public static async Task<IBackendDataConnector> GetDataHandler(string tenantId)
    {
        var logic = new TenantLogic();
        var tenant = await logic.GetTenantById(tenantId);  
        var settings = tenant.Settings;

        if (settings == null)
            throw new ArgumentNullException("Settings");

        switch (settings.EnvironmentType)
        {
            case Constants.EnvironmentType.ExampleA:
                return new DataHandlerClass(settings);
            case Constants.EnvironmentType.ExampleB:
                throw new NotImplementedException();
            default:
                throw new Exception("Invalid EnvironmentType");
        };
    }
}

并且每个使用此处理程序的方法都调用以下代码:

var handler = await GetDataHandler(tenantId);
return await handler.DoSomething();

或在一行中:

return await (await GetDataHandler(tenantId)).DoSomething();

最终目标:所有逻辑 classes 必须能够调用正确的数据 class 而不必担心是哪个数据 class,也不必先手动检索设置并将其传递给工厂。工厂 应该能够检索设置(如果设置为空)和 return 正确的 IBackendConnector 实现。执行此操作的最佳实践/最佳模式是什么?

TL/DR:试图实现一个工厂模式,需要调用一个异步方法来决定具体的 class 到 return。解决此问题的最佳方法是什么?

我没有在您的代码中看到定义抽象的任何充分理由 class,因为它定义的唯一方法是静态的。 所以我建议的第一件事是让你的 class 非抽象。

让我们将GetDataHandler重命名为CreateDataHandlerAsync,以明确向您工厂的消费者表明此方法是async。您还可以看到我从方法签名中删除了 static。 对我来说,它做了太多事情,让我们让 TenantLogic 成为这个工厂的依赖项。

public interface ITenantLogic
{    
   Task<Tenant> GetTenantByIdAsync(int tenantId);
}

public class TenantLogic: ITenantLogic
{
   public async Task<Tenant> GetTenantByIdAsync(int tenantId)
   {
      // logic to get the tenant goes here
   }
}

现在让我们把它定义为我们工厂中的依赖

public class ExternalDataFactory
{
   private readonly ITenantLogic _tenantLogic;
   public ExternalDataFactory(ITenantLogic tenantLogic)
   {
       if(tenantLogic == null) throw new ArgumentNullException("tenantLogic");
       _tenantLogic = tenantLogic;
   }

   public async Task<IBackendDataConnector> CreateDataHandlerAsync(string tenantId)
        {
            var tenant = await _tenantLogic.GetTenantByIdAsync(tenantId);  
            var settings = tenant.Settings;

            if (settings == null)
                throw new ArgumentException("Specified tenant  has no settings defined");

            switch (settings.EnvironmentType)
            {
                case Constants.EnvironmentType.ExampleA:
                    return new DataHandlerClass(settings);
                case Constants.EnvironmentType.ExampleB:
                    throw new NotImplementedException();
                default:
                    throw new Exception("Invalid EnvironmentType");
            };
        }
    }

我建议也更改 DoSomething 的方法名称,因为 运行 async 使它成为 DoSomethingAsync()。根据经验,我建议在所有异步方法后缀 Async

现在是客户端代码。

var factory = new ExternalDataFactory(new TenantLogic());
IBackendDataConnector dataConnector =
   await factory.CreateDataHandlerAsync(tenantId);
return await dataConnector.DoSomethingAsync();

最后但同样重要的是,我也会考虑如何在 CreateDataHandlerAsync 方法中摆脱 switch/case

我认为这个版本可以是一个很好的起点 - 它会实现你的最终目标 - 在支持新环境时,你将实现具体数据 class,添加一个新的 case 语句(说真的,我虽然会删除它)并且您的所有客户都可以享受工厂支持新环境的好处。

希望这对您有所帮助。


编辑

进一步了解这一点,我想再补充一件事。 我真的不认为 ExternalDataFactory 应该了解租户,如何找到租户等

我认为工厂的消费者代码应该处理租户的检索并将环境传递给 factory,这反过来会创建一个具体的数据 class 和 return。

正如我之前所说,如果我们能够摆脱 switch/case

,我们可以使工厂更加灵活和优雅 (IMO)

我们先重新定义相关的数据连接器classes/interfaces

public interface IBackendDataConnector
{
    public Task<Something> DoSomethingAsync(Settings settings);
    public bool CanHandleEnvironment(EnvironmentType environment);
}

public abstract class DataHandlerAbstractBase: IBackendDataConnector
{
   protected abstract EnvironmentType Environment { get; }

   // interface API
   public abstract async Task<Something> DoSomethingAsync(Settings settings);

   public virtual bool CanHandleEnvironment(EnvironmentType environment)
   {
       return environment == Environment;
   }
}

public class DataHandlerClass: DataHandlerAbstractBase
{
   protected override EnvironmentType Environment 
   {
     get { return EnvironmentType.ExampleA; }
   }

   public override async Task<Something> DoSomethingAsync(Settings settings)
   {
     // implementation goes here
   }
}

现在有了上面的内容让我们重新审视ExternalDataFactory

 public class ExternalDataFactory
 {
    private readonly IEnumerable<IBackendDataConnector> _dataConnectors =
          new [] {new DataHandlerClass() /* other implementations */}

    public IBackendDataConnector CreateDataHandler(Settings setting)
    {                
       IBackendDataConnector connector = _dataConnectors
.FirstOrDefault(
     c => c.CanHandleEnvironment(setting.EnvironmentType));

       if (connector == null)
       {
           throw new ArgumentException("Unsupported environment type");
       }

       return connector;
    }
 }

关于重构的几句话。

如前所述,现在工厂方法不会 know/bother 通过其 ID 等获取租户。它所能做的就是根据传递的环境(它从传递的设置中读取)创建具体数据 class进去)。

现在每个具体数据 class 都可以说明它是否可以处理给定的环境,所以我们只需将调用从工厂委托给它。每个具体数据 class 实现都必须定义环境 属性(由于继承自抽象基础 DataHandlerAbstractBase

有了这些更改,现在客户必须负责获取租户并将其传递给工厂

var tenantLogic = new TenantLogic();
var tenant = await tenantLogic.GetTenantByIdAsync(tenantId); 
var factory = new ExternalDataFactory();
IBackendDataConnector dataConnector =
        factory.CreateDataHandler(tenant.Settings);
return await dataConnector.DoSomethingAsync(tenant.Settings);