7

我可以使用 instanceCreator 上下文(aka )注册单个注册项Func<T>,但 RegisterAll 似乎没有相同的限额。

TL;DR - 找到接受的答案并查看更新 2(或跳至此问题的更新 3)

这就是我想要做的:

container.RegisterAll<IFileWatcher>(
    new List<Func<IFileWatcher>>
    {
        () => new FileWatcher(
            @".\Triggers\TriggerWatch\SomeTrigger.txt",
            container.GetInstance<IFileSystem>()),
        () => new FileWatcher(
            @".\Triggers\TriggerWatch\SomeOtherTrigger.txt",
            container.GetInstance<IFileSystem>())
    });

我尝试根据先前的 Stack Overflow 答案为多个注册添加扩展,但似乎最后一个获胜:

public static class SimpleInjectorExtensions
{
    public static void RegisterAll<TService>(this Container container,
        IEnumerable<Func<TService>> instanceCreators) 
        where TService : class
    {           
        foreach (var instanceCreator in instanceCreators)
        {
            container.RegisterSingle(typeof(TService),instanceCreator);
        }

        container.RegisterAll<TService>(typeof (TService));
    }
}

我也很好奇为什么首先需要RegisterAll存在。这是我使用过的 5 个依赖注入容器中的第一个。其他的只是允许您针对服务注册多种类型,然后通过调用Resolve<IEnumerable<TService>>(autofac) 或GetAllInstances<TService>(SimpleInjector 和 Ninject) 将它们全部加载。

更新

为了更清楚起见,我正在尝试构建一个项目列表,我可以将其传递给处理每个单独项目的复合材料。它遇到了与上述相同的问题,因为它属于一组任务,这些任务都被注册为基于调度、触发器和事件 (Rx) 运行。暂时删除所有寄存器并删除其他一些内容:

container.Register<ITask>(() => new FileWatchTask(
    container.GetInstance<IFileSystem>(),
    container.GetInstance<IMessageSubscriptionManagerService>(),
    configuration,
    container.GetAllInstances<IFileWatcher>()));

你可以看到我正在抓取之前注册的文件观察者的所有实例。

我需要知道的是这个问题的简单解决方法以及何时实施(或者如果没有,为什么不会实施)。鉴于 Simple Injector 设计的当前限制,我也将接受这是不可能的。我不能接受的是我需要改变和调整我的架构以满足工具的限制。

更新 2

让我们谈谈 OCP(Open Closed Principle aka the O in SOLID)以及我在 SimpleInjector 在某些情况下如何打破这一特定原则的印象。

开闭原则就是这样,对扩展开放,对修改关闭。这意味着您可以在不更改其源代码的情况下更改实体的行为。

现在让我们转向一个相关的例子:

var tasks = container.GetAllInstances<ITask>();
foreach (var task in tasks.OrEmptyListIfNull())
{
  //registers the task with the scheduler, Rx Event Messaging, or another trigger of some sort  
  task.Initialize();
}

注意那是多么干净。为了能够做到这一点,我需要能够注册接口的所有实例:

container.RegisterAll<ITask>( 
  new List<Func<ITask>>{
    () => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()),
    () => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()),
    () => new DefaultImportFilesTask(container.GetInstance<IFileSystem>())
  }
);

对?所以这里的教训是,这很好并且符合 OCP。我可以通过添加或删除已注册的项目来更改任务运行器的行为。打开扩展,关闭修改。

现在让我们专注于尝试按照以下答案中建议的方式进行操作(在第二次更新之前,最终回答了这个问题),作者给人的印象是更好的设计。

让我们从维护者提到的良好注册设计开始。我得到的观点是我必须牺牲我的代码以某种方式使 ITask 更灵活地使用 SimpleInjector:

container.Register<ITask<SomeGeneric1>(() => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()));
container.Register<ITask<SomeGeneric2>(() => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()));
container.Register<ITask<SomeGeneric3>(() => new DefaultImportFilesTask(container.GetInstance<IFileSystem>()));

