JavaScript DI/IoC equivalents to standard DI patte

2019-03-14 05:28发布

问题:

.NET and Java both have a slew of DI/IoC containers available to them and each have a number of patterns that I've found very useful at various points in working with them. I'm now at a point where I would like to do equivalent things in JavaScript. As JavaScript is a dynamic language, I don't expect DI/IoC containers to have direct equivalents to all the functionality provided by the containers found in statically typed languages so alternatives to these patterns are welcome. I also expect that the DI/IoC containers available in JavaScript will vary in their functionality, so references to varying containers are more than welcome.

The following patterns are those supported by Autofac 3 that I believe are applicable to dynamic languages. For general information about these patterns and relationships, see http://autofac.readthedocs.org/en/latest/resolve/relationships.html and http://nblumhardt.com/2010/01/the-relationship-zoo/. Most, if not all, of the concepts below are also available in other languages and DI/IoC containers such as Google Guice and Spring.

What are the nearest equivalents to the concepts and patterns described below in JavaScript?

General Concepts

Concept 1: Registration with the IoC container

Before the IoC container can create instances of a type, it needs to be aware of the type. This is done through registration. Registration is usually done declaratively:

class A {}
var builder = new ContainerBuilder();
builder.RegisterType<A>();

The above makes the IoC container aware of the type A. It discovers A's dependencies through reflection. Registration can also happen through functions that act as factories. These functions are often lambdas and may be written inline:

class B {}
class A {
    A(string name, B b) { }
}
builder.RegisterType<B>();
builder.Register(c => // c is a reference to the created container
    new A("-name-", c.Resolve<B>()));

Being able to provide factory functions is especially useful when you have a type that needs to be parameterized with a dependency that's not a service, such as the name in the example above.

As C# supports interfaces and abstract classes, it's frequently not the concrete data type that's important but instead the abstract type that it implements. In these cases you'll register the type as the interface or abstract class under which it should be available:

interface IPlugin {}
class P : IPlugin
builder.RegisterType<P>().As<IPlugin>();

With the above registration, any attempt to request a P would fail, but a request for an IPlugin would succeed.

Concept 2: Container Creation & the Composition Root

Once all of the registrations have been performed, the container needs to be created:

public class Program {
    public static void Main(string[] args) {
        var builder = new ContainerBuilder();
        /* perform registrations on builder */
        var container = builder.Build();
        /* do something useful with container */
    }
}

The container is created very early in the program lifecycle and becomes the composition root -- the location within the code that composes all the pieces of the application, ensuring that all the necessary dependencies are created. The container is then used to resolve the main component within the application:

public static void Main(string[] args) {
    var builder = new ContainerBuilder();
    /* perform registrations on builder */
    var container = builder.Build();
    var application = container.Resolve<Application>();
    application.Launch();
}

Concept 3: Lifetime & instance management

Given:

class A {}

If we want a new instance of A created for every dependency, it can be registered as builder.RegisterType<A>() without need to specify anything further.

If we want the same instance of A to be returned every time we need to register it as a 'SingleInstance':

builder.RegisterType<A>().SingleInstance();

Sometimes we want to share an instance within a certain scope but for different scopes we want different instances. For example, we might want to share a single database connection within all DAOs used to process a specific HTTP request. This is typically done by creating a new scope for each HTTP request and then ensuring that the new scope is used to resolve the dependencies. In Autofac this can be controlled manually as follows:

builder.RegisterType<A>().InstancePerLifetimeScope();
var scope = container.BeginLifetimeScope();
// within the request's scope
var root = scope.Resolve<RequestProcessor>();
root.Process();

General Patterns

Pattern 1: A needs an instance of B

class B {}     // registered as: builder.RegisterType<B>()
class A {      // registered as: builder.RegisterType<A>()
    A(B b) {}
}

var a = container.Resolve<A>();

The IoC container uses reflection to discover the dependency on B and inject it.

Pattern 2: A needs a B at some point in the future

class B {}
class A {
    A(Lazy<B> lazyB) {
        // when ready for an instance of B:
        try {
            var b = lazyB.Value;
        } catch (DependencyResolutionException) {
            // log: unable to create an instance of B
        }
    }
}

In this pattern the instantiation of the dependency needs to be delayed for some reason. In this case, let's assume that B is a plugin created by a 3rd party and whose construction may fail. In order to safely work with it the object construction would have to be guarded.

Pattern 3: A needs to create instances of B

class B {}
class A {
    A(Func<B> factory) {
        try {
            // frequently called multiple times
            var b = factory.Invoke();
        } catch (DependencyResolutionException) {
            // log: Unable to create
        }
    }
}

This pattern is typically used when there's a need to create multiple instances of a non value object. This also allows the creation of the instance to be deferred but usually does so for different reasons than those in Pattern 2 (A needs a B at some point in the future).

Pattern 4: A provides parameters of types X and Y to B.

class X {}
class Y {}
class B {
    B(X x, Y y) { }
}

This pattern is typically used when an injected dependency needs to be controlled or configured. Consider, for example, a DAO that needs a database connection string provided:

class DAO {
    DAO(string connectionString) {}
}
class A {
    A(Func<DAO> daoFactory) {
        var dao = daoFactory.Invoke("DATA SOURCE=...");
        var datum = dao.Get<Data>();
    }
}

