Sunday, March 11, 2012

Dependency injection using scala traits

Consider the following code example which does not depend on any kind of configuration system. We've just defined a trait, XConfiguration, which describes how we can get simple configuration data.
trait XConfigurable {
  def getSetting(key:String):Option[String] = None
}

trait XPlugin extends XConfigurable {
  def domain:String
}

trait XSniffPlugin extends XPlugin {
  val period=getSetting("period") map {_.toLong} getOrElse 5000L
  val periodInS=period/1000
}

abstract class XSSHPlugin extends XSniffPlugin {
  val host = getSetting("host") getOrElse "127.0.0.1"
  val username = getSetting("username") getOrElse util.Properties.userName
  val password = getSetting("password") 
  val port = getSetting("port") map {_.toInt} getOrElse 22
  def cmd:String
}

case class XTopPlugin(domain:String) extends XSSHPlugin {
  val ignoreKeywords = getSetting("ignoreKeywords")
  def cmd="top -c -b -d %d".format(periodInS)
  override def toString = 
    "top each %ds through ssh to %s%s@%s".format(
        period/1000, username, password map {":"+_} getOrElse "", host
        )
}
Now I would like to modify XTopPlugin behavior in order to let it takes its configuration through typesafe config library, BUT without modifying anything in the code.

The idea here is to create a new trait which inheritates from XConfigurable :
object PlayWithTrait {
  import com.typesafe.config.Config
  import com.typesafe.config.ConfigFactory
  import com.typesafe.config.ConfigException.{Missing,WrongType}
    
  private lazy val context = ConfigFactory.parseString("""
        |Defaults {
        |  period=5000
        |}
        |SSHPlugin = ${Defaults}
        |SSHPlugin = {
        |  host:"127.0.0.1"
        |  port:22
        |}
        |TopPlugin = ${SSHPlugin}
        |TopPlugin = {
        |  ignoreKeywords:"tty$"
        |} """.stripMargin).resolve()

  trait XConfigurableUsingConfig extends XConfigurable {
    def config:Config
    override def getSetting(key:String):Option[String] = {
      try {
        Some(config.getString(key))
      } catch {
        case x:Missing => None
        case x:WrongType => None
      }
    }
  }

...
And then we'll take advantage of scala trait feature which allow us to stack a new behavior to an object at instanciation time :
  def main(args: Array[String]) {
    import collection.JavaConversions._
    
    val localCfg = Map(
        "host" -> "192.168.2.1",
        "username" -> "test",
        "password" -> "testtest"
        )
    
    val topConfig = ConfigFactory.parseMap(localCfg).withFallback(context.getConfig("TopPlugin"))
    
    val top = new XTopPlugin("myserver") with XConfigurableUsingConfig {def config=topConfig}
    
    println(top)
  }
So now our XTopPlugin instance is taking its configuration through typesafe "Config" library, using a kind of closure to give the configuration context (topConfig).


One can object that it is less flexible than using for example spring and its xml configuration files, but remember that scala compiler can be easily embedded in any application, so you just have to use a scala configuration file which will get compiled at runtime... it will bring you very awesome dependencies injection features using pure scala.

No comments:

Post a Comment