Home > Mobile >  Scala Factory Pattern with Generic
Scala Factory Pattern with Generic

Time:10-13

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

CodePudding user response:

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.

CodePudding user response:

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")
}

// I currently only have an old version of scala, so I had to use cast
// but I think it shouldn't be necessary with newer version of scala
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)
  • Related