1
votes

Assume I have a List of possibles Credit Card

val availableCreditsCard = List(MasterCardCreditCard, VisaCreditCard)

Both those classes extends a trait called CreditCard

Now, I want to create a method, called isValid that accept the trait CreditCard

Something like:

  def isValid(creditCard: CreditCard): Boolean = {
    creditCard match {
      case t: MasterCardCreditCard => MasterCardCreditCardPayment.isValid(t)
      case s: VisaCreditCard => VisaCreditCardPayment.isValid(s)
    }
  }

Both MasterCardCreditCardPayment and VisaCreditCardPayment extends a trait called CreditCardPayment.

So, now, my question is, what if I want to add a new possible CreditCard (Amex), with his own CreditCardPayment (AmexCreditCardPayment), but just change the list availableCreditsCard and doesn't touch the match/case inside the isValid method?

Is it possible to create dinamically this match/case based on the list availableCreditsCard?

EDIT

This is the CreditCardPayment trait.

trait CreditCardPayment[CreditCard] {
  def isValid(creditCard: CreditCard): Boolean
  def checkCVV(creditCard: CreditCard): Boolean
}

and an example of VisaCreditCardPayment

object VisaCreditCardPayment extends CreditCardPayment[VisaCreditCard] {

  override def isValid(creditCard: VisaCreditCard): Boolean = {
    val listValidCreditCard = loadFile()
    listValidCreditCard.contains(creditCard)
  }

  //Implemented because it the trait, not necessary
  override def checkCVV(creditCard: VisaCreditCard): Boolean = {
    val listCreditCard = loadFile()
    val cvvToCheck = creditCard.cvv
    listCreditCard.exists(_.cvv == cvvToCheck)
  }
}

Now, your suggestion is, inside the isValid method, use a case t => CreditCardPayment.isValid(t). This is not even actual possible, since CreditCardPayment is a trait :(

2
If you are changing the requirement I think you will have to change the code too and you should to avoid any bug.Raman Mishra
Yup, I'm completely agree with this, and thinking by myself, I was like, well, yeah, If I add a new type of payments, I should change the code itself to be extra sure that everything is going to work properly, but still, do you think this is possible in any way?Tizianoreica
Just make .isValid a method on CreditCard. Then you can just do creditCard.isValid() without any auxiliary code to keep in synch.Dima
If you want to perform some common functionality on all the other type of cards you can write _ case in your match case.Raman Mishra
@AntonioCalì you don't implement it on the trait, just declare it there. Then implement for each subclass.Dima

2 Answers

5
votes

If you want it "dynamic", just use inheritance rather than implementing it "on the side":

   sealed trait CreditCard {
      def isValid: Boolean
   }

   class MasterCard extends CreditCard {
      def isValid = MasterCardCreditCardPayment.isValid(this)
   }
   class Visa extends CreditCard {
      def isValid = VisaCreditCardPayment.isValid(this)
   }

Now, you can just do def isValid(c: CreditCard) = c.isValid, no need for any match statements to keep in sync.

And when you add Amex:

   class Amex extends CreditCard {
       def isValid = AmexCreditCardPayment.isValid(this)
   }

The isValid(amex) will just work, nothing needs to be updated.

1
votes

Complementing Dima's answer. Since, you always want to delegate the isValid logic to the appropriate Payment. I will present you two ways to do not redefine the method in each sub-class.

One method using F-Bounded Polymorphism, which will require quite a boilerplate code for each sub-class, and it is not completely typesafe - thus may not worth the work.
And the second using typeclasses, which will be more typesafe, and will require less boilerplate for each new card.
For a more detailed discussion about F-Bounded vs Typeclasses, read this article.

Using F-Bounded

sealed trait CreditCardPayment[CC <: CreditCard[CC]] {
  def isValid(creditCard: CC): Boolean
  def checkCVV(creditCard: CC): Boolean
}

object VisaCreditCardPayment extends CreditCardPayment[VisaCreditCard] {
  private final val validCreditCards: List[VisaCreditCard] = ???

  override def isValid(creditCard: VisaCreditCard): Boolean =
    validCreditCards.contains(creditCard)

  override def checkCVV(creditCard: VisaCreditCard): Boolean =
    validCreditCards.exists(_.cvv == creditCard.cvv)
}

object MasterCreditCardPayment extends CreditCardPayment[MasterCreditCard] {
  private final val validCreditCards: List[MasterCreditCard] = ???

  override def isValid(creditCard: MasterCreditCard): Boolean =
    validCreditCards.contains(creditCard)

  override def checkCVV(creditCard: MasterCreditCard): Boolean =
    validCreditCards.exists(_.cvv == creditCard.cvv)
}

sealed trait CreditCard[CC <: CreditCard[CC]] { self: CC =>
  def paymentMethod: CreditCardPayment[CC]

  def cvv: String

  final def isValid: Boolean =
    paymentMethod.isValid(this)
}

final class VisaCreditCard (override val cvv: String) extends CreditCard[VisaCreditCard] {
  override final val paymentMethod: CreditCardPayment[VisaCreditCard] = VisaCreditCardPayment
}

final class MasterCreditCard (override val cvv: String) extends CreditCard[MasterCreditCard] {
  override final val paymentMethod: CreditCardPayment[MasterCreditCard] = MasterCreditCardPayment
}

Using Typeclasses

sealed trait CreditCardPayment[CC <: CreditCard] {
  def isValid(creditCard: CC): Boolean
  def checkCVV(creditCard: CC): Boolean
}

sealed trait CreditCard {
  def cvv: String
}

// Provides the 'isValid' & 'checkCVV' extension methods to any CredictCard.
implicit class CreditCardOps[CC <: CreditCard](val self: CC) extends AnyVal {
  def isValid(implicit payment: CreditCardPayment[CC]): Boolean =
    payment.isValid(self)

  def checkCVV(implicit payment: CreditCardPayment[CC]): Boolean =
    payment.checkCVV(self)
}

final class VisaCreditCard (override val cvv: String) extends CreditCard

object VisaCreditCard {
    final implicit val VisaCreditCardPayment: CreditCardPayment[VisaCreditCard] = new CreditCardPayment[VisaCreditCard] {
      final val validCreditCards: List[VisaCreditCard] = ???

      override def isValid(creditCard: VisaCreditCard): Boolean =
        validCreditCards.contains(creditCard)

      override def checkCVV(creditCard: VisaCreditCard): Boolean =
        validCreditCards.exists(_.cvv == creditCard.cvv)
    }
}

final class MasterCreditCard (override val cvv: String) extends CreditCard

object MasterCreditCard {
  final implicit val MasterCreditCardPayment: CreditCardPayment[MasterCreditCard] = new CreditCardPayment[MasterCreditCard] {
    final val validCreditCards: List[MasterCreditCard] = ???

    override def isValid(creditCard: MasterCreditCard): Boolean =
      validCreditCards.contains(creditCard)

    override def checkCVV(creditCard: MasterCreditCard): Boolean =
      validCreditCards.exists(_.cvv == creditCard.cvv)
  }
}

Using the typeclass approach you can also define the isValid method for CreditCards as a function instead.
(This way you do not need to define and import the CreditCardOps implicit / value class).

def isValid[CC <: CreditCard](cc: CC)(implicit payment: CreditCardPayment[CC]): Boolean =
  payment.isValid(cc)