6

Let us assume we have a trait T. What is the best way to achieve the following:

  • Everybody who writes an implementation of T should be forced to provide a possibility that allows a parameter-free initialization of T, i.e., we probably have to enforce the implementation of a configurable factory.
  • All logic/data that only depends on the actual initialization parameters (of a certain implementation A of T) should be handled/stored centrally, but should be available in both the factory and A.

The most simple/concise way I see to achieve this (approximately) would be to add a trait for a factory and link T to this factory:

trait T {
  val factory: TFactory
}
trait TFactory {
  def build(): T
  val description: String   // example for logic/data that only depend on the parameters
}

// example implementation:
class A(val factory: AFactory, paramA: Int, paramB: Int, paramC: Int) extends T

class AFactory(paramA: Int, paramB: Int, paramC: Int) extends TFactory {
  def build = new A(this, paramA, paramB, paramC)
  val description = f"$paramA $paramB $paramC"
}

Obviously this does not really "enforce" the implementation of a factory (as long as there is an alternative implementation available) and obviously it is possible to generate instantiations of A which link to a "wrong" TFactory. What I also don't like about this approach is the repetition of the initialization parameters. I often create yet another class AParams which again wraps all parameters (for instance to facilitate adding new parameters). Thus, I end up with three classes, which imho is a lot of boilerplate for this simple problem.

My question is whether there is a (maybe completely) different approach, which achieves the same primary goals but is more concise?

bluenote10
  • 23,414
  • 14
  • 122
  • 178

2 Answers2

1

I'm not quite sure I get the full intent of your requirements but what do you think of this behavior?

trait TFactory{
    def build():T
    val description:String
}

trait T extends TFactory

//can't declare A without build and not make it abstract
class A(paramA: Int, paramB: Int, paramC: Int) extends T {
    def build = new A(paramA, paramB, paramC)
    val description = f"$paramA $paramB $paramC"    
}

val a1 = new A(1, 4, 5)
val a2 = a1.build()

//We can give ourselves as a factory to something that expects TFactory
val factory:TFactory = a1
val a_new = factory.build()

//More likely we can just give our build method
def func(f: ()=>T) = {
    val new_t = f()
    new_t
}
val a_newer = func(a1.build)


println(a1 +": " + a1.description)
println(a2 +": " + a2.description)
println(a_new +": " + a_new.description)
println(a_newer +": " + a_newer.description)

Output:

Main$$anon$1$A@69267649: 1 4 5
Main$$anon$1$A@69b1fbf4: 1 4 5
Main$$anon$1$A@24148662: 1 4 5
Main$$anon$1$A@3f829e6f: 1 4 5
Eugene Cheipesh
  • 373
  • 1
  • 9
  • Definitely an interesting idea, thanks! A practical problem I see, is that intuitively I'll lose the notion that a factory is lightweight, while an actual implementation of `T` may be rather heavyweight. Often construction a real `A` in my use cases involves a large amount of initialization, resulting in an instance of `A` with a considerable memory footprint. I would probably often end up with an instance of `A`, which I would never really use in its actual sense of being a `T`, but simply as factory with an unnecessary overhead. But maybe that is the price to pay for the simplification. – bluenote10 Dec 12 '13 at 14:54
  • It seems like you need to use the constructor parameters of T in your factory, so I don't see how you could have such a factory without having an instance of T. Not unless, as you say, you wrap the parameters in a class that could be provided to both a factory and a constructor of subclass of T. – Eugene Cheipesh Dec 12 '13 at 15:28
  • Speaking purefly from design perspective I'm not sure you need to enforce such a factory if it's not the only way to build T. Presumably you have some method that needs a factory, which can be just a function: `MakeTsAndDoUsefulThings(factory: ()=>T)`. Then in the future if I as a client of you code make `SonOfT` and find that I need to use that function I will implment a factory for `SonOfT`, probably in my companion object and make this call: `MakeTsAndDoUsefulThings(SonOfT.defaultFactory)` If I never need to use this method, I will never need to make the factory, which seems ok. – Eugene Cheipesh Dec 12 '13 at 16:03
1

Add a representation type parameter:

trait Factory[Prod] {
  def build(): Prod
}

trait Prod[Repr] {
  def factory: Factory[Repr]
}

Or, if you want to "enforce" that the type remains the same (I wouldn't do that unless you gain something from it):

trait Prod[Repr <: Prod[Repr]] {
  def factory: Factory[Repr]
}

Then:

case class AConfig(a: Int, b: Int)

case class A(config: AConfig) extends Prod[A] {
  def factory = AFactory(config)
}

case class AFactory(config: AConfig) extends Factory[A] {
  def build() = A(config)
}

val f0 = AFactory(AConfig(1, 2))
val p0 = f0.build()
val f1 = p0.factory
val p1 = f1.build()
assert(p0 == p1)
0__
  • 66,707
  • 21
  • 171
  • 266