【问题标题】:How to implement interface with additional parameters/info per implementation如何使用每个实现的附加参数/信息来实现接口
【发布时间】:2018-08-14 12:24:30
【问题描述】:

我的 MVC webapp 允许用户添加和删除图像。 UI 在我的业务层中调用 ImageService.SaveImage(...),它在内部使用一个标志来告诉方法保存到 Azure 或文件系统。我最终可能会将 S3 添加到我认为这里的界面会很好用。

这就是我在 ImageService 类中对代码进行成像的样子。它不关心文件的保存方式或位置。

// Service the UI uses
public static class ImageService
{
    public static void SaveImage(byte[] data, IImageProvider imageProvider)
    {
        string fileName = "some_generated_name.jpg"
        imageProvider.Save(fileName, data);
    }
}

所以我创建了这些实现

public interface IImageProvider
{
    void Save(string filename, byte[] imageData);
    byte[] Get(string filename);
    void Delete(string filename);
}

// File system implementation
public class FSImageProvider : IImageProvider
{
    public void Delete(string filename)
    {
        File.Delete(filename);
    }

    public byte[] Get( filename)
    {
        return File.ReadAllBytes(filename);
    }

    public void Save(string filename, byte[] imageData)
    {
        File.WriteAllBytes(filename, imageData);
    }
}

// Azure implementation
public class AzureBlobImageProvider : IImageProvider
{
    private string _azureKey = "";
    private string _storageAccountName = "";

    public AzureBlobImageProvider(string azureKey, string storageAccount)
    {
        _azureKey = azureKey;
        _storageAccountName = storageAccount;
    }

    public void Delete(string filename)
    {
        throw new NotImplementedException();
    }

    public byte[] Get(string filename)
    {
        throw new NotImplementedException();
    }

    public void Save(string filename, byte[] imageData)
    {
        throw new NotImplementedException();
    }
}

问题 1) 传递每个提供商可能需要的其他信息的最佳方式是什么? IE。 Azure 需要知道容器名称、blob 名称(文件名)和 storageAccount 名称。 S3 可能还需要更多。一个很好的例子是文件路径。这可能因每个提供商而异,或者根本不存在。 Azure 需要容器名称,文件系统需要目录名称。如果每个提供商的它们都不同,我将如何将其添加到界面中?

问题2)我应该使用依赖注入来解析业务层ImageService类中的接口还是应该在UI中解析并传递给类?

【问题讨论】:

  • “容器名称”、“存储帐户” - 对我来说听起来像是配置数据。您会将配置数据存储在哪里?
  • @Fildor 我想将它存储在 UI 的设置文件或 appSettings 中。我相信业务层中的服务需要将它们传入而不是存储在该级别,但不确定。好问题。
  • 你现在要问的问题是:这些具体参数将如何/谁/何时传递?你想让调用者动态传递它们吗?您希望将它们配置为应用程序吗?也许某些参数是动态的,而其他参数是配置的?尝试用用例推理。

标签: c# dependency-injection interface


【解决方案1】:

首先,将 IImageProvider 传递给 SaveImage 方法有很大的架构缺点。在您的 IImageProvider 的生命周期需要跨方法控制的情况下,您需要这种函数签名。在您的情况下,您只是保存图像并且几乎不关心您的任何类的任何生命周期,但仍然使用这种方法,它最终会使您的代码混乱,duh - 您甚至不关心这个提供者特别(这就是为什么你将它包装到我认为的界面中)

问问自己:

“我的 IImageProvider 是否真的在 ImageService 之外的任何地方使用过? 如果不是,为什么每个人(方法、类)都需要知道它是 存在吗?”

其次,而不是创建提供程序 - 使您的 ImageService 简单类(删除静态),为其定义接口,并为 Azure/FS/等实现。具体实施使用工厂:

public interface IImageService
{
    void SaveImage(byte[] bytes);
}

public interface IImageServiceFactory
{
    IImageService Create(/*here goes enum, string, connections strings, etc*/);
}


