Dependency Injection as Function Currying
Join the DZone community and get the full member experience.
Join For FreeDependency Injection is one of the techniques that I use regularly when
I am programming in Java. It's a nice way of making an application
decoupled from concrete implementations and localize object creation
logic within specific bootstrapping modules. Be it in the form of
Spring XML or Guice Modules, the idea is to keep it configurable so
that specific components of your application can choose to work with
specific implementations of an abstraction.
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.
Opinions expressed by DZone contributors are their own.
Comments