Advanced Object-Oriented Programming in Kotlin

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

Lesson 03: Single Responsibility & Open-Closed Principles

Implementing Open-Closed Principle

Episode complete

Play next episode

Next
Transcript

The Open-Closed Principle states that a software entity should be open for extension but closed for modification. In this demo, you’ll use this principle to enhance the quality of your e-commerce app.

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 OpenClosedPrinciple.kts in the Starter folder for Lesson 3.

In your e-commerce app, CheckoutService doesn’t follow the open-closed principle. You can’t extend its capabilities without modifying the class itself. Assuming this were a library, you wouldn’t be able to add new functionality easily in a clean way.

Run the app:

Processing payment using Stripe.
Payment successful. Order has been processed.

It displays output from a fixed processor that handles the payment — Stripe.

To make your app support another payment method, like PayPal, you’re forced to update CheckoutService, risking the possibility of introducing a regression. The Open-Closed Principle states that you should be able to open CheckoutService for extension but close it for modification. Adding new capabilities shouldn’t require updates to CheckoutService.

To fix this, create a PaymentGateway interface to represent your app’s payment gateway capability. Add the following code below // TODO: Fix the violation:

interface PaymentGateway {
  fun processPayment(shoppingCart: ShoppingCart): Boolean
}

This interface provides a method every payment processor can implement to process payments.

Modify StripePaymentGateway to make it conform to the app’s payment processing interface:

class StripePaymentGateway : PaymentGateway {
  override fun processPayment(shoppingCart: ShoppingCart): Boolean {
    // Logic to process payment using Stripe
    println("Processing payment using Stripe.")
    // Actual payment processing logic with Stripe API would go here
    return true
  }
}

You can now easily create a payment gateway for PayPal too. Make the change below StripePaymentGateway:

class PaypalPaymentGateway : PaymentGateway {
  override fun processPayment(shoppingCart: ShoppingCart): Boolean {
    // Logic to process payment using PayPal
    println("Processing payment using PayPal.")
    // Actual payment processing logic with Paypal API would go here
    return true
  }
}

To put it all together, modify CheckoutService to accept the payment gateway interface. This is what makes it flexible, scalable, and easy to maintain. Instead of tight coupling it with one payment processor in a closed environment, it accepts a payment gateway interface. This opens it up to a wide variety of payment gateways to work with:

class CheckoutService(private val paymentGateway: PaymentGateway) {
  fun processOrderPayment(shoppingCart: ShoppingCart) {
    val paymentResult = paymentGateway.processPayment(shoppingCart)

    if (paymentResult) {
      println("Payment successful. Order has been processed.")
    } else {
      println("Payment failed for order.")
    }
  }
}

CheckoutService won’t need to change anymore whenever you need to use a new payment processor. The class remains the same, yet allows for extending its capabilities. This is what the Open-Closed Principle is about.

Finally, create some constant to help you choose between the type of payment gateway to use for payment:

enum class PaymentGatewayType {
  STRIPE,
  PAYPAL
}

Since CheckoutService accepts a gateway now, modify its constructor in main to accept one:

val selectedGateway = PaymentGatewayType.STRIPE

 // Creating an instance of the selected payment gateway
 val paymentGateway = when (selectedGateway) {
 PaymentGatewayType.STRIPE -> StripePaymentGateway()
 PaymentGatewayType.PAYPAL -> PaypalPaymentGateway()
 }

 // Using the CheckoutService with the selected payment gateway
 val checkoutService = CheckoutService(paymentGateway)

That’s it. Run the app, and it should be as before:

Processing payment using Stripe.
Payment successful. Order has been processed.

Now, change selectedGateway to PAYPAL and rerun the app:

Processing payment using PayPal.
Payment successful. Order has been processed.

This time, you realize you can use different payment providers. All you need to extend CheckoutService’s ability to process with more payment options is to create a payment gateway that implements the PaymentGateway interface.

That’s all for this demo. Continue to the concluding part of this lesson.

See forum comments
Cinema mode Download course materials from Github
Previous: Learning Open-Closed Principle Next: Conclusion