如何编写库而不强制用户使用库的IOC容器?

8
简短的问题是: 给定一个使用特定IOC容器作为其内部机制的库warrants,当应用程序使用一个IOC容器来连接其依赖项时,如果两个容器不同,它们如何协调工作?
情景是这样的,应用程序定义了依赖于库中类型的类。因此,当应用程序容器尝试构建这样的类时,它需要知道如何解析存在于库中的类型。
以下是冗长的问题:
这个问题似乎以不同的形式在SO上被问过,但我似乎找不到我需要的答案,所以我将用一个假想的、过于简化的具体例子来尝试回答这个问题。 我们想编写一个日志记录库,用户可以将其作为包包含在他们的解决方案中,以获得开箱即用的日志记录功能。 该库公开的接口为... public interface ILogger {} public interface ITarget {}
具体实现为
internal class Logger: ILogger { public Logger(ITarget target) {}}
internal class FileTarget : ITarget {}
要求是,如果用户包括我们的包并定义了一个类型为ILogger的属性的类或者有一个类型为ILogger的ctor参数,那么我们的库负责将该接口的具体实现注入到用户定义的类中。默认情况下,注入的记录器将进入文件系统进行记录,因为ILogger实现中注入的ITarget的默认实现是我们库中的FileTarget。
如果用户决定编写一个实现ITarget接口的类,那么我们的库将使用它来注入到Logger类中,而不使用其默认的FileTarget实现。
因此,我希望展示的是这里存在双向依赖关系。
1.我们的库依赖于用户的程序集,因为它需要扫描用户的程序集以加载任何扩展点(即ITarget实现)并将其在默认实现之前注入到自己的对象中。 2.用户的程序集依赖于库,因为如果用户选择定义一个依赖ILogger接口的类,则该用户对象应在运行时由我们的库提供该接口的具体引用。
简单的解决方案是,如果用户和我们的库都使用相同的IOC容器,那么问题就解决了。但这是一个强烈的假设。我想要做的是...
  1. 使用与库最符合要求的IOC容器,对我来说是Ninject。
  2. 在运行时,以某种API方式为用户提供机制,调用我的库,确保Ninject被启动并扫描用户的程序集,将所有扩展点考虑在内进行连接。

到目前为止,这很完美可行,但接下来就有些棘手了。

  • 如果用户也在使用Ninject,则问题自动解决,因为Ninject已经知道如何解析我们库中的接口。但是,如果用户决定使用自己选择的IOC容器呢?

我几乎想在库中定义一些类似于子容器功能的接口,例如:

public interface IDependencyResolvingModule { T Get<T>(); []T GetAll<T>(); }

并提供一个实现,该实现使用我们库的选择容器(即Ninect)来解析上述两个方法中请求的类型。

我希望用户的IOC容器具有某些功能,以便如果它无法解析依赖项(例如ILogger),则应钩入IDependencyResolvingModule实现并请求该依赖项。

这样,我们的库就可以使用其选择的IOC容器,而用户的代码则可以解决其IOC容器完全不了解的依赖关系。如果IOC容器能够提供注册任何在执行程序集目录中找到的IDependencyResolverModules单例实例的功能,并在无法解析类型时询问任何单例模块,那么这种解决方案是否可行呢?

但是,除非每个其他IOC容器都能适应该解决方案,否则还有什么其他方法可以解决这个问题呢?因此,问题就在于当第三方程序集选择使用IOC容器来处理其内部工作时,是否有一种简单的解决方案,使得该库可以提供一种机制,以便外部的IOC容器可以连接并解决位于库中的依赖项。


哎呀,我的 MyLibrary.SetLogger(ILogger logger) 不起作用了? - Ritch Melton
日志示例是我提供的一个过于简化的场景,以展示双向依赖关系。我正在开发的库需要更复杂的依赖关系解析,不能仅仅通过注入依赖项来满足静态方法的要求,正如你所演示的那样。 - afif
好的。我的问题更多地是关于当一个库需要使用自己的IOC容器时,用户有要求使用他们自己的IOC容器,这两者如何协同工作。 - afif
1个回答

3
我在这里看到了几种可能的方法:
  1. 为所有流行的IoC容器编写默认注册器。每个容器都应该放置在单独的程序集中。然后开发者可以选择需要的容器,并对其进行配置。

  2. 定义自己的工厂抽象,并编写默认实现,以返回默认记录器。让开发人员替换该工厂的实现。例如,使用他最喜欢的容器的适配器。这种方法最容器无关,因为开发人员可以直接使用默认的工厂实现。但这种方式与自动装配没有任何关系。

  3. 第一种方法的惰性变体。编写有关如何配置容器以使用默认实现的简短手册。然后开发者就可以自己配置容器。

  4. 将所有先前的解决方案组合起来,以满足每位开发人员的需求。:)

编辑:添加两个容器集成的示例。

var allPublicInterfacesFromLibrary = typeof(AnyLibraryType)
    .Assembly.GetTypes()
    .Where(t => t.IsInterface && t.IsPublic);

foreach (var libraryInterface in allPublicInterfacesFromLibrary)
{
    var local = libraryInterface; //to prevent closure
    applicationContainer.Register(
        Component.For(libraryInterface)
        //delegate resolving
        .UsingFactoryMethod(k => libraryContainer.Resolve(local)) 
        //delegate lifetime management
        .LifestyleTransient() 
    );
}

考虑到库绝对需要使用其选择的容器,而使用库的应用程序也使用了一个 ioc 容器,并且不能假设它们是相同的。因此,解决方案将需要沿着以下两个方向之一:要么通过钩子扩展应用程序的 ioc 容器的解析能力,以插入到库的 ioc 容器中,要么找到一种方法,通过从库的 ioc 容器中解析依赖项来在应用程序的 ioc 容器中注册这些依赖项。在两种情况下,库的容器都需要在应用程序的 ioc 容器中注册为单例。 - afif
该库的类型没有任何参考容器的内容。但是,根据扩展点所在位置、可以重写哪些类型、什么参数定义如何在运行时重写它们等运行时要求,决定了这些类型的构造方式。基本上,决定库对象图在运行时如何构造的特性可能会导致库使用最佳满足这些要求的IOC容器。 - afif
另一个例子是在运行时基于不同的约定要求将实现装饰到给定接口。您可以根据运行时提供的上下文,选择容器来注册类型到接口时如何进行装饰。这些要求并不意味着容器被引用在类内部,也就是服务定位器反模式,但它确实意味着库需要在运行时选择其容器以构建行为 - 我认为这是完全合理的。 - afif
我在我的最后两条评论中引用了几个例子。 - afif
这也是我想的。但我在思考是否需要为库中的每种类型注册一个工厂,还是可以用一行代码为所有类型完成?你能演示一下使用任何常见容器的一行代码吗? - afif
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接