Advanced Object-Oriented Programming in Kotlin

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

Lesson 03: Single Responsibility & Open-Closed Principles

Implementing Single Responsibility Principle

Episode complete

Play next episode

Next
Transcript

The Single Responsibility Principle states that a class should have only one responsibility and only one reason to change. 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 SingleResponsibilityPrinciple.kts in the Starter folder for Lesson 3.

The app can add items to a cart, create an order, process payment, generate an invoice, and send a confirmation email. Run the code to see the app in action:

- You have 2 order items, at the cost of $2000.0
Order created.
Credit card payment was processed with a total amount of $2000.0.
Payment successful.
Invoice generated...
Email sent: Order confirmed.

All looks good until you realize that you frequent the OrderService class for almost every update to every feature that isn’t directly related to an order. Change the message printed when an invoice is generated to: Invoice generated successfully ....

You realize this change has nothing to do with an order, yet the OrderService was updated. The same thing happens when you decide to update the message generated by the email confirmation feature.

This is a violation of the single responsibility principle. To fix it, you’ll refactor OrderService so that other classes handle things unrelated to it.

Reviewing OrderService, you can separate notifications, payments, invoices, and logging into separate services.

Create an interface for all these features at the part marked // TODO: Fix this class breaking the SRP principle:

interface NotificationService {
  fun sendConfirmationEmail()
}

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

interface InvoiceService {
  fun generateInvoice(shoppingCart: ShoppingCart)
}

interface LoggingService {
  fun logOrderActivity(activity: String)
}

It’s always good practice to code interface rather than concrete implementation. This helps make your code testable and maintainable. Now, provide the actual implementation just below the above code:

class EmailNotificationService : NotificationService {
  override fun sendConfirmationEmail() {
    // Logic to send confirmation email
    println("Email sent: Order confirmed.")
  }
}

class CreditCardPaymentService : PaymentService {
  override fun processPayment(shoppingCart: ShoppingCart): Boolean {
    // Logic to process credit card payment
    println(
      "Credit card payment was processed with a total amount of $${
        shoppingCart.getTotalOrderPrice()
      }."
    )
    return true
  }
}

class InvoiceGenerationService : InvoiceService {
  override fun generateInvoice(shoppingCart: ShoppingCart) {
    // Logic to generate invoice
    println("Invoice generated...")
  }
}

class ConsoleLoggingService : LoggingService {
  override fun logOrderActivity(activity: String) {
    // Logic to log order activity to console
    println(activity)
  }
}

The implementation is currently the same as OrderService. Each service class has a single responsibility, making the code modular and easier to maintain. Now, refactor OrderService to use these classes to implement the createOrder feature:

class OrderService (
  private val notificationService: NotificationService,
  private val paymentService: PaymentService,
  private val invoiceService: InvoiceService,
  private val loggingService: LoggingService
) {
  fun createOrder(shoppingCart: ShoppingCart) {
    // Business logic for creating an order
    loggingService.logOrderActivity("Order created.")

    // Payment processing
    if (paymentService.processPayment(shoppingCart)) {
      loggingService.logOrderActivity("Payment successful.")

      // Invoice generation
      invoiceService.generateInvoice(shoppingCart)

      // Notification
      notificationService.sendConfirmationEmail()
    } else {
      loggingService.logOrderActivity("Payment failed.")
    }
  }
}

You provided the classes to OrderService via its constructor. Then, you replaced each of the tasks with the appropriate class.

All you’ve got to do now is provide these classes to the OrderService in main. Update the instantiation code like this:

// Creating instances of individual services
val emailNotificationService: NotificationService = EmailNotificationService()
val creditCardPaymentService: PaymentService = CreditCardPaymentService()
val invoiceGenerationService: InvoiceService = InvoiceGenerationService()
val consoleLoggingService: LoggingService = ConsoleLoggingService()

// Creating an OrderService with the individual services
val orderService = OrderService(
  emailNotificationService,
  creditCardPaymentService,
  invoiceGenerationService,
  consoleLoggingService
)

Rerun the app to ensure it works the same as before.

- You have 2 order items, at the cost of $2000.0
Order created.
Credit card payment processed with a total amount of $2000.0.
Payment successful.
Invoice generated...
Email sent: Order confirmed.

Update the message from the invoice generation feature to include “successfully”:

println("Invoice generated successfully...")

This time, no order class was tampered with except the class responsible for the feature.

In the next segment, you’ll learn about the Open-Closed Principle.

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