Dependency Inject (DI) “friendly” library

后端 未结 4 558
天命终不由人
天命终不由人 2020-11-22 00:23

I\'m pondering the design of a C# library, that will have several different high level functions. Of course, those high-level functions will be implemented using the SOLID c

相关标签:
4条回答
  • 2020-11-22 00:42

    The term "dependency injection" doesn't specifically have anything to do with an IoC container at all, even though you tend to see them mentioned together. It simply means that instead of writing your code like this:

    public class Service
    {
        public Service()
        {
        }
    
        public void DoSomething()
        {
            SqlConnection connection = new SqlConnection("some connection string");
            WindowsIdentity identity = WindowsIdentity.GetCurrent();
            // Do something with connection and identity variables
        }
    }
    

    You write it like this:

    public class Service
    {
        public Service(IDbConnection connection, IIdentity identity)
        {
            this.Connection = connection;
            this.Identity = identity;
        }
    
        public void DoSomething()
        {
            // Do something with Connection and Identity properties
        }
    
        protected IDbConnection Connection { get; private set; }
        protected IIdentity Identity { get; private set; }
    }
    

    That is, you do two things when you write your code:

    1. Rely on interfaces instead of classes whenever you think that the implementation might need to be changed;

    2. Instead of creating instances of these interfaces inside a class, pass them as constructor arguments (alternatively, they could be assigned to public properties; the former is constructor injection, the latter is property injection).

    None of this presupposes the existence of any DI library, and it doesn't really make the code any more difficult to write without one.

    If you're looking for an example of this, look no further than the .NET Framework itself:

    • List<T> implements IList<T>. If you design your class to use IList<T> (or IEnumerable<T>), you can take advantage of concepts like lazy-loading, as Linq to SQL, Linq to Entities, and NHibernate all do behind the scenes, usually through property injection. Some framework classes actually accept an IList<T> as a constructor argument, such as BindingList<T>, which is used for several data binding features.

    • Linq to SQL and EF are built entirely around the IDbConnection and related interfaces, which can be passed in via the public constructors. You don't need to use them, though; the default constructors work just fine with a connection string sitting in a configuration file somewhere.

    • If you ever work on WinForms components you deal with "services", like INameCreationService or IExtenderProviderService. You don't even really know what what the concrete classes are. .NET actually has its own IoC container, IContainer, which gets used for this, and the Component class has a GetService method which is the actual service locator. Of course, nothing prevents you from using any or all of these interfaces without the IContainer or that particular locator. The services themselves are only loosely-coupled with the container.

    • Contracts in WCF are built entirely around interfaces. The actual concrete service class is usually referenced by name in a configuration file, which is essentially DI. Many people don't realize this but it is entirely possible to swap out this configuration system with another IoC container. Perhaps more interestingly, the service behaviors are all instances of IServiceBehavior which can be added later. Again, you could easily wire this into an IoC container and have it pick the relevant behaviors, but the feature is completely usable without one.

    And so on and so forth. You'll find DI all over the place in .NET, it's just that normally it's done so seamlessly that you don't even think of it as DI.

    If you want to design your DI-enabled library for maximum usability then the best suggestion is probably to supply your own default IoC implementation using a lightweight container. IContainer is a great choice for this because it's a part of the .NET Framework itself.

    0 讨论(0)
  • 2020-11-22 00:58

    EDIT 2015: time has passed, I realize now that this whole thing was a huge mistake. IoC containers are terrible and DI is a very poor way to deal with side effects. Effectively, all of the answers here (and the question itself) are to be avoided. Simply be aware of side effects, separate them from pure code, and everything else either falls into place or is irrelevant and unnecessary complexity.

    Original answer follows:


    I had to face this same decision while developing SolrNet. I started with the goal of being DI-friendly and container-agnostic, but as I added more and more internal components, the internal factories quickly became unmanageable and the resulting library was inflexible.

    I ended up writing my own very simple embedded IoC container while also providing a Windsor facility and a Ninject module. Integrating the library with other containers is just a matter of properly wiring the components, so I could easily integrate it with Autofac, Unity, StructureMap, whatever.

    The downside of this is that I lost the ability to just new up the service. I also took a dependency on CommonServiceLocator which I could have avoided (I might refactor it out in the future) to make the embedded container easier to implement.

    More details in this blog post.

    MassTransit seems to rely on something similar. It has an IObjectBuilder interface which is really CommonServiceLocator's IServiceLocator with a couple more methods, then it implements this for each container, i.e. NinjectObjectBuilder and a regular module/facility, i.e. MassTransitModule. Then it relies on IObjectBuilder to instantiate what it needs. This is a valid approach of course, but personally I don't like it very much since it's actually passing around the container too much, using it as a service locator.

    MonoRail implements its own container as well, which implements good old IServiceProvider. This container is used throughout this framework through an interface that exposes well-known services. To get the concrete container, it has a built-in service provider locator. The Windsor facility points this service provider locator to Windsor, making it the selected service provider.

    Bottom line: there is no perfect solution. As with any design decision, this issue demands a balance between flexibility, maintainability and convenience.

    0 讨论(0)
  • 2020-11-22 01:02

    What I would do is design my library in a DI container agnostic way to limit the dependency on the container as much as possible. This allows to swap out on DI container for another if need be.

    Then expose the layer above the DI logic to the users of the library so that they can use whatever framework you chose through your interface. This way they can still use DI functionality that you exposed and they are free to use any other framework for their own purposes.

    Allowing the users of the library to plug their own DI framework seems a bit wrong to me as it dramatically increases amount of maintenance. This also then becomes more of a plugin environment than straight DI.

    0 讨论(0)
  • 2020-11-22 01:04

    This is actually simple to do once you understand that DI is about patterns and principles, not technology.

    To design the API in a DI Container-agnostic way, follow these general principles:

    Program to an interface, not an implementation

    This principle is actually a quote (from memory though) from Design Patterns, but it should always be your real goal. DI is just a means to achieve that end.

    Apply the Hollywood Principle

    The Hollywood Principle in DI terms says: Don't call the DI Container, it'll call you.

    Never directly ask for a dependency by calling a container from within your code. Ask for it implicitly by using Constructor Injection.

    Use Constructor Injection

    When you need a dependency, ask for it statically through the constructor:

    public class Service : IService
    {
        private readonly ISomeDependency dep;
    
        public Service(ISomeDependency dep)
        {
            if (dep == null)
            {
                throw new ArgumentNullException("dep");
            }
    
            this.dep = dep;
        }
    
        public ISomeDependency Dependency
        {
            get { return this.dep; }
        }
    }
    

    Notice how the Service class guarantees its invariants. Once an instance is created, the dependency is guaranteed to be available because of the combination of the Guard Clause and the readonly keyword.

    Use Abstract Factory if you need a short-lived object

    Dependencies injected with Constructor Injection tend to be long-lived, but sometimes you need a short-lived object, or to construct the dependency based on a value known only at run-time.

    See this for more information.

    Compose only at the Last Responsible Moment

    Keep objects decoupled until the very end. Normally, you can wait and wire everything up in the application's entry point. This is called the Composition Root.

    More details here:

    • Where should I do Injection with Ninject 2+ (and how do I arrange my Modules?)
    • Design - Where should objects be registered when using Windsor

    Simplify using a Facade

    If you feel that the resulting API becomes too complex for novice users, you can always provide a few Facade classes that encapsulate common dependency combinations.

    To provide a flexible Facade with a high degree of discoverability, you could consider providing Fluent Builders. Something like this:

    public class MyFacade
    {
        private IMyDependency dep;
    
        public MyFacade()
        {
            this.dep = new DefaultDependency();
        }
    
        public MyFacade WithDependency(IMyDependency dependency)
        {
            this.dep = dependency;
            return this;
        }
    
        public Foo CreateFoo()
        {
            return new Foo(this.dep);
        }
    }
    

    This would allow a user to create a default Foo by writing

    var foo = new MyFacade().CreateFoo();
    

    It would, however, be very discoverable that it's possible to supply a custom dependency, and you could write

    var foo = new MyFacade().WithDependency(new CustomDependency()).CreateFoo();
    

    If you imagine that the MyFacade class encapsulates a lot of different dependencies, I hope it's clear how it would provide proper defaults while still making extensibility discoverable.


    FWIW, long after writing this answer, I expanded upon the concepts herein and wrote a longer blog post about DI-Friendly Libraries, and a companion post about DI-Friendly Frameworks.

    0 讨论(0)
提交回复
热议问题