how to write libraries without forcing users to us

2019-04-24 04:19发布

问题:

The short question is: Given a library warrants using a particular IOC container for its internals, when an application consumes that library, given the app warrants using an IOC container for wiring its dependencies, given if the the two containers are different, how can they play well together?

The scenario is, the application has classes defined that depend on types from the library. So when the application container attempts to build such a class, it needs to know how to resolve the type that lives in the library.

Here's the long winded question:

This question does seem to have been asked in different shapes and form before on SO, but I can't seem to find the answer I need so I am going to have a go at it with a hypothetical _over_simplified_ concrete example. We want to write a library for logging that users can include as a package in their solution to get logging functionality out of the box. The public interfaces the library exposes are..

public interface ILogger {}

public interface ITarget {}

Concrete implementations are

internal class Logger: ILogger { public Logger(ITarget target) {}}

internal class FileTarget : ITarget {}

Requirements are if the user includes our package and defines a class with a property of type ILogger or has a ctor argument of type ILogger then our library is responsible for injecting a concrete implementation for that interface into the user defined class. By default that injected logger will go to the file system for logging because the default implementation of an ITarget injected into the ILogger implementation is a FileTarget by our library.

If the user decides to write a class implementing the ITarget interface then our library will use that to inject into the Logger class and not use its default FileTarget implementation.

SO what I wish to demonstrate, is their is a bi-directional dependency here.

  1. Our library depends on the user's assemblies, since it needs to scan the user's assemblies to load any extension points (i.e. an ITarget implementation) and inject those into its own objects ahead of any default implementations.

  2. The user's assemblies depends on the library, since if the user chooses to define a class with an ILogger interface as a dependency, then that user object should get a concrete reference to that interface provided at runtime by our library.

The easy solution is if the user and our library are both using the same IOC container, then problem is solved. But this is a strong assumption. What I wish to do is

  1. Use an IOC container with the library that caters best to the library's requirement, in my case its Ninject.
  2. At run time somehow provide a mechanism for the user to call via some API into my library that will ensure Ninject is fired up and it scans the user's assemblies, and wires everything taking into account all extension points.

So far so good, its perfectly achievable, but here comes the tricky part.

  • if the user is also using Ninject, then problem automatically solved, since Ninject already knows how to resolve Interfaces living in our library. But what if the user decides to use his/her choice of IOC container?

I almost want to define some sort of child container functionality in the library with an interface like such

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

and provide an implementation that uses our library's choice of container (i.e. Ninect) to resolve the type requested in the two methods define above.

I want the user's IOC container to have some functionality where if it can't resolve a dependency (i.e. an ILogger), it should hook into the IDependencyResolvingModule implementation and ask for the dependency.

This way our library gets to use its choice of IOC Container, and the user's code has a way to resolve dependencies that its IOC container has no clue about. Wouldn't this solution work if IOC containers out there some how provided functionality to register singleton instances of any IDependencyResolverModules found in assemblies in the executing assembly's dir and when they can't resolve a type, ask any of the singleton modules?

But barring a solution that requires every other IOC container to accommodate, how else can this be solved? SO the problem in a few lines is, when a third party assembly chooses to use an IOC container for its internals, what is an easy solution such that this library can simply provide a mechanism for an IOC container sitting outside to hook into and resolve dependencies that live in the library.

回答1:

I see few possible approaches here:

  1. Write default registrator for all of the popular IoC containers. Each of them should be placed in the separate assembly. Then developer can choose the one he needs and configure his container with it.

  2. Define your own factory abstraction and write default implementation that will return the default logger. Let developer to substitute implementation of that factory. For example, with adapter for his favorite container. This approach is most container-agnostic, because developer can just use the default factory implementation. But this way has nothing to do with auto-wiring.

  3. The lazy variation of the first approach. Write small manual about configuring a container to work with default implementations. Then developer could configure the container himself.

  4. Combine all previous solutions to satisfy every developer. :)

EDIT: added example of integration of two containers

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