Search code examples
scalagenericstype-systems

How to create a factory for a parameterized class?


I'm trying to build a factory for implementations of a generic trait.

Given my domain model:

trait Person

case class Man(firstName: String, lastName: String) extends Person  
case class Woman(firstName: String, lastName: String) extends Person

I created a repository for those classes like this:

trait Repository[T <: Person] {
  def findAll(): List[T]
}

class ManRepository extends Repository[Man] {
  override def findAll(): List[Man] = {
    List(
      Man("Oliver", "Smith"),
      Man("Jack", "Russel")
    )
  }
}

class WomanRepository extends Repository[Woman] {
  override def findAll(): List[Woman] = {
    List(
      Woman("Scarlet", "Johnson"),
      Woman("Olivia", "Calme")
    )
   }
 }

So far so good, some pretty simple classes. But I'd wanted to create a factory to create an instance of these repositories depending on some parameters.

object RepositoryFactory {
  def create[T <: Person](gender: String): Repository[T] = {
    gender match {
      case "man" => new ManRepository()
      case "woman" => new WomanRepository()
    }
  }
}

But this last piece won't compile. If I ommit the explicit return type of the factory, it compiles but returns a repository of type Repository[_1] instead of Repository[Man]

I can't seem to find a proper solution, do any of you guys have got some tips for me?


Solution

  • How about this?

    • Instead of using a string, use a sealed trait that knows how to create the appropriate repo
    • Use dependent types to return the correct type of repository

    Code:

    object GenderStuff {
    
      trait Person
      case class Man(firstName: String, lastName: String) extends Person
      case class Woman(firstName: String, lastName: String) extends Person
    
      // Note that Repository has to be covariant in P.
      // See http://blogs.atlassian.com/2013/01/covariance-and-contravariance-in-scala/ for explanation of covariance.
      trait Repository[+P <: Person] {
        def findAll(): List[P]
      }
    
      class ManRepository extends Repository[Man] {
        override def findAll(): List[Man] = {
          List(
            Man("Oliver", "Smith"),
            Man("Jack", "Russel")
          )
        }
      }
    
      class WomanRepository extends Repository[Woman] {
        override def findAll(): List[Woman] = {
          List(
            Woman("Scarlet", "Johnson"),
            Woman("Olivia", "Calme")
          )
        }
      }
    
      sealed trait Gender {
        type P <: Person
        def repo: Repository[P]
      }
      case object Male extends Gender {
        type P = Man
        def repo = new ManRepository()
      }
      case object Female extends Gender {
        type P = Woman
        def repo = new WomanRepository()
      }
    
      object RepositoryFactory {
        def create(gender: Gender): Repository[gender.P] = gender.repo
    
        // or if you prefer you can write it like this 
        //def create[G <: Gender](gender: G): Repository[G#P] = gender.repo
      }
    
      val manRepo: Repository[Man] = RepositoryFactory.create(Male)
      val womanRepo: Repository[Woman] = RepositoryFactory.create(Female)
    }
    

    Of course, this makes the factory object pretty pointless - it's easier to just call Male.repo directly :)