2015-10-01 28 views
2

键子图这将是一个有点疯狂,但我相信,如果有可能,这将是在手头的任务最易维护的解决方案。解决与级联后备

我们的应用程序使用Autofac进行依赖注入。

我们使用,我们需要能够演进技术(性能/存储空间优化)或域名的原因自定义数据文件格式。该应用程序将永远只能最新格式的版本,但需要能够阅读所有以前的版本了。在各个版本之间进化通常是相当缓慢的,只有少数几个地方有变化,所以很多读取代码的代码将保持不变。

文件格式的版本号被存储为在文件的开头的整数值。读取任何版本的文件格式将始终导致相同的数据结构,这里称为Scenario

,可以从文件中读取数据的一类呈现IReadDataFile依赖性:

public interface IReadDataFile 
{ 
    Scenario From(string fileName); 
} 

背后即用于读取场景的各个部分的非平凡的对象图。然而,所需的图形看起来对于每个文件格式的版本的稍有不同(说明性示例,而不是实际的类型;真正的图是复杂得多):

版本1:

ReadDataFileContents : IReadDataFileContents 
└> ReadCoreData : IReadCoreData 
└> ReadAdditionalData : IReadAdditionalData 
    └> NormalizeName : INormalizeName 

2版:

ReadDataFileContentsV2 : IReadDataFileContents 
└> ReadCoreData : IReadCoreData 
└> ReadAdditionalDataV2 : IReadAdditionalData 
    └> NormalizeNameV2 : INormalizeName 
     └> AdditionalNameRegex : IAdditionalNameRegex 

版本3:

ReadDataFileContentsV2 : IReadDataFileContents 
└> ReadCoreData : IReadCoreData 
└> ReadAdditionalDataV3 : IReadAdditionalData 
    └> NormalizeNameV2 : INormalizeName 
     └> AdditionalNameRegexV3 : IAdditionalNameRegex 

(我只考虑ENTI依赖这样的单独图表;在单个图表中处理这种情况,并且每次切换时版本相关的差异显然非常迅速地变得非常混乱)。

现在无论何时调用IReadDataFile.From()方法来加载文件,都需要获取适当的子图为文件格式版本。实现这一目的的一种简单方法是通过注入工厂:

public class ReadDataFile : IReadDataFile 
{ 
    private readonly IGetDataFileVersion getDataFileVersion; 
    private readonly Func<int, IReadDataFileContents> createReadDataFileContents; 

    public ReadDataFile(
     IGetDataFileVersion getDataFileVersion, 
     Func<int, IReadDataFileContents> createReadDataFileContents) 
    { 
     this.getDataFileVersion = getDataFileVersion; 
     this.createReadDataFileContents = createReadDataFileContents; 
    } 

    public Scenario From(string fileName) 
    { 
     var version = this.getDataFileVersion.From(fileName); 
     var readDataFileContents = this.createReadDataFileContents(version); 
     return readDataFileContents.From(fileName); 
    } 
} 

问题是如何注册和解决这些子图的工作。

手动注册完整的子图为Keyed<T>非常复杂且容易出错,并且对于其他文件格式版本(特别是图形比示例复杂得多)无法很好地扩展。

相反,我想如上所述,看起来像这样对整个事情的注册:

builder.RegisterAssemblyTypes(typeof(IReadDataFile).Assembly).AsImplementedInterfaces(); 

builder.RegisterType<ReadDataFileContents>().As<IReadDataFileContents>(); 
builder.RegisterType<ReadDataFileContentsV2>().Keyed<IReadDataFileContents>(2); 

builder.RegisterType<ReadAdditionalData>().As<IReadAdditionalData>(); 
builder.RegisterType<ReadAdditionalDataV2>().Keyed<IReadAdditionalData>(2); 
builder.RegisterType<ReadAdditionalDataV3>().Keyed<IReadAdditionalData>(3); 

builder.RegisterType<NormalizeName>().As<INormalizeName>(); 
builder.RegisterType<NormalizeNameV2>().Keyed<INormalizeName>(2); 

builder.RegisterType<AdditionalNameRegex>().As<IAdditionalNameRegex>(); 
builder.RegisterType<AdditionalNameRegexV3>().Keyed<IAdditionalNameRegex>(3); 

builder.Register<Func<int, IReadDataFileContents>>(c => 
{ 
    var context = c.Resolve<IComponentContext>(); 

    return version => // magic happens here 
}); 

这意味着只有该图形之间变化的成分明确的注册。并以“奇迹发生在这里”,我的意思是,对于越来越远这个最小的注册,该决议将不得不做繁重。

我希望这样做的方式是这样的:对于要解决的每个组件(在此子图中),它都试图解析注册到所请求的文件格式版本的注册。如果该尝试失败,则为另一个更低的版本进行,等等;当密钥2的分辨率失败时,将解决默认注册。

