Reputation: 575
I have an architectural problem, more precisely, a suboptimal situation.
For an adaptable test environment, there is a context that is updated by a range of definition methods, which each define different entities, i.e. alter the context. For simplicity, the definitions here will just be integers, and the context a growing Seq[Int].
trait Abstract_Test_Environment {
def definition(d: Int): Unit
/* Make definitions: */
definition(1)
definition(2)
definition(3)
}
This idea is now implemented by a consecutively altered “var” holding the current context:
trait Less_Abstract_Test_Environment extends Abstract_Test_Environment {
/* Implement the definition framework: */
var context: Context = initial_context
val initial_context: Context
override def definition(d: Int) = context = context :+ d
}
Since the context must be set before “definition” is applied, it cannot be set by variable assignment in the concluding class:
class Concrete_Test_Environment extends Less_Abstract_Test_Environment {
context = Seq.empty
}
An intermediate “initial_context” is required but a plain overriding does not do the job either:
class Concrete_Test_Environment extends Less_Abstract_Test_Environment {
override val initial_context = Seq.empty
}
The only viable solution seems to be an early initialization, which most likely is the purpose this feature has been created for:
class Concrete_Test_Environment extends {
override val initial_context = Seq.empty
} with Less_Abstract_Test_Environment
HOWEVER, our setting still fails because when “definition” is applied in “Abstract_Test_Environment”, the VAR “context” in “Less_Abstract_Test_Environment” is still not bound, i.e. null. Whereas the def “definition” is “initialized on demand” in “Less_Abstract_Test_Environment” (from “Abstract_Test_Environment”), the var “context” is not.
The “solution” I came up with is merging “Abstract_Test_Environment” and “Less_Abstract_Test_Environment”. This is not what I wanted since it destroys the natural separation of interface/goal and implementation, which has been realized by the two traits.
Do you see any better solution? I am sure Scala can do better.
Upvotes: 0
Views: 291
Reputation: 6094
You can employ the idea of a whiteboard, which contains only data, which is shared by a number of traits which contain only logic (not data!). See below some untested code off the cuff:
trait WhiteBoard {
var counter: Int = 0
}
trait Display {
var counter: Int
def show: Unit = println(counter)
}
trait Increment {
var counter: Int
def inc: Unit = { counter = counter + 1 }
}
Then you write unit tests like this:
val o = new Object with Whiteboard with Display with Increment
o.show
o.inc
o.show
Doing this way, you separate definition of the data from places where the data is required, which basically means that you can potentially mix in traits in any order. The only requirement is that the whiteboard (which defines data) is the first trait mixed in.
Upvotes: 0
Reputation: 40510
One possibility is to make your intermediate trait a class:
abstract class Less_Abstract_Test_Environment(var context: Context = Seq.empty) extends Abstract_Test_Environment {
override def definition(d: Int) = context = context :+ d
}
You can now subclass it, and pass different initial contexts in as parameters to constructor.
You can do this at the "concrete" level too, if you'd rather have the intermediate as a trait:
trait Less_Abstract_Test_Environment extends Abstract_Test_Environment {
var context: Context
override def definition(d: Int) = context = context :+ d
}
class Concrete_Test_Environment(override var context: Context = Seq.empty) extends Less_Abstract_Test_Environment
What would be even better though is using functional approach: context
should be a val
, and definion
should take the previous value, and return the new one:
trait Abstract {
type Context
def initialContext: Context
val context: Context = Range(1, 4)
.foldLeft(initialContext) { case (c, n) => definition(c, n) }
def definition(context: Context, n: Int): Context
}
trait LessAbstract extends Abstract {
override type Context = Seq[Int]
override def definition(context: Context, n: Int) = context :+ n
}
class Concrete extends LessAbstract {
override def initialContext = Seq(0)
}
Upvotes: 1
Reputation: 53358
Simple solution: Do not initialize your object during its creation, except you are in the bottom level class. Instead, add an init
method, which contains all of the initialization code and then call it either in the most bottom level class (which is safe, since all parent classes have already been created) or wherever the object is created.
Side effect of the whole thing is that you can even override the initialization code in a subclass.
Upvotes: 1