Inject all implementations of a certain trait/clas

2019-07-28 17:13发布

问题:

I have

trait Builder[T, K] {
    def build(brick: T) : K

For that trait, i have multiple implementations...

class StringBuilder extends Builder[Foo, String] { ... }
class HouseBuilder  extends Builder[Baa, House]  { ... }
class BaaBuilder    extends Builder[Baz, Int]    { ... }

Depending on a given Type, i would like to choose from one implementation. Something like this (Pseudo-code):

class BuildingComponent @Inject()(builder: Set[Builder]){
    def doIt(item: Any) = {
       item match {
          case _: Foo => builder.filter(Foo).build(item)
          case _: Baa => builder.filter(Baa).build(item)
          case _: Baz => builder.filter(Baz).build(item)
    }
}

So 2 point:

  1. how could i inject all implementations of the trait "Builder"?? I found a bunch of questions that go in the same direction (using multibinder, TypeLiteral etc. but none of them facing the problem of injecting all implementations. Its just about "how to inject a specific implementation") I know how to bind multiple instances using multibinder; but not if it is a generic class...

  2. In the end i would like to use kind of facade-pattern. having one "builder" to inject, that gets all implementations injected and knows what builder is needed (see match-case-fracment above). But instead of using match-case, i had an eye on the MapBinder. Something like binding the Builder-implementations to a Map, that uses the class as a key.

e.g. (Pseudo-code)

class BuildingComponent @Inject()(builder: Map[Class,Builder]){
  def doIt(item: Any) = {
     builder.get(item.class).build(item)
  }
}

回答1:

Guice initialize only classes it knows about. So you can inject all implementations into your facade and order them as you want. You will need to change this facade each time you add new implementation.. So not that good..

Alternative

To inform guice about your implementations dynamically you need some reflection. You can either use standard scala if you can have your Builder as sealed trait (example of getting all subclasses you can find here) or with thirdparty library (e.g. reflections).

I will explain last case

You will need imports in your build.sbt:

libraryDependencies ++= Seq(
  "com.google.inject.extensions" % "guice-multibindings" % "<your guice version>",
  "org.reflections" % "reflections" % "0.9.11")

You will need to create module and notify guice about it, e.g. in case of play framework you will need to put in application.conf

play.modules.enabled += "com.example.MyModule"

Now, I assume that you are able to put all your implementations into same package (you can check docs how to get all implementations in other cases). Let's say it is com.example.builders. Also in my example I assume that you are able to move type parameters T and K into type aliases (with generics it will be a bit more tricks with binding and injecting - you can try to find this way yourself). And your builder will be:

trait Builder {
  type T
  type K
  def build(brick: T) : K
}

Now to your module MyModule:

package com.example

import com.google.inject.AbstractModule
import com.google.inject.multibindings.Multibinder
import org.reflections.Reflections


class MyModule extends AbstractModule {
  override def configure(): Unit = {
    import scala.collection.JavaConverters._
    val r = new Reflections("com.example.builders")
    val subtypes = r.getSubTypesOf(classOf[Builder])

    val executorBinder = Multibinder.newSetBinder(binder(), classOf[Builder])
    subtypes.asScala.foreach {clazz =>
      executorBinder.addBinding().to(clazz)
    }
  }
}

Finally, you can inject builders: java.util.Set[Builder] where you need it.

Update (added example how to handle typed implementation)

As example: you will need additional class to keep your brick type info. I used abstract class as soon as Builder is trait it should be ok

import scala.reflect.runtime.universe._

trait Builder[T, K] {
  def build(brick: T): K
}

abstract class TypeChecker[T: TypeTag] {
  this: Builder[T, _] =>

  def isDefinedAt[C: TypeTag](t: C) = {
      typeOf[C] =:= typeOf[T]
  }
}

class Foo
class Baa
class House

// first builder implementation
class StringBuilder
  extends TypeChecker[Foo]
    with Builder[Foo, String] {
  override def build(brick: Foo) = {
    println("StringBuilder")
    ""
  }
}
// second builder implementation
class HouseBuilder
  extends TypeChecker[Baa]
    with Builder[Baa, House] {
  override def build(brick: Baa) = {
    println("HouseBuilder")
    new House
  }
}

// our set of builders
val s: Set[Builder[_, _] with TypeChecker[_]] = Set(
  new StringBuilder,
  new HouseBuilder
)


// here we check and apply arrived brick on our set of builders
def check[T: TypeTag](t: T) =
  s.filter(_.isDefinedAt(t)).
    foreach {b => b.asInstanceOf[Builder[T, _]].build(t)}


check(new Foo)
check(new Baa)