一个完整的例子:

  • createReadDataFileContents工厂被调用的3一个version值,所以所要求的图形是一个用于以上给出文件格式版本3。
  • 尝试解决IReadDataFileContents与关键3。这是不成功的;没有这样的注册。
  • 现在尝试使用密钥2解决IReadDataFileContents。这成功了。
  • 构造函数需要一个IReadCoreData。试图用密钥3,然后2来解决这个问题;都会失败,所以默认注册已解决,这是成功的。
  • 第二个构造函数参数是IReadAdditionalData;尝试使用成功的密钥3来解决此问题。
  • 构造函数需要INormalizeName;分辨率为3失败,则尝试2成功。
  • 这个构造函数反过来需要IAdditionalNameRegex;关键字3的解析度尝试成功。

这里棘手的事情(和一个我不知道怎样做)是该版本“倒计时”回退过程需要发生的每个个人的依赖得到解决,每次从开始初始值为version

围绕Autofac API和一些Google搜索产生了一些看起来很有趣的事情,但他们都没有提供一个明显的解决方案路径。

  • Module.AttachToComponentRegistration() - 我已经使用这个别处使用registration.Preparing挂钩到解析过程;然而,只有在找到合适的注册时才会发生该事件,并且在此之前似乎没有发生过事件,也没有办法在解决失败(我感到意外)的情况下注册回调。
  • IRegistrationSource - 这似乎是实现这种更一般的注册/解决方案原则的方式,但我无法理解我内部需要做的事情,以防事实上是我所在的地方寻找。
  • WithKeyAttribute - 我们不能在这里使用它,因为我们需要控制从外部注入的依赖项的“版本”(同样,实际的业务代码将依赖于Autofac,这从来都不是好的)。
  • ILifetimeScope.ResolveOperationBeginning - 这看起来很有希望,但事件只是提出了已经成功的决议。
  • IIndex<TKey, TValue> - 另一件看起来非常好的东西,但它包含已经构建的实例,无法获得较低级别分辨率的版本密钥。

解决问题的一方将限制整个事情只是实际上与此相关的类型,但我想这可以做基于惯例(命名空间等),如果需要的话。

另一个可能有用的想法是,在完成所有注册(必须以某种方式确定)之后,“缺口”可以“填满” - 意味着如果注册键与3键但没有键2,将会添加一个等于默认注册的值。这将允许用相同的关键字解决子图中的所有所有依赖关系,并取消对可能是整个事情中最困难部分的“级联后备”机制的需要。

Autofac有什么办法可以实现吗?

(另外,感谢首先阅读这部史诗!)

+0

你有没有想过使用特定于版本的标记接口? '公共类ReadDataFileContentsV2:IReadDataFileContents,IReadDataFileContentsV2'种事情? –

+1

不具体,不。你有什么样的使用方法?在其他类中依赖它们会产生与使用'WithKeyAttribute'类似的问题 - 类本身永远无法决定它需要哪个版本的依赖关系。例如,请参阅上面的'NormalizeNameV2' - 读取版本2和3的文件是一样的,但'IAdditionalNameRegex'的必需实现不同。 – TeaDrivenDev

+1

顺便说一句,_excellent_写起来。我知道记录这类事情的复杂性需要永远的时间,并且其他问答者和答复者都会对此表示赞赏。我希望更多的人会花费这样的时间和精力。 –

回答

2

开箱,Autofac并没有真正有这种水平的控制。但是如果你不介意在中间加一个工厂,那么你可以建立它。

首先,让我发布一个工作的C#文档,然后我会解释它。你应该可以将它粘贴到一个.csx scriptcs文档中,然后看看它 - 这就是我写的地方。

using Autofac; 
using System.Linq; 

// Simple interface just used to prove out the 
// dependency chain that gets resolved. 
public interface IDependencyChain 
{ 
    IEnumerable<Type> DependencyChain { get; } 
} 

// File reading interfaces 
public interface IReadDataFileContents : IDependencyChain { } 
public interface IReadCoreData : IDependencyChain { } 
public interface IReadAdditionalData : IDependencyChain { } 
public interface INormalizeName : IDependencyChain { } 
public interface IAdditionalNameRegex : IDependencyChain { } 

// File reading implementations 
public class ReadDataFileContents : IReadDataFileContents 
{ 
    private readonly IReadCoreData _coreReader; 
    private readonly IReadAdditionalData _additionalReader; 
    public ReadDataFileContents(IReadCoreData coreReader, IReadAdditionalData additionalReader) 
    { 
    this._coreReader = coreReader; 
    this._additionalReader = additionalReader; 
    } 

    public IEnumerable<Type> DependencyChain 
    { 
    get 
    { 
     yield return this.GetType(); 
     foreach(var t in this._coreReader.DependencyChain) 
     { 
     yield return t; 
     } 
     foreach(var t in this._additionalReader.DependencyChain) 
     { 
     yield return t; 
     } 
    } 
    } 
} 

