changing how bindings are wired to code

1 view
Skip to first unread message

Alex Eagle

unread,
Nov 19, 2009, 2:18:13 PM11/19/09
to noop...@googlegroups.com, kev...@google.com
Jeremie, Michael Feathers and I had lunch with Kevin Bourillion this week, and talked about Guice and Noop. I've been thinking lately that we can reduce our risk if we have injection in Noop work the same way as it does in Guice, because we'll take advantage of the design decisions they made and all the thought that went into it.

We have a very new idea which I don't think any existing injection framework does: adding new bindings at runtime by pushing a child injector. There are a couple problems. The big one is that you break the seam if you bind an interface right before making an instance, so we'd need some mechanism for tests to have their bindings override that one even though our normal model would suggest the youngest child injector should override what happened in the test. A second problem is we allow people to change bindings in code, which from a pure DI perspective, is at least a bad practice.

Kevin asked a simple question: do we have the concept of a module? In answering that question, we came up with a new design, which I'll try to summarize:

1) Keep the bindings keyword. A file can contain bindings directly, like this:
package foo;
bindings Main {
  Application -> MyApplication;
}

2) When starting a program, you list one or more bindings to start:
noop foo.Main

3) We create the injector using the bindings provided. Like with Guice best practice, this means we create the eager parts of the object graph immediately in our internal "main", then call a method on the first part of the object graph. We'll call the main() method defined on MyApplication
package foo;
class MyApplication() implements Application {
  Int main() {
  }
}

4) We no longer need to receive the list of args in the main. I think that's actually good. The MyApplication class can depend on a RawCommandLineArgs or a CommandLineFlags which would be pre-parsed, but even better, we can do the FlagsBinder ourselves 

5) Tests still get to override the bindings as they see fit:
class MyClass(Console c) {
  Void print() {
    c.println("hello");
  }
  unittest "should print" bindings (Console -> MockConsole) {
    this.print();
  }
}

test "myclass should print" bindings (Console -> MockConsole) {
  MyClass mc = MyClass();
  mc.print();
}

6) We would change the syntax to no longer recognize the bindings keyword on a class or method. If a library needs some bindings in order to work, they must be provided when the application is started.

Thoughts?
-Alex

Christian Edward Gruber

unread,
Nov 19, 2009, 10:07:05 PM11/19/09
to noop...@googlegroups.com, kev...@google.com

On Nov 19, 2009, at 2:18 PM, Alex Eagle wrote:

> Jeremie, Michael Feathers and I had lunch with Kevin Bourillion this
> week, and talked about Guice and Noop. I've been thinking lately
> that we can reduce our risk if we have injection in Noop work the
> same way as it does in Guice, because we'll take advantage of the
> design decisions they made and all the thought that went into it.

Boosterism aside, I generally agree, with caveats. Scopes,
specifically, need to be related to other scopes, in the sense of what
scopes are narrower or wider (or orthogonal). Right now, Guice
doens't provide that. PicoContainer (and other) provide it by means
of making the Container BE the scope, so to speak... you have a new
injector/container for each instance of scope, so there are two parts
to the injector - the wiring metadata, and the running container with
instances. Two different approaches, but we need to make sure that
whichever design choice we make, we ensure scope lifetimes are
enforced (so you can't depend on something scoped in a narrower
lifetime than yours.

> We have a very new idea which I don't think any existing injection
> framework does: adding new bindings at runtime by pushing a child
> injector.

Actually, most do - picocontainer specifically works this way. I
think guice's child injectors are more permissive, but that's based on
a javadoc comment I saw in passing yesterday in the guice codebase. I
need to look more carefully.

> There are a couple problems. The big one is that you break the seam
> if you bind an interface right before making an instance, so we'd
> need some mechanism for tests to have their bindings override that
> one even though our normal model would suggest the youngest child
> injector should override what happened in the test.

This needs a whiteboard. I agree, we need to solve this.

> A second problem is we allow people to change bindings in code,
> which from a pure DI perspective, is at least a bad practice.

This is the biggest thing that freaks me out. Actually, it freaks me
out about a language-supported injection scheme at all - how do you
ensure the lifecycle of the code such that you can discover all the
necessary bindings. If we let them do it in the code itself, we have
the above problem of coupling. If not, we end up with the problem of
staging all the bindings before the code, so the code is properly
contextualized when it's run. But you start to address this below.
I think is pretty decent. I think it should, however, be possible to
dynamically load bindings much as Java loads classes in custom
classloaders. But you can't override bindings that exists in loaded
production code (but you can in test blocks, per the above).

I think this starts to change the whole ball-game, from a staging
perspective, and makes it easier on the interpreter, to be sure,
because you get all the bindings you care about. In essence, Main is
a root binder which also declares a startup entry point (or we have a
default entry-point binding called Application which something has to
bind to, much like Java requires that something implement static
main. This can be one of a very few "well known" binding keys.

I think we should try it, and see. We may need to alter some of the
Guice semantics, but the basic approach should be definitely of that
style. Personally, however, I think there's a place for lifecycle
management, currently not supported in Guice (separating wiring/
configuration from initialization/startup as separate walks through
the dependency tree (or just in time)). This is so we can introspect
the configured instances without starting services (opening
connections, etc.), allowing for cheaper wiring tests and other such
things.

Christian.


cheers,
Christian.
Reply all
Reply to author
Forward
0 new messages