internal sealed class AzureImageService : IImageService {/*implmentation*/}
internal sealed class FileSystemImageService : IImageService {/*implmentation*/}

总体

不要在方法中传递依赖关系。您的方法应该看起来很简单,没有像 ILoggerIImageProvider 之类的任何您认为可以传入内部的杂乱无章的东西。如果您在某些实现中的某个时刻需要它们 - 只需创建通过构造函数获取所需的所有依赖项的类(因此 static 修饰符几乎总是被禁止并仅用于语言扩展)。您可以更轻松地管理依赖和重构代码,而不会因为在甚至不需要的地方不断重用相同的界面而造成任何直接损害。

【讨论】:

    【解决方案2】:

    通常这种类型的实现特定数据将在具体类构造函数中提供,就像您在示例中所做的那样。

    我不会在 UI 中创建具体实例。您可以使用依赖注入,也可以使用一组工厂类来创建具有适当配置的实例。关键是您希望将这些服务的配置集中到一个位置,而不是在整个应用程序中散布特定于实现的代码。

    【讨论】:

    • 我认为这可能是我断开连接的地方。我的所有服务都在业务层中,我认为它是供我的 UI 使用的 API。听起来我应该有一个服务接口,而不是将接口作为参数传递。你能举一个工厂模式的例子吗?听起来即使我没有将服务移动到接口,这也可能解决我当前的问题
    • 另外,我的想法是 ImageService 知道每个 IImageProvider 实现中不包含的一些其他逻辑,我是否需要重新设计以在每个实现中复制该逻辑?
    【解决方案3】:
    1. 您可以考虑在配置文件中包含每个实现的额外信息,例如在您的 Web 应用程序中您将拥有 web.config。您可以在任何实现中访问它们中的任何一个。

    2. 我建议使用依赖注入而不是将IImageProvider 作为参数传递。如果解析实现的逻辑足够复杂,您甚至可以有一个ImageProviderResolver 并将解析FileImageProviderAzureImageProvider 等的逻辑封装在那里,然后调用解析器来解析@987654326 中的ImageProvider 实例@

    【讨论】:

      【解决方案4】:

      最好的方法是使图像服务类通用。

          // Service the UI uses
          public class ImageService<T>  where T : class, new()
          {
                public ImageService(T t)
                {
                     _imageProvider = t;
                }
                private IImageProvider _imageProvider;
                public void SaveImage(byte[] data, string fileName)
                {
                  //string fileName = "some_generated_name.jpg"
                  _imageProvider.Save(fileName, data);
                }
          }
      

      【讨论】:

        【解决方案5】:

        1) 你的问题是如何为同一个界面创建不同的子类型。 您可以使用诸如 Abstract Factory of Builder 之类的创建模式来解决此问题。这两种模式都允许您封装区分签名的每个对象的构造逻辑。 将你创建的每一个 ImageProvider 封装到一个工厂中,根据需要接受不同的参数。

        2) 这里的问题有点广泛。 您错过了 ImageService 和 ImageProvider 之间的一个类。 ImageService 只需从 UI 接收输入并将它们路由到实现整个​​保存逻辑的类(包括保存位置)。 如果用户需要决定保存在哪里,那么您只需向 SaveImange 方法传递一个参数(枚举、字符串)来指示目标。

        您错过的类(我们称之为 SaveImageCommandHandler)负责 1)根据参数创建正确的 ImageProvider,以及 2)在该类上调用 Save。

        要在 ImageService 上获取此类的实例,您可以 1) 使用 new 创建它,或者更好的是 2) 将其注入 ImageService 构造函数中或 3) 更好地使用即时解析依赖关系的 CommandDispatcher。

        您的命令处理程序将注入创建所需的所有 ImageProvider 所需的所有工厂/构建器,并根据参数或您的内部逻辑决定要调用的女巫。

        【讨论】:

          【解决方案6】:

          您可以从 web.config/app.config 中选择要使用的图像提供程序。首先添加一些适当的配置条目:

          <configuration>
            <imageProviderConfiguration>
              <imageProvider name="fsImageProvider" />
              <fsImageProvider directory="C:\Images" />
              <azureImageProvider azureKey="foo" storageAccountName="bar" />
            </imageProviderConfiguration>
          </configuration>
          

          您需要包含类才能从配置文件中访问此信息:

          public class ImageProviderSection : ConfigurationSection
          {
              public const string ImageProviderConfigurationSection = "imageProviderConfiguration";
              public const string ImageProviderProperty = "imageProvider";
              public const string FSImageProviderProperty = "fsImageProvider";
              public const string AzureImageProviderProperty = "azureImageProvider";
          
              [ConfigurationProperty(ImageProviderProperty)]
              public ImageProviderElement ImageProvider
              {
                  get { return (ImageProviderElement)this[ImageProviderProperty]; }
                  set { this[ImageProviderProperty] = value; }
              }
          
              [ConfigurationProperty(FSImageProviderProperty)]
              public FSImageProviderElement FSImageProvider
              {
                  get { return (FSImageProviderElement)this[FSImageProviderProperty]; }
                  set { this[FSImageProviderProperty] = value; }
              }
          
              [ConfigurationProperty(AzureImageProviderProperty)]
              public AzureImageProviderElement AzureImageProvider
              {
                  get { return (AzureImageProviderElement)this[AzureImageProviderProperty]; }
                  set { this[AzureImageProviderProperty] = value; }
              }
          }
          public class ImageProviderElement : ConfigurationElement
          {
              private const string nameProperty = "name";
              [ConfigurationProperty(nameProperty, IsRequired = true)]
              public string Name
              {
                  get { return (string)this[nameProperty]; }
                  set { this[nameProperty] = value; }
              }
          }
          public class FSImageProviderElement : ConfigurationElement
          {
              private const string directoryProperty = "directory";
              [ConfigurationProperty(directoryProperty, IsRequired = true)]
              public string Directory
              {
                  get { return (string)this[directoryProperty]; }
                  set { this[directoryProperty] = value; }
              }
          }
          public class AzureImageProviderElement : ConfigurationElement
          {
              private const string azureKeyProperty = "azureKey";
              private const string storageAccountNameProperty = "storageAccountName";
              [ConfigurationProperty(azureKeyProperty, IsRequired = true)]
              public string AzureKey
              {
                  get { return (string)this[azureKeyProperty]; }
                  set { this[azureKeyProperty] = value; }
              }
              [ConfigurationProperty(storageAccountNameProperty, IsRequired = true)]
              public string StorageAccountName
              {
                  get { return (string)this[storageAccountNameProperty]; }
                  set { this[storageAccountNameProperty] = value; }
              }
          }
          

          现在您可以确定在读取配置文件的简单工厂类中使用哪个提供程序:

          public static class ImageProviderFactory
          {
              public static IImageProvider GetImageProvider()
              {
                  ImageProviderSection imageProviderSection = ConfigurationManager.GetSection(ImageProviderSection.ImageProviderConfigurationSection) as ImageProviderSection;
                  switch (imageProviderSection.ImageProvider.Name)
                  {
                      case ImageProviderSection.FSImageProviderProperty:
                          return new FSImageProvider();
                      case ImageProviderSection.AzureImageProviderProperty:
                          return new AzureImageProvider;
                      default:
                          throw new Exception("Invalid image provider in configuration");
                  }
              }
          }
          

          并在静态类中保留一个实例(或者您可以使用类似的逻辑设置依赖注入):

          public static class Providers
          {
              public static IImageProvider ImageProvider { get; } = ImageProviderFactory.GetImageProvider();
          }
          

          每个图像提供者类都可以从配置文件中访问它的相关配置信息:

          public interface IImageProvider
          {
              void Save(string filename, byte[] imageData);
              byte[] Get(string filename);
              void Delete(string filename);
          }
          
          public class FSImageProvider : IImageProvider
          {
              private string _directory = string.Empty;
              public FSImageProvider()
              {
                  ImageProviderSection imageProviderSection = ConfigurationManager.GetSection(ImageProviderSection.ImageProviderConfigurationSection) as ImageProviderSection;
                  _directory = imageProviderSection.FSImageProvider.Directory.Trim();
                  if (!_directory.EndsWith("\\"))
                      _directory += "\\";
              }
          
              public void Delete(string filename)
              {
                  File.Delete(_directory + filename);
              }
          
              public byte[] Get(string filename)
              {
                  return File.ReadAllBytes(_directory + filename);
              }
          
              public void Save(string filename, byte[] imageData)
              {
                  File.WriteAllBytes(_directory + filename, imageData);
              }
          }
          
          public class AzureImageProvider : IImageProvider
          {
              private string _azureKey = string.Empty;
              private string _storageAccountName = string.Empty;
              public AzureImageProvider()
              {
                  ImageProviderSection imageProviderSection = ConfigurationManager.GetSection(ImageProviderSection.ImageProviderConfigurationSection) as ImageProviderSection;
                  _azureKey = imageProviderSection.AzureImageProvider.AzureKey;
                  _storageAccountName = imageProviderSection.AzureImageProvider.StorageAccountName;
              }
          
              public void Delete(string filename)
              {
                  throw new NotImplementedException();
              }
          
              public byte[] Get(string filename)
              {
                  throw new NotImplementedException();
              }
          
              public void Save(string filename, byte[] imageData)
              {
                  throw new NotImplementedException();
              }
          }
          

          但是,如果您想根据图像服务中的某些参数使用不同的图像提供程序,只需从配置中删除 imageProvider 元素并更改工厂以提供基于(例如)枚举的 IImageProvider:

          public enum ImageProviders
          {
              FileSystem,
              Azure
          }
          public static class ImageProviderFactory
          {
              public static IImageProvider GetImageProvider(ImageProviders provider)
              {
                  switch (provider)
                  {
                      case ImageProviders.FileSystem:
                          return new FSImageProvider();
                      case ImageProviders.Azure:
                          return new AzureImageProvider;
                      default:
                          throw new Exception("Invalid image provider");
                  }
              }
          }
          

          【讨论】:

            【解决方案7】:

            在 Net.Core 中

            构造函数中的附加信息:

            services.AddSingleton<FSImageProvider>();
            
            services.AddSingleton<AzureBlobImageProvider>(serviceProvider=>
                {
                     return new AzureBlobImageProvider(azureKey,storageAccount);
                }
            );
            

            通过键值进行依赖注入:

            services.AddByName<IImageProvider>()
                            .Add<FSImageProvider>("FSImageProvider")
                            .Add<AzureBlobImageProvider>("AzureBlobImageProvider")
                            .Build();
            

            在构造函数注入中:

            public constructorxxxx(IServiceByNameFactory<IImageProvider> imageProvider)
            {
                 IImageProvider fsImageProvider = imageProvider.GetByName("FSImageProvider");
                 IImageProvider azureBlobImageProvider =  imageProvider.GetByName("AzureBlobImageProvider");
            }
            

            最好使用 Enum 生成每个实现的键值(“FSImageProvider”和“AzureBlobImageProvider”),不要直接使用字符串。

            获取实现代码在:https://github.com/yuriy-nelipovich/DependencyInjection.Extensions

            【讨论】:

            • 在 DI 容器中使用硬编码的字符串/枚举/任何内容作为键在所有可能的方面都是糟糕的设计。我仍然了解它是否会设置一些动态依赖项,例如插件或其他任何东西,但将其用作接口实现的关键?使用接口。它们最初就是为此而创建的。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2021-11-22
            • 1970-01-01
            • 2016-09-09
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-04-16
            相关资源
            最近更新 更多