public class ReadDataFileContentsV2 : ReadDataFileContents 
{ 
    public ReadDataFileContentsV2(IReadCoreData coreReader, IReadAdditionalData additionalReader) 
    : base(coreReader, additionalReader) 
    { 
    } 
} 

public class ReadCoreData : IReadCoreData 
{ 
    public IEnumerable<Type> DependencyChain 
    { 
    get 
    { 
     yield return this.GetType(); 
    } 
    } 
} 

public class ReadAdditionalData : IReadAdditionalData 
{ 
    private readonly INormalizeName _normalizer; 
    public ReadAdditionalData(INormalizeName normalizer) 
    { 
    this._normalizer = normalizer; 
    } 

    public IEnumerable<Type> DependencyChain 
    { 
    get 
    { 
     yield return this.GetType(); 
     foreach(var t in this._normalizer.DependencyChain) 
     { 
     yield return t; 
     } 
    } 
    } 
} 

public class ReadAdditionalDataV2 : ReadAdditionalData 
{ 
    public ReadAdditionalDataV2(INormalizeName normalizer) 
    : base(normalizer) 
    { 
    } 
} 

public class ReadAdditionalDataV3 : ReadAdditionalDataV2 
{ 
    public ReadAdditionalDataV3(INormalizeName normalizer) 
    : base(normalizer) 
    { 
    } 
} 

public class NormalizeName : INormalizeName 
{ 
    public IEnumerable<Type> DependencyChain 
    { 
    get 
    { 
     yield return this.GetType(); 
    } 
    } 
} 

public class NormalizeNameV2 : INormalizeName 
{ 
    public readonly IAdditionalNameRegex _nameRegex; 
    public NormalizeNameV2(IAdditionalNameRegex nameRegex) 
    { 
    this._nameRegex = nameRegex; 
    } 

    public IEnumerable<Type> DependencyChain 
    { 
    get 
    { 
     yield return this.GetType(); 
     foreach(var t in this._nameRegex.DependencyChain) 
     { 
     yield return t; 
     } 
    } 
    } 
} 

public class AdditionalNameRegex : IAdditionalNameRegex 
{ 
    public IEnumerable<Type> DependencyChain 
    { 
    get 
    { 
     yield return this.GetType(); 
    } 
    } 
} 

public class AdditionalNameRegexV3 : AdditionalNameRegex { } 

// File definition modules - each one registers just the overrides needed 
// for the upgraded version of the file type. ModuleV1 registers the base 
// stuff that will be used if things aren't overridden. If any version 
// of a file format needs to "revert back" to an old mechanism, like if 
// V2 needs NormalizeNameV2 and V3 needs NormalizeName, you'd have to re-register 
// the base NormalizeName in the V3 module - override the override. 
public class ModuleV1 : Module 
{ 
    protected override void Load(ContainerBuilder builder) 
    { 
    builder.RegisterType<ReadDataFileContents>().As<IReadDataFileContents>(); 
    builder.RegisterType<ReadCoreData>().As<IReadCoreData>(); 
    builder.RegisterType<ReadAdditionalData>().As<IReadAdditionalData>(); 
    builder.RegisterType<NormalizeName>().As<INormalizeName>(); 
    } 
} 

public class ModuleV2 : Module 
{ 
    protected override void Load(ContainerBuilder builder) 
    { 
    builder.RegisterType<ReadDataFileContentsV2>().As<IReadDataFileContents>(); 
    builder.RegisterType<ReadAdditionalDataV2>().As<IReadAdditionalData>(); 
    builder.RegisterType<NormalizeNameV2>().As<INormalizeName>(); 
    builder.RegisterType<AdditionalNameRegex>().As<IAdditionalNameRegex>(); 
    } 
} 

public class ModuleV3 : Module 
{ 
    protected override void Load(ContainerBuilder builder) 
    { 
    builder.RegisterType<ReadAdditionalDataV3>().As<IReadAdditionalData>(); 
    builder.RegisterType<AdditionalNameRegexV3>().As<IAdditionalNameRegex>(); 
    } 
} 

// Something has to know about how file formats are put together - a 
// factory of some sort. Here's the thing that "knows." You could probably 
// drive this from config or something else, too, but the idea holds. 
public class FileReaderFactory 
{ 
    private readonly ILifetimeScope _scope; 
    public FileReaderFactory(ILifetimeScope scope) 
    { 
    // You can always resolve the current lifetime scope as a parameter. 
    this._scope = scope; 
    } 

    public IReadDataFileContents CreateReader(int version) 
    { 
    using(var readerScope = this._scope.BeginLifetimeScope(b => RegisterFileFormat(b, version))) 
    { 
     return readerScope.Resolve<IReadDataFileContents>(); 
    } 
    } 

