Advanced Object-Oriented Programming in Kotlin

May 22 2024 · Kotlin 1.9, Android 14, Kotlin Playground

Lesson 04: Liskov Substitution, Interface Segregation & Dependency Inversion

Implementing Liskov Substitution Principle

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll redesign your payment processing functionality to include rewards while following the Liskov Substitution principle.

Start by opening the Kotlin playground in your browser. Download the course material from the GitHub link at the side of the video. Copy and paste the code from LiskovSubstitution.kts in the Starter folder for Lesson 4.

As it stands, your app recognizes two kinds of cards for payments: credit and debit cards. Run the app to see them at work:

Validating debit card ...
Running fraud checks on debit card ...
Handling debit card payment ...
Saving payment details to database ...

Validating credit card ...
Running fraud checks on credit card ...
Handling credit card payment ...
Saving payment details to database ...

PaymentMode defines these modes of payment. It has validate, runFraudChecks, and handlePayment abstract methods that the cards comply with:

abstract class PaymentMode {
  // Validate the mode of payment
  abstract fun validate()

  // Check for fraud
  abstract fun runFraudChecks()

  // Perform debit transaction to seal payment
  abstract fun handlePayment()
}

Look at how CreditCard and DebitCard implement PaymentMode:

internal class CreditCard : PaymentMode() {
  override fun validate() {
    println("Validating credit card ...")
  }

  override fun runFraudChecks() {
    println("Running fraud checks on credit card ...")
  }

  override fun handlePayment() {
    println("Handling credit card payment ...")
  }
}

And here’s how the PaymentProcessor ties them together:

class PaymentProcessor {
  fun process(orderDetails: OrderDetails, paymentMode: PaymentMode) {
    try {
      paymentMode.validate()
      paymentMode.runFraudChecks()
      paymentMode.handlePayment()
      saveToDatabase(orderDetails, paymentMode)
    } catch (e: Exception) {
      // Exception handling with specific exception type
    }
  }

  private fun saveToDatabase(orderDetails: OrderDetails, paymentMode: PaymentMode) {
    println("Saving payment details to database ...")
  }
}

class OrderDetails {}

You decide to add rewards as a mode of payment in the app. Loyal customers may earn rewards or points that they can later use to purchase items in the app. Since it’s a type of payment, you decide to make it implement PaymentMode. But then you immediately realize that runFraudChecks doesn’t apply to rewards. Throwing an exception or any other kind of behavior could potentially break the app since the Reward class won’t behave as expected. This also means you can’t safely use it wherever PaymentMode is used, thus violating the Liskov substitution principle.

To fix this, rewrite the PaymentMode to enforce functionality common to all implementations:

interface IPaymentMode {
  fun validate()

  fun handlePayment()
}

Since you’re certain that every form of payment now in and in the future will need to be validated, you make it the primary requirement for all types of payment.

Update the payment processor to call only the guaranteed functions when handling payments:

class PaymentProcessor {
  fun process(orderDetails: OrderDetails, paymentMode: IPaymentMode) {
    try {
      paymentMode.validate()
      paymentMode.handlePayment()
      saveToDatabase(orderDetails, paymentMode)
    } catch (e: Exception) {
      // Exception handling with specific exception type
    }
  }

  private fun saveToDatabase(orderDetails: OrderDetails, paymentMode: IPaymentMode) {
    println("Saving payment details to database ...")
  }
}

PaymentProcessor‘s process function now accepts IPaymentMode instead of PaymentMode. Also, there’s no runFraudChecks() anymore.

To provide the missing behavior, which is common to cards other than payments in general, create an abstract BaseCard class:

internal abstract class BaseCard : IPaymentMode {
  abstract override fun validate()

  override fun handlePayment(){
    runFraudChecks()
  }

  abstract fun runFraudChecks()
}

Now, update CreditCard and DebitCard to inherit from the new BaseCard. Here’s how CreditCard will look:

internal class CreditCard : BaseCard() {
  override fun validate() {
    println("Validating credit card ...")
  }

  override fun runFraudChecks() {
    println("Running fraud checks on credit card ...")
  }

  override fun handlePayment() {
    super.handlePayment()
    println("Handling credit card payment ...")
  }
}

Do the same for DebitCard. Rerun the app.

Validating debit card ...
Running fraud checks on debit card ...
Handling debit card payment ...
Saving payment details to database ...

Validating credit card ...
Running fraud checks on credit card ...
Handling credit card payment ...
Saving payment details to database ...

With this modification, you can safely add a Rewards implementation without breaking the app:

internal class RewardsCard : IPaymentMode {
  override fun validate() {
    println("Validating rewards card ...")
  }

  override fun handlePayment() {
    println("Handling rewards card payment ...")
  }
}

In main(), add a new reward payment mode after // TODO: Add new Reward card:

val rewardsCard = RewardsCard()
paymentProcessor.process(orderDetails, rewardsCard)

Now, rerun the app:

Running fraud checks on debit card ...
Handling debit card payment ...
Saving payment details to database ...

Validating credit card ...
Running fraud checks on credit card ...
Handling credit card payment ...
Saving payment details to database ...

Validating rewards card ...
Handling rewards card payment ...
Saving payment details to database ...

That’s it. You’ve implemented the Liskov Substitution Principle in your e-commerce app. In the next segment, you’ll learn about the Interface Segregation Principle.

See forum comments
Cinema mode Download course materials from Github
Previous: Learning Liskov Substitution Principle Next: Learning Interface Segregation Principle