joesan
joesan

Reputation: 15385

Factory design pattern in Scala with case classes

I'm trying to implement a factory design pattern in Scala using the apply methods available on the companion object. I have the following approach.

sealed trait MyType {
  def param: String
}

case class TypeA(param: String) extends MyType
case class TypeB(param: String, anotherParam: String) extends MyType 

object MyType {
  def apply(param: String): TypeA = ???
  def apply(param, anotherParam: String): TypeB = ???
}

How do I now force the callers of the above trait to go via the companion object when creating instances of TypeA or TypeB?

Upvotes: 3

Views: 4542

Answers (3)

tabdulradi
tabdulradi

Reputation: 7596

You can move the case classes inside the companion object, and set the constructors to be private and accessed only within the companion object.

sealed trait MyType {
  def param: String
}

object MyType {
  case class TypeA private[MyType] (param: String) extends MyType
  case class TypeB private[MyType] (param: String, anotherParam: String) extends MyType 

  def apply(param: String): TypeA = TypeA(param)
  def apply(param: String, anotherParam: String): TypeB = TypeB(param, anotherParam)
}

No one would be able to instantiate the case classes directly, unless though reflection.

scala> MyType("Test")
res0: MyType.TypeA = TypeA(Test)

scala> MyType("Test", "another test")
res1: MyType.TypeB = TypeB(Test,another test)

scala> MyType.TypeA("test??")
<console>:12: error: constructor TypeA in class TypeA cannot be accessed in object $iw
              MyType.TypeA("test??")
                     ^

Upvotes: 12

cchantep
cchantep

Reputation: 9168

The trait MyType is sealed. That me others can do something like new MyType{} to instantiate it.

Then you can remove the case classes.

// No more public case classes TypeA & TypeB
object MyType {
  def apply(p: String): MyType = /* case A */ new MyType { val param = p }

  private case class InternalB(param: String, other: String) extends MyType
  def apply(param: String, anotherParam: String): MyType = InternalB(param, anotherParam)
}

At this point, it's required to use companion object to create MyType instances.

Then you can restore pattern matching for these different cases.

object MyType {
  // the apply functions, plus extractors thereafter...

  /** Extracts mandatory parameter whatever is the case. */
  def unapply(t: MyType): Option[String] = Some(t.param)

  /** Extracts both parameter, extra parameter for case B, None for other */
  def unapply(t: MyType): Option[(String, String)] = t match {
    case InternalB(mandatory, extra)/* Only possible there as private */ =>
      Some(mandatory -> extra)
    case _ => None
  }
}

// Then pattern matching can do...

val test1: Boolean = MyType("A") match {
  case MyType(param) => true
  case _ => false
}
// Will be true

val test2: Boolean = MyType("B", "extraB") match {
  case MyType(param, extra) => true
  case _ => false
}
// Will be true

val test3: Int = MyType("A") match {
  case MyType(param, extra) => 2
  case MyType(param) => 1
  case _ => 0 
}
// Will be 1

val test4: Boolean = MyType("B", "extraB") match {
  case MyType(param) => true
  case _ => false
}
// Will be true

It allows a full control over instantiation, and abstraction over implementation of cases.

Upvotes: 1

Michael Zajac
Michael Zajac

Reputation: 55569

You can simply call the apply method of the case classes themselves. There doesn't seem to be a way to prevent client code from calling TypeA.apply directly, though, as that would prevent MyType from calling it.

sealed trait MyType {
    def param: String
}

case class TypeA(param: String) extends MyType
case class TypeB(param: String, anotherParam: String) extends MyType 

object MyType {
   def apply(param: String): TypeA = TypeA(param)
   def apply(param: String, anotherParam: String): TypeB = TypeB(param, anotherParam)
}

Upvotes: 1

Related Questions