tags:

views:

80

answers:

2

I want my client code to look somewhat like this:

    val config:Config = new MyConfig("c:/etc/myConfig.txt")
    println(config.param1)
    println(config.param2)        
    println(config.param3)

Which means that:

  • The Config interface defines the config fields
  • MyConfig is a Config implementation -- all the wiring needed is the instantiation of the desired implementation
  • Data is loaded lazily -- it should happen on first field reference (config.param1 in this case)

So, I want the client code to be friendly, with support for interchangeable implementations, with statically typed fields, hiding lazy loading. I also want it to be as simple as possible for making alternative implementations, so Config should somewhat guide you.

I am not satisfied with what I came up with so far:

trait Config {
  lazy val param1:String = resolveParam1
  lazy val param2:String = resolveParam2
  lazy val param3:Int = resolveParam3

  protected def resolveParam1:String
  protected def resolveParam2:String
  protected def resolveParam3:Int
}

class MyConfig(fileName:String) extends Config {
  lazy val data:Map[String, Any] = readConfig

  // some dummy impl here, should read from a file
  protected def readConfig:Map[String,Any] = Map[String, Any]("p1" -> "abc", "p2" -> "defgh", "p3" -> 43)

  protected def resolveParam1:String = data.get("p1").get.asInstanceOf[String]
  protected def resolveParam2:String = data.get("p2").get.asInstanceOf[String]
  protected def resolveParam3:Int = data.get("p3").get.asInstanceOf[Int]
}

I'm sure there are better solutions, that's where you can help :)

One thing I especially don't like here is that MyConfig defines an intermediate container with some arbitrary keys, and since it is Map[String, Any], I need to cast the values.

A: 

If you just want to simplify it, the "param" fields could be methods...

trait Config {
  def param1:String
  def param2:String
  def param3:Int
}

class MyConfig(fileName:String) extends Config {
  lazy val data:Map[String, Any] = readConfig

  // some dummy impl here, should read from a file
  protected def readConfig:Map[String,Any] = 
    Map[String, Any]("p1" -> "abc", "p2" -> "defgh", "p3" -> 43)

  def param1:String = data.get("p1").get.asInstanceOf[String]
  def param2:String = data.get("p2").get.asInstanceOf[String]
  def param3:Int = data.get("p3").get.asInstanceOf[Int]
}

To get rid of the casting, you could have MyConfig wrap a non-lazy inner class that's lazy loaded by MyConfig.

class MyConfig(fileName:String) extends Config {
  private class NonLazyConfig(val p1:String, p2:String, p3:int) extends Config {
      def param1 = p1
      def param2 = p2
      def param1 = p3
  }
  lazy val inner:Config = readConfig

  // some dummy impl here, should read from a file
  protected def readConfig:Config = {
    return new NonLazyConfig("abc", "defgh", 43)
  }
  def param1:String = inner.param1
  def param2:String = inner.param2
  def param3:Int = inner.param3
}
sblundy
+1  A: 

There's nothing preventing you from just making the values abstract. You cannot enforce laziness in the super-trait, but that's ok since lazy-loading is really an implementation detail anyway:

trait Config {
  val param1: String
  val param2: String
  val param3: Int
}

class MyConfig extends Config {
  lazy val param1 = readConfig().("p1")
  ...

  def readConfig(): Map[String, String] = ...
}

On a stylistic note, readConfig() should be declared and called with parens (rather than without) as it is a side-effecting method. The no-parens syntax is designed to denote pure-functional methods.

Daniel Spiewak
Wouldn't I also need the lazy val for the Map in MyConfig, to make sure I don't actually read from disk more than once? In your sample, first reference to each param will cause a read of all params.
Germán