|
ProposalForNewableVsInjectable
How the injector will distinguish between newable and injectable types
IntroductionThis feature is motivated by Misko's blog post, To new or not to new (http://misko.hevery.com/2008/09/30/to-new-or-not-to-new/) The basic idea is that a class should ideally either be constructed entirely from things the injector knows how to provide, or entirely from objects that another class should create at runtime. Mixing them is always problematic, so perhaps we can disallow it in Noop. Injector needs to provide all dependenciesLike Guice, we would never allow constructing an object partly from the injector and partly from objects created at runtime: class BankService(String name, DatabaseConnection c) {...}
// Not allowed
String foo = "bar";
BankService b = new BankService(foo, /* some magic to get the DB connect */);And what if we let the user mix'n'match?We might actually want to be able to let the injector injects what it can and the user fill in what he/she can. class BankServiceImpl(String name, DbConnection c) {
// some code
}
class Foo() {
Void myMethod() {
BankServiceImpl.new(name -> "myName"); // let the injector fill the blanks
BankServiceImpl.new(String -> "myName"); // let the injector fill the blanks
}
}
Even newables are mockableWe want to guarantee that there is always a seam between two classes, so that they can be tested in isolation. This should even be true for newable types. // in Foo.noop
class Foo() {
Void method() {
email = Email.new("alexeagle@google.com", "How's it going?", "Just checking in, thanks");
email.addAttachment(attachment); // there's some nasty MIME encoding under here, let's say
}
}
// in FooTest.noop
test {
Email -> MockEmail;
Foo.new().method(); // doesn't do the real attachment logic
}So, this means that every object instantiation is handled by the injector. But, in the email class, the injector needs to just accept the user's parameters. class Email(String to, String subject, String body) {
}Determining whether a type is newable or injectableIn this case, we could bootstrap our knowledge by noting that surely String is newable. Since Email takes only newable parameters, Email is newable as well. Types we know to be newable: String, Int, Array, Date, Float, Map, Enum, etc Disallow mixing newable and injectable as dependenciesThis is controversial, because it might be really surprising. So this would be illegal: class BankService(BankDatabase db) {}
// Can't mix newable and injectable types!
class Email(String to, BankService service) {}Does this work in general? Can we mark a type as newable, and then just follow the rule that you either compose newable types or non-newable types? A problemWe can't really tell whether an object is newable or injectable from its compile-time type. If we are in a scope where Email is bound to an instance, then Email is injectable within that scope, and it makes sense to have a Foo(Email e, BankService b) constructor. But if you're outside that scope, that Foo constructor breaks the newable/injectable rule. This may be a deal-killer for this whole idea. |
We could tell if we would the class with a keyword creatable, injectable, or interface (can be one and only one of the three.
interface BankService { WithdrawalTransaction withdraw(Money m); } newable Money(Float amount, Currency currency) { ... } injectable BankServiceImpl { WithdrawalTransaction withdraw(Money m) { ... } }The use of interface and class may be just java-isms we have to jettison, instead using interface, injectable, newable... or maybe contract, injectable, newable
This shares strong resemblance with what you find in C# and C++ between class keyword and struct.
- class keyword would then allow to define injectables (class types) and would accept class types and struct types as fields.
- struct keyword would allow to define newables (struct types) and would only accept struct types as fields.
Similar, though with structs, you don't have method declarations, or mutators/accessors. Newables would be first-class objects like Injectables, but they wouldn't be governed by the Injector/Dependency-Injection Container.
Unlike C#, they aren't kept on the stack and quickly garbage collected either - they would behave just like injectables in this regard.
Do we really need to mix "injectables" and "newables"? (I always call them "services" and "domain objects" because thats how it almost always breaks down but its pretty telling that the concept is so well known that everybody has their own term for it but no one has addressed it!). I've always seen the problem this way...
Dependency Injection and new aren't inherently different. Dependency injection is saying 'you can get a instance of an object by asking me for it' and the DI framework will handle the request however it chooses. Sometimes, we tell the framework, "whenever anyone asks for an instance of class C, give them this (singleton) instance". This is inherently part of construction but it doesn't have to be part of a constructor. We give 'new' a type and a list of (constructor) parameters and tell it 'now give me an object of the requested type with that info'. Only part of that process has to be the 'constructor'. DI with AOP looks exactly like this.
The @Inject annotation mechanism is always far more readable than constructor parameter injection and it won't get in the way of passing newables. Its only drawback, IMHO, is that its not quite as clear what is used in object construction if injectables arent constructor parameters, but this can be fixed with syntax or a code standard.
To be clear, here's your example with a possible syntax (off the top of my head so there are probably better ways):
/* This is how I customize my injector */ Injector.addSingleton(new BankService()); // use singleton rules when newing BankService (ie always return this instance) Injector.addPrototype(new ATM()); // use Prototype rules when newing ATMs (ie create a new one). /* This is how I create an instance of an email that needs a BankService for some reason :-) */ class EMail(String to) { @Injectable BankService bankService; } new EMail("sofat@yourmom.com"); // I already told new how to get a BankService so it just needs a string.If I wanted to customize the BankService? at the time I call new EMail (which would be rare and might be a bad idea) then I could give new rules to 'new' for generating a BankService? OR we could allow a different notation like
new EMail<myOtherBankServiceInstance>("sofat@yourmom.com"); // Pretty crappy notation but you get the idea(Ive got a better idea for this below)
FYI, I actually kind of like the idea of replacing Injector with New in my above examples so they become:
This makes clear the fact that dependency injection is just overloading New's behavior (New being like an Object in Scala parlance). Of course, if DI is going to be statically checked instead of done at run time (which I agree is wise) then this would have to be done a little differently but the general idea remains.
Again, what if I want to sometimes inject a parameter and sometimes pass it? Since the only difference between injection and parameter passing is syntax, this should always be allowed but it doesn't have to be 'clean' since it should be discouraged. If Noop DI ends up looking like PicoContainer?, which I hope it does, then the problem boils down to finding an efficient and expressive way of saying that we need to create a new container that is a child of the "current" container (also known as "New") and change some of its contents. This is cleaner than adding another function call notation like I mentioned above. I think it can be made to work pretty simply.