Spider 101

June 14th, 2009

The Spider is a complete Inversion of Control (IoC) container providing automatic dependency injection for Java classes. Although the Spider is extremely powerful, we have tried hard to ensure using it is simple and intuitive. Our goal is to help make your Java code simpler, easier to test, and easier to change.


How does it work?
The Spider’s primary approach is field based injection. We feel that this approach results in the clearest, and smallest amount of code. The Spider automatically injects class dependencies, declared as fields, based on the scope of the field. The Spider will attempt to inject any non-null field that is not private.

For example:

class Customer {
    private Person p; // not-injected
    Foo foo = new Foo(); // not-injected
    Bar bar; // injected
    Baz baz; // injected
    /* ... elided implementation ... */
}


What About Encapsulation?
Developers are sometimes concerned about exposing these fields instead of marking them as private. Don’t be. The Spider encourages (more precisely, demands) interfaces be used to expose an object’s state and behavior. Forcing you to use interfaces enforces encapsulation. This has many positive flow on effects, such as looser coupling and improved testability.

Concepts



Dependencies
A Dependency is a class that is used by another class. More specifically, it consists of an interface with at least one implementation. To be eligible for injection, it must be a non-private, non-null instance field.


Hosts
A class containing Dependencies is known as a Host. Conceptually, a Host is a node within a directed class Dependency graph. The Spider can handle cyclic dependencies in the graph. Good Spider.


Wiring
At its most basic, wiring represents rules for mapping interfaces to implementations. Wiring is normally performed inside a Web. A Web is a Java class that contains custom Spider configuration. Your Web is used by the Spider bootstrap process.

By using a Mapper, you can specify auto-wiring rules. For example, your coding convention might be that an implementation of an interface X normally has the name XImpl. Using a Mapper allows you to specify this convention to avoid configuring each wiring rule by hand. Wiring then becomes a list of exceptions to this convention.


Providers
Providers instantiate Dependencies. In its simplest form, a Provider is an abstraction of new. More complicated Providers might return proxied implementations or supply default parameters. You can make your own Providers, if you need to. They are the simplest building block for instantiating different types of objects.


Factories
Factories determine the Provider for groups of dependencies. The Spider uses Factories to create Providers. You can make your own Factories, if you need to. Factories are a good way to support instantiating objects of similar flavour or style. Factories are often coupled with a Provider.


Scope
You specify the classes you want the Spider to inject by passing their package names to a Scoper. This is normally done in your Web configuration class (more information below). By defining the scope of the classes you want injected in this way gives you the flexibility to use other dependency injection containers along-side the Spider, or to limit the scope of the Spider, should you wish to.

Configuration



Wiring
A Mapper is used to specify default implementation for interface. For exceptions to the default Mapper rules, you can use a Wire.

If there is more than one implementation for a specific interface, you can specify which implementation to use by using a Wire. You can also specify different implementations, depending on the Host. You can also provide an instance of an implementation class to use for a given interface.

Now for some examples. The examples assume that a field has been declared for the wirer:

Wire wire;

Simple cases, binding an interface to an implementation:

wire.cls(InterfaceImpl.class).to(Interface.class);
wire.cls(OtherImpl.class).to(Other.class);

Multiple implementations for an interface, this example wires a ‘chain’ of objects:

// 1. Wire the general case, wherever you need a Thing interface, inject a DelegatingThing.
// 2. Wire the special case, for the Thing in DelegatingThing, inject a DefaultThing.
wire.cls(DelegatingThing.class).to(Thing.class);
wire.cls(DefaultThing.class).to(Thing.class, DelegatingThing.class);

Singletons:

// Explicitly wire a singleton implementation.
wire.cls(DefaultThing.class).one().to(Thing.class);

// Implicitly wire an instance of a singleton,
// the spider uses existing factories and bindings
// to determine the implementation class.
wire.nu(Thing.class).to(Thing.class);

Wiring instances:

wire.ref(new ArrayList()).to(List.class);