Pattern 5: A needs all the kinds of B

interface IPlugin {}
class X: IPlugin {} // builder.RegisterType<X>().As<IPlugin>()
class Y: IPlugin {} // builder.RegisterType<Y>().As<IPlugin>()
class Z: IPlugin {} // builder.RegisterType<Z>().As<IPlugin>()
class A {
    A(IEnumerable<IPlugin> plugins) {
        foreach (var plugin in plugins) {
            // Add all plugins to menu
        }
    }
}

In this pattern multiple registrations are made for a given type. The consumer can then request all instances of the type and use them accordingly.

Pattern 6: A needs to know about B, OR A needs to know X about B

class B {} // builder.RegisterType<B>().WithMetadata("IsActive", true);

// A needs to know about B
class A {
    A(Meta<B> metaB) {
        if ((bool)metaB.Metadata["IsActive"]) {
            // do something intelligent...
        }
    }
}

// OR...

class B {} // builder.RegisterType<C>().WithMetadata<X>(...);
class X {
    bool IsActive { get; }
}

// A needs to know X about B
class A {
    A(Meta<B, X> metaB) {
        if (metaB.IsActive) {
            // do something intelligent...
        }
    }
}

Let's again say that we have a system that uses plugins. The plugins may be enabled or disabled or reordered at the user's will. By associating metadata with each plugin the system can ignore inactive plugins, or put the plugins in the order desired by the user.

Pattern 7: Composition of the patterns above

interface IPlugin:
class Plugin1 : IPlugin {}
class Plugin2 : IPlugin {}
class Plugin3 : IPlugin {}
class PluginUser {
    PluginUser(IEnumerable<Lazy<IPlugin>> lazyPlugins) {
        var plugins = lazyPlugins
                        .Where(CreatePlugin)
                        .Where(x => x != null);
        // do something with the plugins
    }

    IPlugin CreatePlugin(Lazy<IPlugin> lazyPlugin) {
        try {
            return lazyPlugin.Value;
        } catch (Exception ex) {
            // log: failed to create plugin
            return null;
        }
    } 
}

In this code sample we request a list of all plugins wrapped in a Lazy object so that they can be created or resolved at some point in the future. This allows their instantiation to be guarded or filtered.

Pattern 8: Adapters

This example taken from: https://code.google.com/p/autofac/wiki/AdaptersAndDecorators

interface ICommand {}
class SaveCommand: ICommand {}
class OpenCommand: ICommand {}
var builder = new ContainerBuilder();

// Register the services to be adapted
builder.RegisterType<SaveCommand>()
       .As<ICommand>()
       .WithMetadata("Name", "Save File");
builder.RegisterType<OpenCommand>()
       .As<ICommand>()
       .WithMetadata("Name", "Open File");

// Then register the adapter. In this case, the ICommand
// registrations are using some metadata, so we're
// adapting Meta<ICommand> instead of plain ICommand.
builder.RegisterAdapter<Meta<ICommand>, ToolbarButton>(
   cmd =>
    new ToolbarButton(cmd.Value, (string)cmd.Metadata["Name"]));

var container = builder.Build();

// The resolved set of buttons will have two buttons
// in it - one button adapted for each of the registered
// ICommand instances.
var buttons = container.Resolve<IEnumerable<ToolbarButton>>();

The above allows all the commands registered to be automatically adapted into a ToolbarButton making them easy to add to a GUI.

Pattern 9: Decorators

interface ICommand {
    string Name { get; }
    bool Execute();
}
class SaveCommand : ICommand {}
class OpenCommand : ICommand {}
class LoggingCommandDecorator: ICommand {
    private readonly ICommand _cmd;
    LoggingCommandDecorator(ICommand cmd) { _cmd = cmd; }
    bool Execute() {
        System.Console.WriteLine("Executing {0}", _cmd.Name);
        var result = _cmd.Execute();
        System.Console.WriteLine(
            "Cmd {0} returned with {1}", _cmd.Name, result);
        return result;
    }
}

// and the corresponding registrations
builder.RegisterType<SaveCommand>().Named<ICommand>("command");
builder.RegisterType<OpenCommand>().Named<ICommand>("command");
builder.RegisterDecorator<ICommand>((c,inner) =>
    new LoggingCommandDecorator(inner), fromKey: "command");
// all ICommand's returned will now be decorated with the
// LoggingCommandDecorator. We could, almost equivalently, use
// AOP to accomplish the same thing.

Summary

First, although I've tried to make the examples reasonably represent the described pattern these are illustrative toy examples that may not be ideal because of space constraints. What's more important to me are the concepts, patterns, and nearest JavaScript equivalents. If most IoC/DI containers in JavaScript don't support some of the patterns above because there are far easier ways to do it, fair enough.

What are the nearest equivalents to the concepts and patterns described below in JavaScript?

回答1:

Some of the functionality you mentioned is achieved by using AMD. For example look at RequireJS: http://requirejs.org/docs/whyamd.html

Another implementation worth looking at is of Angular JS DI: https://docs.angularjs.org/guide/di

You can easily inject modules (units of functionality wrapped in a closure and metadata) in such a way that implementation is abstracted away. It allows to switch implementation, run mocks in test units and much more.

Hope it helps