【问题标题】:IoC, SRP and composition - am I creating too many interfaces?IoC、SRP 和组合——我是否创建了太多接口?
【发布时间】:2017-06-28 04:56:32
【问题描述】:

我正在为多个源代码托管商编写一个离线备份工具(在 C#/.NET Core 中,如果这很重要),例如GitHub 和 Bitbucket。

每个宿主(GithubHosterBitbucketHoster 等)都会有一个实现相同接口的类。

我希望该工具具有可扩展性,因此只需创建一些由 IoC 自动注册自动获取的类,就可以轻松添加更多主机。

对于每个主机,该工具必须:

  • 在工具的配置文件中验证该主机的设置
  • 连接到托管商的 API 并获取存储库 URL 列表
  • 执行适当的源代码控制工具将所有存储库克隆/拉取到本地计算机

这显然太多了,不能放在一个单独的类中,所以我使用组合(或者我认为组合的意思)将它分成子部分:

interface IHoster
{
    IConfigSourceValidator Validator { get; }
    IHosterApi Api { get; }
    IBackupMaker BackupMaker { get; }
}

interface IConfigSourceValidator
{
    Validate();
}

interface IHosterApi
{
    GetRepositoryList();
}

interface IBackupMaker
{
    Backup();
}

问题:为了将子类注入到IHoster实现中,我不能直接使用上面显示的子接口,因为这样容器就不知道要注入哪个实现了。

所以我需要创建一些更多的空标记界面,特别是为此目的:

interface IGithubConfigSourceValidator : IConfigSourceValidator
{
}

interface IGithubHosterApi : IHosterApi
{
}

interface IGithubBackupMaker : IBackupMaker
{
}

...所以我可以这样做:

class GithubHoster : IHoster
{
    public GithubHoster(IGithubConfigSourceValidator validator, IGithubHosterApi api, IGithubBackupMaker backupMaker)
    {
        this.Validator = validator;
        this.Api = api;
        this.BackupMaker = backupMaker;
    }

    public IConfigSourceValidator Validator { get; private set; }
    public IHosterApi Api { get; private set; }
    public IBackupMaker BackupMaker { get; private set; }
}

...容器知道要使用哪些实现。

当然我需要实现子类:

class GithubConfigSourceValidator : IGithubConfigSourceValidator
{
    public void Validate()
    {
        // do stuff
    }
}

// ...and the same for GithubHosterApi and GithubBackupMaker

到目前为止这工作,但不知何故感觉不对。

我有“基本”接口:

IHoster
IConfigSourceValidator
IHosterApi
IBackupMaker

..以及 GitHub 的所有类和接口:

IGithubConfigSourceValidator
IGithubApi
IGithubBackupMaker
GithubHoster
GithubConfigSourceValidator
GithubApi
GithubBackupMaker

以后每次添加新的托管商时,我都必须重新创建所有这些:

IBitbucketConfigSourceValidator
IBitbucketApi
IBitbucketBackupMaker
BitbucketHoster
BitbucketConfigSourceValidator
BitbucketApi
BitbucketBackupMaker

我这样做对吗?

我知道我需要所有的类,因为我使用的是组合而不是将所有东西都放在一个神类中(而且它们更容易测试,因为它们每个只做一件事)。

但我不喜欢必须为每个 IHoster 实现创建的额外接口。

也许将来将我的托管程序分成更多的子类是有意义的,然后我将在每个实现中拥有四五个这样的接口。
...这意味着在实现了对一些额外主机的支持之后,我最终会得到 30 个或更多的空接口。


@NightOwl888 要求的其他信息:

我的应用程序支持从多个源代码托管方进行备份,因此它可以在运行时使用多个 IHoster 实现。

您可以将多个主机的用户名等放入配置文件中,如"hoster": "github""hoster": "bitbucket" 等。

所以我需要一个工厂,它从配置文件中获取字符串githubbitbucket,并返回GithubHosterBitbucketHoster 实例。

我希望IHoster 实现提供自己的字符串值,这样我就可以轻松地自动注册它们。

因此,GithubHoster 有一个带有字符串github 的属性:

[Hoster(Name = "github")]
internal class GithubHoster : IHoster
{
    // ...
}

这里是工厂:

internal class HosterFactory : Dictionary<string, Type>, IHosterFactory
{
    private readonly Container container;

    public HosterFactory(Container container)
    {
        this.container = container;
    }

    public void Register(Type type)
    {
        if (!typeof(IHoster).IsAssignableFrom(type))
        {
            throw new InvalidOperationException("...");
        }

        var attribute = type.GetTypeInfo().GetCustomAttribute<HosterAttribute>();
        this.container.Register(type);
        this.Add(attribute.Name, type);
    }

    public IHoster Create(string hosterName)
    {
        Type type;
        if (!this.TryGetValue(hosterName, out type))
        {
            throw new InvalidOperationException("...");
        }
        return (IHoster)this.container.GetInstance(type);
    }
}

注意:如果你想看真正的代码,我的项目在 GitHub 上是公开的,the real factory is here

在启动时,我从 Simple Injector 和 register each one in the factory 获得所有 IHoster 实现:

var hosterFactory = new HosterFactory(container);
var hosters = container.GetTypesToRegister(typeof(IHoster), thisAssembly);
foreach (var hoster in hosters)
{
    hosterFactory.Register(hoster);
}

为了给予应有的荣誉,工厂是用StevenStevena lot of help 创建的,Simple Injector 的创建者。

【问题讨论】:

标签: c# oop dependency-injection composition single-responsibility-principle


【解决方案1】:

您可以使用属性来帮助您确定哪个实现来自哪里。这将避免您必须创建许多新接口。

定义这个属性

public class HosterAttribute : Attribute
{
    public Type Type { get; set; }

    public HosterAttribute(Type type)
    {
        Type = type;
    }
}

为 IGithubHoster 创建其他类并像这样添加 HosterAttribute

[HosterAttribute(typeof(IGithubHoster))]
class GithubBackupMaker : IBackupMaker
{
    public void Backup()
    {
        throw new NotImplementedException();
    }
}
[HosterAttribute(typeof(IGithubHoster))]
class GithubHosterApi : IHosterApi
{
    public IEnumerable<object> GetRepositoryList()
    {
        throw new NotImplementedException();
    }
}

[HosterAttribute(typeof(IGithubHoster))]
class GithubConfigSourceValidator : IConfigSourceValidator
{
    public void Validate()
    {
        throw new NotImplementedException();
    }
}

我没有使用您正在使用的 IoC,因此您可能需要修改以下方法以满足您的需求。 创建一个为您查找相关 IHoster 实现的方法。这是一种方法。

public T GetHoster<T>() where T: IHoster
{
    var validator = Ioc.ResolveAll<IConfigSourceValidator>().Where(x => x.GetType().GetCustomAttribute<HosterAttribute>().Type == typeof(T)).Single();
    var hosterApi = Ioc.ResolveAll<IHosterApi>().Where(x => x.GetType().GetCustomAttribute<HosterAttribute>().Type == typeof(T)).Single();
    var backupMaker = Ioc.ResolveAll<IBackupMaker>().Where(x => x.GetType().GetCustomAttribute<HosterAttribute>().Type == typeof(T)).Single();

    var hoster = Ioc.Resolve<T>(validator, hosterApi, backupMaker);
    return hoster;
}

现在,在您的程序代码中,您所要做的就是

var gitHub = GetHoster<IGithubHoster>();
//Do some work

希望对你有帮助

【讨论】:

  • 有些子类本身有依赖关系,所以我需要注入它们。
  • 没问题。修改了我的答案
【解决方案2】:

由于目前您还没有指定 IoC 容器,我想说它们中的大多数应该让您实现自己的自动注册约定。

查看您的主持人课程:

class GitHubHoster : IHoster
{
    // Implemented members
}

我看到类标识符上有一个可变部分,即 provider。比如GitHub,对吧?我希望其余接口的标识符应该遵循相同的约定。

基于提供者前缀,您可以协助您的 IoC 容器创建显式依赖关系。

例如Castle Windsor would do it as follows:

Component.For<IHoster>()
         .ImplementedBy<GitHubHoster>()
         .DependsOn(Dependency.OnComponent(typeof(IHosterApi), $"{provider}HosterApi"))

现在就是使用一些反射来找出接口的实现,并根据我上面提供的提示将它们全部注册!

【讨论】:

  • 我正在使用Simple Injector,我不确定它是否支持您所描述的注册类型。我将不得不再次阅读文档(也许自己做一些反思)。但是,我希望能够通过自动注册来注册这些类,因此容器会自动选择新的实现。
  • @ChristianSpecht 在我的项目中,我一直在大量基于反射创建自定义约定,并且一切都是自动完成的。为此,您不需要 Simple Injector 中的特定功能。当然,在实现约定优于配置方面有一些初步的努力,但与整个项目相比,你会因为更少的努力而获得高回报!
  • @也许值得考虑切换到 Windsor、Unity 或任何其他成熟的 IoC 容器。如果你还没有最终实现service locator anti-pattern,我相信重构它应该很容易。
  • @Matías Fidemraizer:我使用的是正确的 DI,而不是服务位置...但我认为 SimpleInjector 一个成熟的容器。
【解决方案3】:

我是否创建了太多接口?

是的,您正在创建太多接口。您可以通过使接口通用化来大大简化这一点,因此所有服务都是针对特定实体的“键控”。这类似于通常为存储库模式所做的。

接口

public interface IHoster<THoster>
{
    IConfigSourceValidator<THoster> Validator { get; }
    IHosterApi<THoster> Api { get; }
    IBackupMaker<THoster> BackupMaker { get; }
}

public interface IConfigSourceValidator<THoster>
{
    void Validate();
}

public interface IHosterApi<THoster>
{
    IList<string> GetRepositoryList();
}

public interface IBackupMaker<THoster>
{
    void Backup();
}

IHoster&lt;THoster&gt; 实现

那么您的 IHoster&lt;THoster&gt; 实现将如下所示:

public class GithubHoster : IHoster<GithubHoster>
{
    public GithubHoster(
        IConfigSourceValidator<GithubHoster> validator, 
        IHosterApi<GithubHoster> api, 
        IBackupMaker<GithubHoster> backupMaker)
    {
        if (validator == null)
            throw new ArgumentNullException("validator");
        if (api == null)
            throw new ArgumentNullException("api");
        if (backupMaker == null)
            throw new ArgumentNullException("backupMaker");

        this.Validator = validator;
        this.Api = api;
        this.BackupMaker = backupMaker;
    }

    public IConfigSourceValidator<GithubHoster> Validator { get; private set; }
    public IHosterApi<GithubHoster> Api { get; private set; }
    public IBackupMaker<GithubHoster> BackupMaker { get; private set; }
}

public class BitbucketHoster : IHoster<BitbucketHoster>
{
    public BitbucketHoster(
        IConfigSourceValidator<BitbucketHoster> validator,
        IHosterApi<BitbucketHoster> api,
        IBackupMaker<BitbucketHoster> backupMaker)
    {
        if (validator == null)
            throw new ArgumentNullException("validator");
        if (api == null)
            throw new ArgumentNullException("api");
        if (backupMaker == null)
            throw new ArgumentNullException("backupMaker");

        this.Validator = validator;
        this.Api = api;
        this.BackupMaker = backupMaker;
    }

    public IConfigSourceValidator<BitbucketHoster> Validator { get; private set; }
    public IHosterApi<BitbucketHoster> Api { get; private set; }
    public IBackupMaker<BitbucketHoster> BackupMaker { get; private set; }
}

您的其他类也只需键入GithubHosterBitbucketHoster 与上述相同的具体服务(即使接口实际上不需要泛型)。

简单的喷油器配置

完成后,现在 DI 配置很简单:

// Compose DI
var container = new Container();

IEnumerable<Assembly> assemblies = new[] { typeof(Program).Assembly };

container.Register(typeof(IHoster<>), assemblies);
container.Register(typeof(IConfigSourceValidator<>), assemblies);
container.Register(typeof(IHosterApi<>), assemblies);
container.Register(typeof(IBackupMaker<>), assemblies);


var github = container.GetInstance<IHoster<GithubHoster>>();
var bitbucket = container.GetInstance<IHoster<BitbucketHoster>>();

注意:如果您希望您的应用程序能够在运行时在IHoster 实现之间切换,请考虑将delegate factory 传递给使用它的服务的构造函数,或使用strategy pattern

【讨论】:

  • 认为这对我不起作用。我已经有一个工厂(因为我需要根据配置文件中的字符串值创建多个 IHoster 实例)并且由于通用接口,我不得不将其从 IHoster Create(string hosterName) 更改为 IHoster&lt;T&gt; Create&lt;T&gt;(string hosterName)
  • 因此,我在使用它的地方得到了compiler error CS0411 (var hoster = this.factory.Create(config.Hoster);)。显然,如果不在编译时指定我想要的主机类型,就无法做到这一点,这违背了工厂的目的。
  • 您的问题并不清楚您是否想在运行时交换实现。我建议您在有关如何实施和使用工厂的问题中添加其他详细信息,因为它们对于提供建设性的答案很重要。
【解决方案4】:

好的,我刚刚为我自己的问题想出了另一个可能的解决方案。

我将它发布在这里供其他人评论和赞成/反对,因为我不确定这是否是一个好的解决方案。

我将摆脱标记接口:

class GithubBackupMaker : IBackupMaker { ... }

class GithubHosterApi : IHosterApi { ... }

class GithubConfigSourceValidator : IConfigSourceValidator { ... }

...我会将Github... 类直接注入GithubHoster

class GithubHoster : IHoster
{
    public GithubHoster(GithubConfigSourceValidator validator, GithubHosterApi api, GithubBackupMaker backupMaker)
    {
        this.Validator = validator;
        this.Api = api;
        this.BackupMaker = backupMaker;
    }

    public IConfigSourceValidator Validator { get; private set; }
    public IHosterApi Api { get; private set; }
    public IBackupMaker BackupMaker { get; private set; }
}

我什至不需要在 Simple Injector 中注册子类,因为即使之前没有注册过,它也会解析类。

这意味着我不能测试 GithubHoster 并模拟子类,虽然......但这没关系,因为我根本不测试 GithubHoster,因为它只是一个空壳包含子类。


我不确定这是否是一个好的解决方案,因为互联网上的所有依赖注入示例总是注入接口......你看不到很多有人注入的例子

但我认为在这种特殊情况下使用类而不是接口是合理的。
另外,在我写完这个答案后,I found someone who says it's a good solution for cases like this


作为对@Matías Fidemraizer 的comment 的回答:

我理解为什么你应该使用抽象而不是实现的论点一般

但我的问题是:在这种特殊情况下使用抽象能得到什么?
在这种情况下,我想我不明白你的回答:

首先,GithubHoster 及其所有子类都有接口:

class GithubHoster : IHoster
class GithubBackupMaker : IBackupMaker
class GithubHosterApi : IHosterApi
class GithubConfigSourceValidator : IConfigSourceValidator

我仍然可以毫不费力地切换到另一个GithubHoster 实现,因为我的工厂返回一个IHoster,而所有其他地方都只依赖于IHoster

因此您可以提供改进,而不必强迫任何人使用最新的依赖版本。

我正在编写一个可执行的应用程序,而不是一个库。
因此,如果我更改依赖项,我这样做是因为我希望我的应用程序使用较新的版本。所以我不希望我的应用程序中的任何地方使用最新版本的依赖项。

所以,我的想法是改变这一点:

public GithubHoster(IGithubConfigSourceValidator validator, 
                    IGithubHosterApi api,
                    IGithubBackupMaker backupMaker) { }

...进入这个:

public GithubHoster(GithubConfigSourceValidator validator, 
                    GithubHosterApi api, 
                    GithubBackupMaker backupMaker) { }

不管我怎么做,我知道

  • 那些子类只会在GithubHoster中使用
  • GithubHoster 将始终使用这些子类而不使用其他子类

所以它们基本上是紧密耦合的,因为 SRP,我只是将它们分成多个类。

表示它们属于彼此这一事实的任何其他方式(标记接口/属性/自定义注册约定依赖于以相同字母开头的所有类名)看起来更像是仪式我,只是为了“你应该到处使用抽象”:-)

