Alexander Nekrasov
Alexander Nekrasov

Reputation: 19

Scala Factory Pattern with Generic

I want to use Factory Method with Generics which can work with specific implementations. In service classes i want to have type safety but in controller to operate only know interface.

Code

I have defined different types of operation type

trait Transaction {
  val amount: BigDecimal
}
case class CreditCardTransaction(amount: BigDecimal, ccNumber: String, expiry: String) extends Transaction
case class BankTransaction(amount: BigDecimal, bankAccount: String) extends Transaction

and Services which can work with specific operation types

trait Service[T <: Transaction] {
  def transfer(transaction: T)
}

class CCService() extends Service[CreditCardTransaction] {
  override def transfer(transaction: CreditCardTransaction): Unit = println("pay with cc")
}
class TTService() extends Service[BankTransaction] {
  override def transfer(transaction: BankTransaction): Unit = println("pay with telex transfer")
}

I have created factory with concrete instances

class PaymentSystemFactory(ccService: CCService, ttService: TTService) {
  def getService(paymentMethod: String) = paymentMethod match {
    case "cc" => ccService
    case "tt" => ttService
  }
}

And parser to get specific transaction from external service

object Parser {
  def parse(service: Service[_ <: Transaction]) = service  match {
    case _: Service[CreditCardTransaction] => CreditCardTransaction(100, "Name", "01/01")
    case _: Service[BankTransaction] => BankTransaction(100, "1234")
  }
}

But that code doesn't want to compile due provided types mismatch from PaymentSystemFactory method:

object App {
  val factory = new PaymentSystemFactory(new CCService, new TTService)
  val service = factory.getService("cc") // return Service[_ >: CreditCardTransaction with BankTransaction <: Transaction]
  val transaction: Transaction = Parser.parse(service)
  service.transfer(transaction) // Failed here: Required _$1 found Transaction
}

I would be happy to avoid type erasure if possible due factory method call and wondered why that code doesn't work

Upvotes: 1

Views: 463

Answers (2)

Juh_
Juh_

Reputation: 15539

While I was making the below solution, @Mathias proposed another one that I find really nice. But I still post it as an alternative that might be interesting.

Instead of type argument (i.e. generic), you can use type member:

trait Service {
  type T <: Transaction
  def transfer(transaction: T): Unit
}
class CCService() extends Service {
  type T = CreditCardTransaction
  override def transfer(transaction: CreditCardTransaction): Unit = println("pay with cc")
}
class TTService() extends Service {
  type T = BankTransaction
  override def transfer(transaction: BankTransaction): Unit = println("pay with telex transfer")
}

// need to use asInstanceOf, I don't know how to tell scala that the type is safe
def parse(service: Service): service.T = service  match {
  case _: CCService => CreditCardTransaction(100, "Name", "01/01").asInstanceOf[service.T]
  case _: TTService => BankTransaction(100, "1234").asInstanceOf[service.T]
}

val factory = new PaymentSystemFactory(new CCService, new TTService)
val service = factory.getService("tt")
val transaction = parse(service) // transaction has type service.T
service.transfer(transaction)

Upvotes: 2

Matthias Berndt
Matthias Berndt

Reputation: 4587

What you've done here (apparently accidentally?) is to create a generalized algebraic data type, or GADT for short. If you want to find out more about this feature, that is probably a useful term to search for.

As for how to make this work: The type signature of the parse method needs to reflect that the type of the returned transaction matches service's transaction type.

Also, you can't do case _: Service[CreditCardTransaction], that won't work properly due to erasure. Use case _: CCService instead.

Try this:

object Parser {
  def parse[A <: Transaction](service: Service[A]): A = service  match {
    case _: CCService => CreditCardTransaction(100, "Name", "01/01")
    case _: TTService => BankTransaction(100, "1234")
  }
}

And you'll need to change the calling code too:

object App {
  val factory = new PaymentSystemFactory(new CCService, new TTService)
  factory.getService("cc") match {
    case service: Service[a] =>
      val transaction: a = Parser.parse(service)
      service.transfer(transaction)
  }
}

Note that the match isn't used to actually distinguish between multiple cases. Instead, its only purpose here is to give a name to the transaction type, a in this case. This is one of the most obscure features in the Scala language. When you do a pattern match on a wildcard type and use a lower-case name like a for the type parameter, then it doesn't check that the type is a (like it would for an uppercase name), but it creates a new type variable that you can use later on. In this case, it is used to declare the transaction variable, and also implicitly to call the Parser.parse method.

Upvotes: 4

Related Questions