IoC,SRP和组合 - 我创建了太多接口吗?

时间:2017-02-10 00:08:09

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

我为多个源代码托管商编写了一个离线备份工具(在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);
}

为了给予适当的信任,工厂是使用a lot的创建者of help Steven Simple Injector创建的。

4 个答案:

答案 0 :(得分:2)

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

定义此属性

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

希望这有帮助

答案 1 :(得分:2)

因为目前你还没有指定IoC容器,我想说大多数都应该让你实现自己的自动注册约定。

查看 hoster 类:

class GitHubHoster : IHoster
{
    // Implemented members
}

我看到类标识符上有一个可变部分,即提供程序。例如,GitHub,对吗?我希望其余的接口标识符遵循相同的约定。

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

例如,Castle Windsor would do it as follows

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

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

答案 2 :(得分:2)

  

我创建的界面太多了吗?

是的,您创建的接口太多了。您可以通过使接口通用来大大简化这一过程,因此所有服务都是&#34;键控的&#34;对于特定的实体。这类似于通常对存储库模式所做的操作。

接口

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

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

答案 3 :(得分:0)

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

我在这里发帖给别人评论和upvote / downvote,因为我不确定这是否是一个很好的解决方案。

我将摆脱标记界面:

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,因为它只是一个包含子类的空shell。

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

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

作为@MatíasFidemraizer的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将它们分成多个类。

表达彼此属于的事实的任何其他方式(标记接口/属性/自定义注册约定,取决于以相同字母开头的所有类名)似乎更像是我,只是为了“你到处都使用抽象”: - )

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