也许我真的错过了什么,但现在我不明白它是什么。

【讨论】:

  • 这很难看,您使用的是实现而不是抽象。不要偷懒并实现通过一些反射自动注册所有内容所需的最少代码:D
  • 但是在这种情况下我为什么要使用抽象呢?我同意你的观点,通常使用抽象会更好,但在这种特殊情况下,我看不出 为什么 我需要抽象,因为正如我在回答中已经写的那样 - 我永远不会写测试IHoster 实现,因为它们基本上是空的。
  • TDD 和生成可测试代码的无可辩驳的要求扭曲了创建良好软件架构这一更重要的要求。从理论上讲,您甚至错过了注入 GitHub 托管基础设施的其他实现的机会。依赖抽象而不是实现的另一个关键点是,您可以注入某些给定依赖项的改进版本。因此,您可以提供改进,而不必强迫任何人使用最新的依赖版本。这种方法还有更多优势。
  • @MatíasFidemraizer:我将我对您的评论的回答放在我的回答中,因为评论的文字太多。
  • 作为对您的回答:D 我相信,如果您确定您的方法适用于您的场景,那就去做吧。
猜你喜欢
  • 2019-06-03
  • 1970-01-01
  • 1970-01-01
  • 2012-02-15
  • 2011-07-31
  • 2014-07-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多