NOTE: The default instantiation behaviour of the Spider sometimes can’t provide you with all the different types of objects you need. Two examples of this are when using Swing or Collections. They violate the Spider’s default constructor rules, so can not be instantiated using the default instantiation behaviour.

Spider provides two mechanisms which can be used to overcome this:

  • implementing a Factory (which is discussed later);
  • or implementing a Provider.

Here is an example of a simple List provider:

public final class ListProvider extends CleanProvider {
    public List nu(Object... args) {
        return new ArrayList();
    }
}

You then wire up your Provider like this:

// Use the ListProvider to create a new list every time you need one.
wire.provider(new ListProvider()).to(List.class);

// Use the ListProvider to create a (lazy) singleton list.
wire.provider(new ListProvider()).one().to(List.class);


Aspects
The Spider provides a mechanism for adding an aspect layer to interfaced objects via Java proxies.

The Aspector exposes a straight forward API for registering layers, defining the concrete implementation and then any Layers to be added to it. The Aspector is NOT accumulative so all Layers must be declared up front.

Now for some examples. The examples assume that a field has been declared for the Aspector:

Aspector aspector;

A good example of the power of abstracting to a Layer can be seen with the AsychronousLayer. All interfaced methods for a class can be made asynchronous (careful!) through one declaration:

aspector.cut(Normal.class, AsynchronousLayer.class)


Factories
Under the covers, the Spider uses Factories to infer Providers for groups of classes. You can easily create your own Factories. Using Factories can significantly reduce the amount of wiring configuration.

The Spider provider a number of built in Factories:

  • ImplicitFactory - allows a naming pattern to be used to define implementations. Boost uses a prefix mapping, which specifies that the Spider should try to find an implementation with a prefix of ‘Default’, e.g. Thing would resolve to DefaultThing. Different mappings can be specified, including suffix or package mappings.
  • IncrediblesFactory - provides proxy implementations for immutable data interfaces marked with Incredible, Strong, or Struct. An example of Incredibles in action can be seen in the Boost demo tests, IncrediblesBasicDemoTest, IncrediblesOrderDemoTest.
  • EdgeFactory - provides proxied edge implementations for interfaces marked with Edge. An example of Edge factory in action can be seen in the Edge*DemoTests.

Factories can be implemented in a fairly straight forward manner. As an example this is the IncrediblesFactory:

public final class IncrediblesFactory implements Factory {
    Impl impl;
    Is is;

    public Provider nu(InjectionType type) {
        Class raw = type.rawClass();
        Interface iface = new DefaultInterface(raw);
        return impl.impl(IncrediblesProvider.class, iface);
    }

    public boolean can(InjectionType type) {
        return is(type, Incredible.class);
    }

    private boolean is(InjectionType type, Class match) {
        return is.assignable(type, match);
    }
}

Simply implement the Factory interface and then register your factory, like in the BoostWeb:

Factorer factories;
factories.add(ImplicitFactory.class);


Webs
Webs are the units that the spider uses to define the configuration. An example web:

class ExampleWeb implements Web {
    Factorer factories;
    Aspector aspector;
    Mapper mapper;
    Scoper scoper;
    Wire wire;

    public void web() {
        scope();
        mapping();
        factories();
        wiring();
        aspects();
    }

    private void scope() {
        scoper.scope("");   // Very important.  Discussed below.
    }

    private void mapping() {
        mapper.prefix("Default");   // Also very important. Discussed below.
    }

    private void factories() {
        factories.add(MyCustomFactory.class);
    }

    private void wiring() {
        wire.cls(Implementation.class).to(Interface.class);
    }

    private void aspects() {
        aspector.cut(BackgroundTask.class, AsynchronousLayer.class);
    }
}

Webs are passed to the bootstrap process (see the next section). During bootstrap, the Spider will inject the Web’s fields and then call the web() method.

Note that by passing “” to the Scoper in this way indicates that all classes in the object graph should be injected by the Spider, irrespective of the package they are in. You can narrow the scope of classes the Spider will inject by passing specific package prefixes to the Scoper.

