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)