现在让我们看看这如何改变我们的设计:

var task1 = container.GetInstances<ITask<SomeGeneric1>();
task1.Initialize();
var task2 = container.GetInstances<ITask<SomeGeneric2>();
task2.Initialize();
var task3 = container.GetInstances<ITask<SomeGeneric3>();
task3.Initialize();

哎哟。您可以看到每次我从容器注册中添加或删除项目时,我现在还需要更新另一段代码。一个更改的两个修改位置,我打破了多个设计问题。

你可能会说我为什么要向容器索要这个?好吧,这是在启动领域,但如果我不是,让我们探索一下。

所以我将使用构造函数注入来说明为什么这是不好的。首先让我们将我的示例视为构造注入。

public class SomeClass {
    public SomeClass(IEnumerable<ITask> tasks){}
}

干净整洁。

现在,让我们切换回我对已接受答案观点的理解(同样在更新 2 之前):

public class SomeClass {
    public SomeClass(ITask<Generic1> task1,
                     ITask<Generic2> task2,
                     ITask<Generic3> task3
                     ) {}
}

哎哟。每次我必须编辑多个代码区域时,我们甚至都不要开始了解这个设计有多糟糕。

这里有什么教训?我不是世界上最聪明的人。我维护(或尝试维护:))多个框架,并且我不会假装我比其他人了解更多或更好。我的设计感可能是有偏差的,或者我可能会以某种我还没有想到的未知方式限制其他人。我敢肯定,作者在提供设计建议时是善意的,但在某些情况下,它可能会让人讨厌(而且有点居高临下),尤其是对于我们这些知道自己在做什么的人来说。

更新 3

因此,维护者在更新 2 中回答了这个问题。我试图使用 RegisterAll,因为我没有想到我可以使用Register<IEnumerable<T>>(不幸的是文档没有指出这一点)。现在看起来很明显,但是当人们从其他 IoC 框架中跳出来时,他们背负着一些包袱,可能会错过设计中的这种令人敬畏的简化!我错过了,我的腰带下还有 4 个其他 DI 容器。希望他将其添加到文档中或更好地调用它。

4

1 回答 1

17

从您的第一个示例(使用List<Func<IFileWatcher>>)中,我了解到您想要注册一个临时文件观察器的集合。换句话说,每次迭代列表时,都应该创建一个新的文件观察程序实例。这当然与使用两个(单例)文件观察器(总是返回相同的实例)注册一个列表非常不同。但是,您的问题有些模棱两可,因为在扩展方法中,您似乎将它们注册为单例。对于我的其余答案,我假设您想要瞬态行为。

创建的常见用例RegisterAll是为一个公共接口注册一个实现列表。例如,具有多个IEventHandler<CustomerMoved>实现的应用程序都需要在引发CustomerMoved事件时触发。在这种情况下,您为RegisterAll方法提供System.Type实例列表,并且容器完全控制为您连接这些实现。由于容器控制着创建,因此集合被称为“容器控制”。

但是,仅将RegisterAll创建转发回容器,这意味着默认情况下列表会导致创建瞬态实例(因为未注册的具体类型被解析为瞬态)。这看起来很尴尬,但它允许您注册一个包含不同生活方式元素的列表,因为您可以使用选择的生活方式明确地注册每个项目。它还允许您提供RegisterAllwith 抽象(例如typeof(IService)),这也可以,因为请求被转发回容器。

但是,您的用例有所不同。你想注册一个完全相同类型的元素列表,但每个元素都有不同的配置值。为了使事情变得更加困难,您似乎希望将它们注册为瞬态而不是单例。通过不传递RegisterAll类型列表,但IEnumerable<TService>容器不创建和自动连接这些类型,我们称其为“容器不受控制”集合。

长话短说:我们如何注册?有多种方法可以做到这一点,但我个人喜欢这种方法:

string[] triggers = new[]
{
    @".\Triggers\TriggerWatch\SomeTrigger.txt",
    @".\Triggers\TriggerWatch\SomeOtherTrigger.txt"
};

