IoC、SRP 和组合——我是否创建了太多接口?

IoC, SRP and composition - am I creating too many interfaces?

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

每个托管商(GithubHosterBitbucketHoster 等)将有 class 实现相同的接口。

我希望该工具是可扩展的,因此只需创建一些由 IoC 自动注册自动选取的 classes 即可轻松添加更多主机。

对于每个托管商,该工具必须:

这显然太多了,无法放入一个单一的class,所以我使用组合(或者我认为组合的意思)将它分成子部分:

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

interface IConfigSourceValidator
{
    Validate();
}

interface IHosterApi
{
    GetRepositoryList();
}

interface IBackupMaker
{
    Backup();
}

问题:为了将子classes注入到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; }
}

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

当然我需要实现子classes:

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

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

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

我有 "basic" 个接口:

IHoster
IConfigSourceValidator
IHosterApi
IBackupMaker

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

IGithubConfigSourceValidator
IGithubApi
IGithubBackupMaker
GithubHoster
GithubConfigSourceValidator
GithubApi
GithubBackupMaker

以后每次添加新的主机时,我都必须重新创建这一切:

IBitbucketConfigSourceValidator
IBitbucketApi
IBitbucketBackupMaker
BitbucketHoster
BitbucketConfigSourceValidator
BitbucketApi
BitbucketBackupMaker

我这样做对吗?

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

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

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


@NightOwl888 请求的其他信息:

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

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

所以我需要一个工厂,它从配置文件中获取字符串 githubbitbucket,以及 returns 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);
    }
}

注:如果想看真实代码,我的项目是public on GitHub and 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);
}

为了给予适当的信任,工厂是用 of help from Steven, the creator of Simple Injector 创建的。

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

定义这个属性

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

希望对您有所帮助

因为现在您还没有指定 IoC 容器,我想说它们中的大多数应该让您实现自己的 auto-registration 约定。

查看您的 托管人 class:

class GitHubHoster : IHoster
{
    // Implemented members
}

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

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

例如,Castle Windsor would do it as follows:

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

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

am I creating too many interfaces?

是的,您创建了太多接口。您可以通过使接口通用化来大大简化这一过程,因此所有服务都是 "keyed" 特定实体的。这类似于通常为存储库模式所做的事情。

接口

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<THoster> 实现

那么您的 IHoster<THoster> 实现将如下所示:

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>>();

NOTE: If you want your application to be able to switch between IHoster implementations at runtime, consider passing a to the constructor of the service that uses it, or use the .

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

我把它贴在这里供其他人评论和upvote/downvote,因为我不确定它是否是一个好的解决方案。

我将摆脱标记界面:

class GithubBackupMaker : IBackupMaker { ... }

class GithubHosterApi : IHosterApi { ... }

class GithubConfigSourceValidator : IConfigSourceValidator { ... }

...我会将 Github... classes 直接注入 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 中注册 sub-classes,因为它可以解析 classes,即使它们之前没有注册过。

这意味着我无法测试 GithubHoster 和模拟子 classes,虽然...但这没关系,因为我不测试 GithubHoster,因为它只是一个空的 shell,它包含子 classes.


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

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


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

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

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

首先,GithubHoster 及其子class 确实有接口:

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

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

So you can provide improvements without having to force anyone to use the newest dependency versions.

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

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

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

...进入这个:

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

不管我怎么做,我知道

  • 那些 sub-classes 除了在 GithubHoster
  • GithubHoster 将始终使用那些 sub-classes 而不会使用其他

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

表达它们属于彼此这一事实的任何其他方式(依赖于所有 class 以相同字母开头的名称的标记接口/属性/自定义注册约定) 对我来说更像是仪式,只是为了 "thou shalt use abstractions everywhere" :-)

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