Factories are useful when we need to create multiple instances of a class at run-time, usually providing some parameters, but still without using new
explicitly; we want to make some complex object creation abstract. The created object may depend both on runtime-provided data (the parameters), and some other “services”.
MacWire 0.3 adds support for some factories usage scenarios, while others can be implemented in pure Scala.
TL;DR:
- MacWire 0.3 adds support for wiring using parameters of the method enclosing the
wire[]
macro - traditional approaches to defining injectable factories are verbose
- but using Scala’s nested case classes it is possible to define factories in a simple, compact and readable way
As a running example, our aim will be to create a wrapper for a User
domain object. We will want to wrap the user with a PriceCalculator
, which calculates product prices taking into account user-specific discounts. To read the discounts, the calculator needs to access the database, using an instance of the DatabaseAccess
class.
Hence ideally, we would want a partially-wired PriceCalculator
, which has the database access provided, and which can be easily created for any given user. How to implement this using Scala and MacWire?
Factories in modules
Firstly, factories may be parts of the “modules” which contain the wired instances. In such case, instead of a val
, we should use a def
.
The new feature in MacWire 0.3 is that the enclosing method’s parameters are also used for wiring.
For example:
case class User
class DatabaseAccess
class PriceCalculator(databaseAccess: DatabaseAccess, user: User)
trait ShoppingModule extends Macwire {
lazy val databaseAccess = wire[DatabaseAccess]
def priceCalculator(user: User) = wire[PriceCalculator]
}
In this case the macro will expand the code to:
trait ShoppingModule extends Macwire {
lazy val databaseAccess = new DatabaseAccess
def priceCalculator(user: User) = new PriceCalculator(databaseAccess, user)
}
Note that this would also work if the module was nested in the def; hence we can create whole wired object graphs, using method parameters for wiring (“sub-modules”).
As in factories the parameters often represent data, there may be several parameters of the same type (this includes primitives). That’s why when wiring using enclosing method parameters, if multiple matches for a type are found, an additional by-name matching step is added.
Injectable factories
This works very well in modules, but what if we need to pass the factory as a parameter to another class? For example, if we have a SpecialOfferMailer
, which needs to create per-user PriceCalculator
s?
We could pass in a function object, and declare the dependency as follows:
class SpecialOfferMailer(priceCalculator: User => PriceCalculator)
This can be also viewed as a partially applied PriceCalculator
constructor.
This could work well in simple cases, but has three drawbacks:
- we need to repeat the whole function signature whenever we declare the dependency (however a type alias could help here)
- we loose parameter names, which can cause code readability problems if our factory accepts primitives or multiple parameters of the same type
- special support from MacWire would be needed to automatically convert
def
s to function objects, or a separate function object val would have to be defined manually.
We could also take the traditional route of defining a separate factory trait, e.g.:
class PriceCalculator(databaseAccess: DatabaseAccess, user: User)
object PriceCalculator {
trait Factory {
def create(user: User): PriceCalculator
}
}
class SpecialOfferMailer(priceCalculatorFactory: PriceCalculator.Factory) { ... }
then we could also have some (not yet implemented) support for wiring such factories using MacWire, e.g.:
trait ShoppingModule {
lazy val priceCalculatorFactory
= wireFactory[PriceCalculator, PriceCalculator.Factory]
...
}
which would expand to:
trait ShoppingModule {
lazy val priceCalculatorFactory = new PriceCalculator.Factory {
// the wire here uses values from the method parameters and from the
// outer module
def create(user: User) = wire[PriceCalculator]
}
...
}
However again, this has drawbacks:
- pretty verbose – separate trait
- needs special-case support for auto-wiring
- we need to repeat the factory parameters in the
create
def and in the class constructor
Scala Factories
There is however another way – using case classes in a bit untypical manner we can have an elegant factory implementation. For example:
class PriceCalculatorFactory(databaseAccess: DatabaseAccess) {
case class create(user: User) {
// methods
}
}
class SpecialOfferMailer(priceCalculatorFactory: PriceCalculatorFactory)
// Nothing special here. Just plain ol' MacWire
trait ShoppingModule extends Macwire {
lazy val databaseAccess = wire[DatabaseAccess]
lazy val priceCalculatorFactory = wire[PriceCalculatorFactory]
lazy val specialOfferMailer = wire[SpecialOfferMailer]
}
Note that using this approach the service-parameters and the data-parameters are nicely separated (former being the main class parameters, latter being the nested case class parameters). Also no parameter list definition is repeated! And we don’t need any special support for auto-wiring.
Because the nested class is a case class, using create
looks like a method invocation, while in fact it is a constructor of a new object, e.g.:
class SpecialOfferMailer(...) {
def mailOfferOfTheDay(user: User) {
val priceCalculator = priceCalculatorFactory.create(user)
products.foreach { product =>
mailOffer(user, product, priceCalculator.price(product)) }
}
}
The type of the per-user price calculator object is PriceCalculatorFactory#create
. This is a bit ugly, especially if need to pass it around, but we could add e.g. to the package object a type alias:
type PriceCalculator = PriceCalculatorFactory#create
The sources for MacWire are available on GitHub under the Apache2 license; and the binary release is in the central repository.
Have fun, and let me know what do you think about the Scala-factories implementation!
comments powered by Disqus