Reputation: 19
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.
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
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
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