The Mapper instruction in the example above indicates to the Spider that for all classes in the object graph, the concrete implementations of interfaces (as required when injecting dependencies) will have the prefix of Default. It also indicates that the implementation class will reside in the same package as the interface. You can specify a multitude of different auto-wiring rules using the Mapper.

To avoid over complicated configuration, such as the familiar Spring XML hell, it is important to be able to compose webs. This can be achieved by passing multiple webs to the bootstrap process (next section, I says). You can also compose webs by using a Spinneret like this:

public final class CarWeb implements Web {
    Spinneret spinneret;

    public void web() {
        spinneret.spin(ChassisWeb.class, WheelWeb.class, EngingWeb.class, BodyWeb.class);
    }
}

Bootstrapping



Spider Eggs
Spiders are hatched from eggs (yes, the egg comes first):

Egg egg = new DefaultEgg();
Spider spider = egg.hatch(BoostWeb.class, MyWeb.class);
// lots of useful pre goes here


Main
Given the importance of the egg it is important that you can get one as soon as you get into your program. For command line applications we provide a simple pattern to assist with this.

Firstly you specify a main method as normal. In this main method you specify only two lines:

  • Create an entry point with your webs (MiningWeb).
  • Call that entry point with your Go class and any arguments.

Your Go class (MiningGo) will be instantiated and imbued with spiderly powers (the Miner field is injected).

Example Main:

public final class MiningMain {
    public static void main(String[] args) {
        Main main = new SpiderMain(MiningWeb.class);
        main.main(MiningGo.class, args);
    }
}

Example Go:

public final class MiningGo implements Go {
    Miner iMe;

    public void go(String[] args) {
        iMe.mine();
    }
}

Runtime



Spider
The Spider interface is effectively a facade over four runtime tools, Nu, Impl, Injector and Resolver. The bootstrap processes, such as the Egg and SpideredMain, will return an instance of the spider that can be narrowed to the appropriate tool.


Resolver
The Resolver allows for explicit resolution of a class wired in the spider. It uses the same rules for resolution as the implicit field injection.

Example:

public final class CatInTheHat {
    Resolver resolver;

    public void play() {
        Thing one = resolver.resolve(Thing.class);
        Thing two = resolver.resolve(Thing.class);
        one.playWith(two);
    }
}

Note that using the Resolver does not provide access to the Host class, so wiring that applies to a specific Host and/or name is ignored. Provider implementations must also be careful to handle the fact that not all instances have a Host.


Nu
The Nuer allows for instantiation of objects using just an interface. It also is a powerful mechanism for “nuing” proxied objects, such as Incredibles and Edges.

Example:

public final class GreenEggsAndHam {
    Person bob;
    Nu nu;

    public void eat() {
        Egg egg = nu.nu(Egg.class, Color.GREEN);
        Ham ham = nu.nu(Ham.class, Color.GREEN);
        bob.eat(egg, ham);
    }
}


Injector
The Injector allows already instantiated object to be injected by the spider.

Example:

public final class Doctor {
    Injector injector;
    Person bob;

    public void inject(Syringe syringe) {
        injector.inject(syringe);
        syringe.poke(bob);
    }
}


Impl
In addition to the three core tools, there is an additional class Impl, which behaves in a manner similar to Nu. The primary difference is that Impl can instantiate any resolvable or concrete class.

Importantly when using Impl to construct a concrete implementation it is not possible to add an aspect as no interface is made available.

Lifecycle


Objects created by the spider have a simple construction lifecycle. Objects can hook into this lifecycle using the Constructable interface. This method is called after the object is instantiated and injected. This is just like the init-method in Spring.

Example:

public final class DefaultChickenOrEgg implements ChickenOrEgg, Constructable {
    private final Barn barn;
    private Chicken chicken;
    private Egg egg;
    Nu nu;

    public DefaultChickenOrEgg (Barn barn) {
        this.barn = barn;
    }

    public void constructor() {
        egg = nu.nu(Egg.class, barn.time());
        chicken = nu.nu(Chicken.class, barn.time());
    }

    public Object first() {
        return egg.before(chicken) ? egg : chicken;
    }
}