0

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.

Majakovskij
  • 565
  • 4
  • 10

3 Answers3

1

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.

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
1

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)
    }
Dima
  • 39,570
  • 6
  • 44
  • 70
  • Thank you for your solution! Indeed, I did want to implement context as a val, however, the problem is that during tests I need a current context, which can change entirely (so, naturally, a var). Actual tests would look like that: [CODE] definition(1) definition(2) do_tests clear_all_definitions definition(3) definition(1) definition(4) do_other_tests clear_all_definitions definition(3) do_yet_other_tests [/CODE] How would this be realized with your approach? – Majakovskij Apr 05 '16 at 04:48
  • I don't see how it is "naturally a var" (there is nothing "natural" about vars, really). You should create a new set of classes for each test. That way there would not be any need to cleanup/mutate the context. – Dima Apr 05 '16 at 10:15
  • Hi! By the way, I did implement the definitions the way you said. Creating a new set of classes for each test is not an efficient solution in this case since the definitions would be totally repetitive like TEST1: def1, def2 test; TEST2: def1, def2, def3 test; TEST3: def1, def2, def3, def4, def5, def6 test I test by adding more and more definitions. I don't see how to realize that in a non-mutating way without becoming repetitive. – Majakovskij Apr 06 '16 at 07:27
  • [I have to add: a requirement is to allow to fully reset the context or to reverse it in one test setting. It is not at all two different tests, semantically. The tests are inherently related as I want to test what happens to the objects that are created based on the definitions in the case that these definitions fall away] – Majakovskij Apr 06 '16 at 07:30
  • The way to not become repetitive is create a function. Testing the case when the context is mutated would not be necessary if the context was immutable to begin with. – Dima Apr 06 '16 at 10:14
0

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.

Richard Gomes
  • 5,675
  • 2
  • 44
  • 50