    private static void RegisterFileFormat(ContainerBuilder builder, int version) 
    { 
    switch(version) 
    { 
     case 1: 
     builder.RegisterModule<ModuleV1>(); 
     break; 
     case 2: 
     builder.RegisterModule<ModuleV1>(); 
     builder.RegisterModule<ModuleV2>(); 
     break; 
     case 3: 
     default: 
     builder.RegisterModule<ModuleV1>(); 
     builder.RegisterModule<ModuleV2>(); 
     builder.RegisterModule<ModuleV3>(); 
     break; 
    } 
    } 
} 


// Only register the factory and other common dependencies - not the file 
// format readers. The factory will be responsible for managing the readers. 
// Note that since readers do resolve from a child of the current lifetime 
// scope, they can use common dependencies that you'd register in the 
// container. 
var builder = new ContainerBuilder(); 
builder.RegisterType<FileReaderFactory>(); 
var container = builder.Build(); 
using(var scope = container.BeginLifetimeScope()) 
{ 
    var factory = scope.Resolve<FileReaderFactory>(); 

    for(int i = 1; i <=3; i++) 
    { 
    Console.WriteLine("Version {0}:", i); 
    var reader = factory.CreateReader(i); 
    foreach(var t in reader.DependencyChain) 
    { 
     Console.WriteLine("* {0}", t); 
    } 
    } 
} 

如果您运行此,控制台输出产生在你想要的结果概述文件读取依赖正确的树:

Version 1: 
* Submission#0+ReadDataFileContents 
* Submission#0+ReadCoreData 
* Submission#0+ReadAdditionalData 
* Submission#0+NormalizeName 
Version 2: 
* Submission#0+ReadDataFileContentsV2 
* Submission#0+ReadCoreData 
* Submission#0+ReadAdditionalDataV2 
* Submission#0+NormalizeNameV2 
* Submission#0+AdditionalNameRegex 
Version 3: 
* Submission#0+ReadDataFileContentsV2 
* Submission#0+ReadCoreData 
* Submission#0+ReadAdditionalDataV3 
* Submission#0+NormalizeNameV2 
* Submission#0+AdditionalNameRegexV3 

这里的想法:

,而不是使用键控服务或试图解决主容器外的事情,请使用子生命周期范围来隔离一组文件版本特定的依赖关系。

我在代码中有一系列Autofac模块,每个文件格式一个。在这个例子中,这些模块建立在彼此之上 - 文件格式V1需要模块V1;文件格式V2需要模块V1和模块V2覆盖;文件格式V3需要模块V1,模块V2覆盖和模块V3覆盖。

在现实生活中,你可以使这些都是独立的,但如果每个版本都是最后建立的,这可能更容易维护 - 每个新版本/模块只需要差异。

然后,我有一个中间工厂类,您可以使用它来获取相应的文件版本阅读器。工厂知道如何将文件格式版本与适当的模块组相关联。在更复杂的情况下,您可能会将此配置或属性或某种东西驱动出去,但以这种方式进行说明更容易。

当你想要一个特定的文件格式阅读器时,你可以解析工厂并请求阅读器。工厂采用当前的生命周期范围并产生一个子范围,为该文件格式注册适当的模块,并解析读者。通过这种方式,您可以更自然地使用Autofac,只需让类型排列起来而不是与元数据或其他机制作斗争。

当心IDisposable依赖关系。如果你走这条路线,任何你的文件读取依赖关系都是一次性的,你需要将它们注册为Owned或其他东西,这样工厂内的小孩子生存期范围不会实例化,然后立即处理你要去的东西需要。

看起来很奇怪,只能激发一个小小的寿命范围,但这也是如何工作的InstancePerOwned。幕后有先例。

噢,如果你真的想注册Func<int, IReadDataFileContents>方法,你可以让它解决工厂问题,并在那里调用CreateReader方法。

希望这可以疏通你,或者给你一个你可以接受的地方的想法。我不确定Autofac有哪些标准的开箱即用机制可以更自然地处理它,但这似乎解决了这个问题。

+1

谢谢,特拉维斯,帮助很多。我有一些想法朝着这个方向发展,但是我对于终身范围的了解太少,无法想出这个问题(特别是我不知道是否有可能为他们增加更多的注册)。总的来说,这是一个比我想的更加理智的解决方案,因为它不需要搞实际的解决过程。至于IDisposable的依赖关系,我应该很好地使用'ExternallyOwned()'并自己处理它们,对吧?至于'Func',工厂完全取代了这个;它更多是一种捷径。 – TeaDrivenDev

+0

是的,'ExternallyOwned'应该为您解决任何'IDisposable'问题。只是想确保你没有任何意外的处置,因为工厂里只有活动范围。 –