It so happens that these days possibly I have started looking at things a bit differently. I have been programming more in Scala and Clojure and being exposed to many of the functional paradigms that they encourage and espouse, it has stated manifesting in the way I think of programming. In this post I will look into dependency injection on a different note. At the end of it may be we will see that this is yet another instance of a pattern melding into the chores of a powerful language's idiomatic use.
In one of my projects I have a class whose constructor has some of its parameters injected and the others manually provided by the application. Guice has a nice extension that does this for you - AssistedInject. It writes the boilerplate stuff by generating an implementation of the factory. You just need to annotate the implementation class' constructor and the fields that aren't known to the injector. Here's an example from the Guice page ..
public class RealPayment implements Payment {
@Inject
public RealPayment(
CreditService creditService, // injected
AuthService authService, // injected
@Assisted Date startDate, // caller to provide
@Assisted Money amount); // aller to provide
}
...
}
Then in the Guice module we bind a
Provider<Factory>
..bind(PaymentFactory.class).toProvider(
FactoryProvider.newFactory(
PaymentFactory.class, RealPayment.class));
The
FactoryProvider
maps the create()
method's parameters to the corresponding @Assisted
parameters in the implementation class' constructor. For the other constructor arguments, it asks the regular Injector
to provide values.So the basic issue that
AssistedInject
solves is to finalize (close) some of the parameters at the module level to be provided by the injector, while keeping the abstraction open for the rest to be provided by the caller.On a functional note this sounds a lot like currying .. The best rationale for currying is to allow for partial application of functions, which does the same thing as above in offering a flexible means of keeping parts of your abstraction open for later pluggability.
Consider the above abstraction modeled as a case class in Scala ..
trait CreditService
trait AuthService
case class RealPayment(creditService: CreditService,
authService: AuthService,
startDate: Date,
amount: Int)
One of the features of a Scala case class is that it generates a companion object automatically along with an apply method that enables you to invoke the class constructor as a function object ..
val rp = RealPayment( //..
is in fact a syntactic sugar for
RealPayment.apply( //
.. that gets called implicitly. But you know all that .. right ?Now for a particular module , say I would like to finalize on
PayPal
as the CreditService
implementation, so that the users don't have to pass this parameter repeatedly - just like the injector of your favorite dependency injection provider. I can do this as follows in a functional way and pass on a partially applied function to all users of the module .. scala> case class PayPal(provider: String) extends CreditService
defined class PayPal
scala> val paypalPayment = RealPayment(PayPal("bar"), _: AuthService, _: Date, _: Int)
paypalPayment: (AuthService, java.util.Date, Int) => RealPayment = <function>
Note how the Scala interpreter now treats
paypalPayment
as a function from (AuthService, java.util.Date, Int) => RealPayment
. The underscore acts as the placeholder that helps Scala create a new function object with only those parameters. In our case the new functional takes only three parameters for whom we used the placeholder syntax. From your application point of view what it means is that we have closed the abstraction partially by finalizing the provider for the CreditService
implementation and left the rest of it open. Isn't this precisely what the Guice injector was doing above injecting some of the objects at module startup ?Within the module I can now invoke
paypalPayment
with only the 3 parameters that are still open ..scala> case class DefaultAuth(provider: String) extends AuthService
defined class DefaultAuth
scala> paypalPayment(DefaultAuth("foo"), java.util.Calendar.getInstance.getTime, 10000)
res0: RealPayment = RealPayment(PayPal(foo),DefaultAuth(foo),Sun Feb 28 15:22:01 IST 2010,10000)
Now suppose for some modules I would like to close the abstraction for the
AuthService
as well in addition to freezing PayPal
as the CreditService
. One alternative will be to define another abstraction as paypalPayment
through partial application of RealPayment
where we close both the parameters. A better option will be to reuse the paypalPayment
abstraction and use explicit function currying. Like ..scala> val paypalPaymentCurried = Function.curried(paypalPayment)
paypalPaymentCurried: (AuthService) => (java.util.Date) => (Int) => RealPayment = <function>
and closing it partially using the
DefaultAuth
implementation ..scala> val paypalPaymentWithDefaultAuth = paypalPaymentCurried(DefaultAuth("foo"))
paypalPaymentWithDefaultAuth: (java.util.Date) => (Int) => RealPayment = <function>
The rest of the module can now treat this as an abstraction that uses
PayPal
for CreditService
and DefaultAuth
for AuthService
. Like Guice we can have hierarchies of modules that injects these settings and publishes a more specialized abstraction to downstream clients.