container.RegisterAll<IFileWatcher>(
    from trigger in triggers
    select new FileWatcher(trigger,
        container.GetInstance<IFileSystem>())
);

在这里,我们使用该方法注册一个 LINQ 查询(只是一个IEnumerable<T>) 。RegisterAll每次有人解析 aIEnumerable<IFileWatcher>时,它都会返回相同的查询,但由于该查询的 select 包含 a new FileWatcher,因此在迭代时总是返回新实例。使用以下测试可以看到这种效果:

var watchers = container.GetAllInstances<IFileWatcher>();
var first1 = watchers.First();
var first2 = watchers.First();
Assert.AreNotEqual(first1, first2, "Should be different instances");
Assert.AreEqual(first1.Trigger, first2.Trigger);

正如这个测试所示,我们解析了一次集合,但每次迭代它(.First()迭代集合)时,都会创建一个新实例,但两个实例具有相同的@".\Triggers\TriggerWatch\SomeTrigger.txt"值。

如您所见,没有任何限制可以阻止您有效地执行此操作。但是,您可能需要换一种方式思考。

我也很好奇为什么首先需要 RegisterAll 存在。

这是一个非常明确的设计决策。你是对的,大多数其他容器只允许你做一堆相同类型的注册,当被要求收集时,所有的注册都会被返回。这样做的问题是很容易意外地再次注册类型,这是我想要防止的。

此外,所有容器都有不同的行为,即在请求单个实例而不是请求集合时返回注册。有些人返回第一个注册,其他人返回最后一个。我也想防止这种歧义。

最后但同样重要的是,请注意,注册相同类型的项目集合通常应该是一个例外。根据我的经验,在 90% 的情况下,当开发人员想要注册同一抽象的多种类型时,他们的设计会存在一些歧义。通过明确注册集合,我希望能够突出这一点。

我不能接受的是我需要改变和调整我的架构以满足某些工具的限制。

我同意这一点。你的架构应该是领先的,而不是工具。您应该相应地选择您的工具。

但请注意,Simple Injector 有很多限制,其中大部分限制都是故意选择的,以刺激用户进行简洁的设计。例如,每次您在代码中违反SOLID 原则之一时,都会遇到问题。在保持代码灵活性、测试可读性和Composition Root可维护性方面会遇到问题。这实际上适用于所有 DI 容器,但对于 Simple Injector 可能更是如此。这是经过深思熟虑的,如果开发人员对应用 SOLID 原则不感兴趣,并且想要一个在任何给定情况下都可以工作的 DI 容器,那么 Simple Injector 可能不是这项工作的最佳工具。例如,将 Simple Injector 应用于遗留代码库可能会令人生畏。

我希望这能给 Simple Injector 的设计一些观点。

更新

如果你需要单例,这更简单。您可以按如下方式注册它们:

var fs = new RealFileSystem();

container.RegisterSingle<IFileSystem>(fs);

container.RegisterAll<IFileWatcher>(
    new FileWatcher(@".\Triggers\TriggerWatch\SomeTrigger.txt", fs),
    new FileWatcher(@".\Triggers\TriggerWatch\SomeOtherTrigger.txt", fs)
);

更新 2

您明确要求RegisterAll<T>(Func<T>)支持懒惰地创建一个集合。事实上,已经有了对此的支持,只需使用RegisterSingle<IEnumerable<T>>(Func<IEnumerable<T>>),如您在此处看到的:

container.RegisterSingle<IEnumerable<IFileWatcher>>(() =>
{
    return
        from 
    var list = new List<IFileWatcher>
    {
        new FileWatcher(@".\Triggers\TriggerWatch\SomeTrigger.txt", container.GetInstance<IFileSystem>()),
        new FileWatcher(@".\Triggers\TriggerWatch\SomeOtherTrigger.txt", container.GetInstance<IFileSystem>())        
    };

    return list.AsReadOnly();
});

