This is a library for type-safe, boilerplate-free dynamic dependency injection for Akka actors. It offers a DI model for actors dependent on asynchronous dependencies, like:
Futurecalls- messages received from other actors
<dependency>
<groupId>com.github.wilaszekg</groupId>
<artifactId>scala-dynamic-di_2.11</artifactId>
<version>0.0.2</version>
</dependency>libraryDependencies += "com.github.wilaszekg" %% "scala-dynamic-di" % "0.0.2"
The Dependencies holds dependencies for an actor represented as Future computation. You can build dependencies for an actor using Future's, functions and calls to other actors. Dependencies class is aware of types of all dependencies it holds - let's say reflected as a tuple. Each dependency type must be unique. This allows the library to automatically compose dependencies by their type. Each dependency has a set of required types and a type it can produce. This way you can build Dependencies with fluent API and while adding a new dependency (look at requires function below) Scala compiler will check that:
- all required types for the new dependency are available
- the new produced dependency type will be unique
This means that if you break any of these rules, your code won't compile.
You tell the lubrary how to produce your actor by creating ProxyProps instance which requires a function of any arity returning Akka Props. The types of arguments of this function must be found in Dependencies you are using to conifgure your actor - otherwise the code won't compile.
val proxyProps = new ProxyProps((products: Products, promotions: Promotions) => Props(new PriceCalculator(products, promotions)))
val dependencies = Dependencies().withVal(user).withFuture(futureShop)
.requires(basketDependency)
.requires(promotionsDependency)
.requires(productsDependency)
val actorRef: ActorRef = system.actorOf(proxyProps from dependencies)First, you have to create ProxyProps for the actor you want to inject dependencies into. You need to pass a function taking ANYTHING and returning Akka Props. The ANYHTHING defines dependencies of your actor. In this case it means that the actor PriceCalculator requires two dependencies of types Products and Promotions:
import com.github.wilaszekg.scaladdi.akka.ProxyProps
new ProxyProps((products: Products, promotions: Promotions) => Props(new PriceCalculator(products, promotions)))To start building dependencies for the actor, create a Dependencies object:
import com.github.wilaszekg.scaladdi.Dependencies
val dependencies = Dependencies()It just creates empty dependencies.
To construct dependencies further you can add a defined and already known value:
val user: User = ???
val dependencies = Dependencies().withVal(user)or a a simple Future value:
val futureShop: Future[Shop] = ???
val dependencies = Dependencies().withVal(user).withFuture(futureShop)The Dependencies constructed here holds dependencies with two types: User and Shop
You can use three types od dependencies:
FutureDependency- requires values of a some types and produces aFutureof another typeActorDependency- requires values of a some types and awaits for a response of another type from a givenActorRefFunctionDependency- requires values of a some types and produces a value of another type
You create FutureDependency passing a function requiring some values and returning a Future of a value. This dependency requires User and Shop to be built and produces a Basket:
import com.github.wilaszekg.scaladdi.FutureDependency
def findBasket(user: User, shop: Shop): Future[Basket] = ???
val basketDependency = FutureDependency(findBasket _)Now, you can add this dependency to an existing Dependencies object if it already contains required types: User and Shop:
val dependencies = Dependencies().withVal(user).withFuture(futureShop)
.requires(basketDependency)FunctionDependency is very similar to FutureDependency but it uses a function producing a synchronous result. It may be a blocking call and it will be wrapped to a Future:
import com.github.wilaszekg.scaladdi.FunctionDependency
def findPromotions(shop: Shop): Promotions = ???
val promotionsDependency = FunctionDependency(findPromotions _)
val dependencies = Dependencies().withVal(user).withFuture(futureShop)
.requires(basketDependency)
.requires(promotionsDependency)ActorDependency requires an ActorRef a class of expected response message (the type of the dependency) from the actor and a function creating answer message - arguments of this function define typrs required for this dependency:
import com.github.wilaszekg.scaladdi.akka.ActorDependency
val productsDependency = ActorDependency(basketActorRef, (b: Basket) => AskForProducts(b), classOf[Products])
val dependencies = Dependencies().withVal(user).withFuture(futureShop)
.requires(basketDependency)
.requires(promotionsDependency)
.requires(productsDependency)We have built dependencies from known values user, futureShop and dyncamic dependencies basketDependency, promotionsDependency and productsDependency. Each dynamic dependency has some requirements and a produced type:
val basketDependency: FutureDependency[(User, Shop), Basket] = ...
val promotionsDependency: FunctionDependency[Tuple1[Shop], Promotions] = ...
val productsDependency: ActorDependency[Tuple1[Basket], Products] = ...The required types are reflected as tuples. The produced value is always the second type argument.
It is crucial that Dependencies keeps lazy computation model of the futures so it will not run any future until you call its run method. This method is used by the proxy actor to start fetching the dependencies.
Whenever you add a new dependency using requires method, the compile will check the following conditions:
- all required types are present
- the produced type of the new dependency is not present in dependencies yet - it allows the compiler to control the types of all dependencies
If you have a ProxyProps and Dependencies instances, you have to use them to obtain a Props instance and start a proxy actor (using Akka context or system):
val props: Props = proxyProps from dependencies
val proxyActor: ActorRef = context.actorOf(props)The proxy actor hides complexity of running future dependencies and allows you to implement simple actors code.
When you create ProxyProps instance you only need to pass a propsFunction. But you can configure it with some additional properties:
dependenciesTriesMax- how many times the proxy will try to run the future dependencies if any of the future failsreConfigureAfterTerminated- specifies what happens if the target actor is terminated. Is set totrue, the proxy will run the future dependencies once again to recover the actorsupervisionStrategy- Akka supervision strategy for the target actor - specifies how the proxy will deal with its child proxied actordependencyError- a function transforming an error to a message to be sent from the proxy to its parrent if fetching the future dependencies fails
ProxyProps provides defaults for all these properties:
ProxyProps(propsFunction, dependenciesTriesMax: Option[Int] = None,
reConfigureAfterTerminated: Boolean = true,
supervisionStrategy: SupervisorStrategy = SupervisorStrategy.defaultStrategy,
dependencyError: Throwable => Any = ProxyProps.defaultError)