RegisterAll<T>(IEnumerable<T>)实际上,这是一个方便的重载,最终会调用RegisterSingle<IEnumerable<T>>(collection).

请注意,我明确返回了一个只读列表。这是可选的,但它是一种额外的安全机制,可防止集合被任何应用程序代码更改。使用RegisterAll<T>集合时会自动包装在只读迭代器中。

using 的唯一问题RegisterSingle<IEnumerable<T>>是,当您调用container.Verify(). 但是,在您的情况下,这不是问题,因为当集合的元素无法初始化调用时,调用GetInstance<IEnumerable<IFileWatcher>>也会失败,并且调用Verify().

更新 3

如果我给人的印象是我的意思是你的设计是错误的,我深表歉意。我没有办法知道这一点。由于您明确询问了为什么缺少某些功能,因此我已尽力解释其背后的基本原理。但是,这并不意味着我认为您的设计不好,因为我无法知道。

让我们切换回维护者对良好设计的看法

我不知道你为什么认为这是我对好的设计的看法?每次在SomeClass系统中添加任务时都需要更改构造函数,这绝对不是一个好的设计。我们可以放心地同意这一点。这打破了OCP。我永远不会建议任何人做这样的事情。除了具有许多参数的构造函数之外,至少是一种设计气味。Simple Injector 的下一个小版本甚至添加了关于具有太多依赖关系的类型的诊断警告,因为这通常表明 SRP 违规。但是再次看看 Simple Injector 如何通过提供指导来“帮助”开发人员。

不过,我确实提倡使用通用接口,这就是 Simple Injector 设计特别优化的情况。ITask 接口就是一个很好的例子。在这种情况下,ITask<T>通常将是对您希望执行的某些业务行为的抽象,并且T是一个参数对象,其中包含要执行的操作的所有参数(您可以将其视为带有消息处理程序的消息)。然而,这仅在消费者需要使用一组特定参数(特定版本)执行操作时才有用T,例如它想要执行ITask<ShipOrder>。由于您在不提供参数的情况下执行一批所有任务,因此基于此的设计ITask<T>可能会很尴尬。

但是让我们假设它是合适的。让我们假设这一点,所以我可以解释在这种情况下如何优化 Simple Injector。在本次更新结束时,我将向您展示 Simple Injector 如何仍然能够为您提供帮助,所以请屏住呼吸。在您的代码示例中,您按如下方式注册通用任务:

container.Register<ITask<SomeGeneric1>(() => new FileWatchTask(container.GetInstance<IFileSystem>(),container.GetInstance<IMessageSubscriptionManagerService>(),configuration,container.GetAllInstances<IFileWatcher>()));
container.Register<ITask<SomeGeneric2>(() => new DefaultFtpTask(container.GetInstance<IFtpClient>(),container.GetInstance<IFileSystem>()));
container.Register<ITask<SomeGeneric3>(() => new DefaultImportFilesTask(container.GetInstance<IFileSystem>()));

这是在系统中注册所有任务的一种相当痛苦的方式,因为每次更改任务实现的构造函数时,都必须更改此代码。Simple Injector 允许您通过查看它们的构造函数来自动连接类型。换句话说,Simple Injector 允许您将此代码简化为以下内容:

container.Register<ITask<SomeGeneric1>, FileWatchTask>();
container.Register<ITask<SomeGeneric2>, DefaultFtpTask>();
container.Register<ITask<SomeGeneric3>, DefaultImportFilesTask>();

这已经更易于维护,带来更好的性能,并允许您稍后添加其他有趣的场景,例如基于上下文的注入(因为 Simple Injector 控制着整个对象图)。这是在 Simple Injector 中注册事物的建议方法(如果可能,请避免使用 Func)。

尽管如此,当拥有一个以任务为中心元素的架构时,您可能会定期添加新的任务实现。这将导致有几十个注册行,并且每次添加任务时都必须返回此代码添加一行。然而,Simple Injector 具有批量注册功能,可让您将其缩减为一行代码:

// using SimpleInjector.Extensions;
container.RegisterManyForOpenGeneric(typeof(ITask<>), typeof(ITask<>).Assembly);

通过调用此行,容器将搜索ITask<T>位于接口程序集中的所有实现,并为您注册它们。由于这是在运行时使用反射完成的,因此在将新任务添加到系统时不必更改该行。

既然你在谈论 OCP,IMO Simple Injector 对 OCP 有很好的支持。在某些方面,它甚至击败了所有其他框架。当我想到 OCP 时,我特别想到了一种特定的模式:装饰器模式。装饰器模式是应用 OCP 时非常重要的模式。例如,不应通过更改某些业务逻辑本身来添加横切关注点,而最好通过使用装饰器包装类来添加。使用 Simple Injector,只需一行代码即可添加装饰器:

// using SimpleInjector.Extensions;
container.RegisterDecorator(typeof(ITask<>), typeof(TransactionTaskDecorator<>));

这确保了(瞬态)在解决所有实现时TransactionTaskDecorator<T>被包裹起来。ITask<T>这些装饰器集成在容器的管道中,这意味着它们可以拥有自己的依赖项,可以拥有初始化程序,并且可以拥有特定的生活方式。装饰器可以轻松堆叠:

container.RegisterDecorator(typeof(ITask<>), typeof(TransactionTaskDecorator<>));
container.RegisterDecorator(typeof(ITask<>), typeof(DeadlockRetryTaskDecorator<>));

这将所有任务包装在事务装饰器中,并将该事务装饰器再次包装在死锁重试装饰器中。你甚至可以有条件地应用装饰器:

container.RegisterDecorator(typeof(ITask<>), typeof(ValidationTaskDecorator<>),
    context => ShouldApplyValidator(context.ServiceType));

如果您的装饰器具有泛型类型约束,Simple Injector 将在泛型类型约束匹配时自动应用装饰器,您无需对此做任何事情。并且由于 Simple Injector 生成表达式树并将它们编译为委托,这都是一次性成本。这并不意味着它是免费的,但您只需支付一次,而不是每次解决。

没有其他 DI 库可以像 Simple Injector 那样简单灵活地添加装饰器。

所以这就是 Simple Injector 真正闪耀的地方,但这对你没有多大帮助:-)。在这种情况下,通用接口对您没有帮助,但即使在您的情况下,您也可以使您的注册更易于维护。如果您在系统中有许多任务实现(也就是说,远远多于三个),您可能能够自动化这样的事情:

var taskTypes = (
    from type in typeof(ITask).Assemby.GetTypes()
    where typeof(ITask).IsAssignableFrom(type)
    where !type.IsAbstract && !type.IsGenericTypeDefinition
    select type)
    .ToList();

// Register all as task types singleton
taskTypes.ForEach(type => container.Register(type, type, Lifestyle.Singleton));

// registers a list of all those (singleton) tasks.
container.RegisterAll<ITask>(taskTypes);

或者,使用 Simple Injector 2.3 及更高版本,您可以将Registration实例直接传递到RegisterAll方法中:

var taskTypes =
    from type in typeof(ITask).Assemby.GetTypes()
    where typeof(ITask).IsAssignableFrom(type)
    where !type.IsAbstract && !type.IsGenericTypeDefinition
    select type;

// registers a list of all those (singleton) tasks.
container.RegisterAll(typeof(ITask),
    from type in taskTypes
    select Lifestyle.Singleton.CreateRegistration(type, type, container));

但是,这确实假设所有这些任务实现都有一个公共构造函数,并且所有构造函数参数都是可解析的(没有配置值,例如 int 和 string)。如果不是这种情况,有一些方法可以更改框架的默认行为,但是如果您想对此有所了解,最好将该讨论移至新的 SO 问题。

再次,如果我惹恼了你,我很抱歉,但我宁愿惹恼一些开发人员,也不愿错过帮助其他人的机会:-)

于 2013-03-09T11:06:31.187 回答