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 Dependency Inversion Principle

Episode complete

Play next episode

Next
Transcript

Dependency Inversion states that high-level modules shouldn’t depend on low-level modules but on abstractions. In this demo, you’ll adhere to this principle to reduce coupling in 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 DependencyInversion.kts in the Starter folder for Lesson 4.

Your app uses a DatabaseService to persist data to the database. Run the app to see it at work:

Processing order...
Persist order amounting to 2400.0 to an SQLite Database.

You’re currently using an SQLite database. But then you need to swap that for an in-memory database during tests. With the current design, there’s no clean way to achieve this. You’ll either have to rewrite DatabaseService for testing or hack some logic that uses an in-memory database if the current execution environment is a test.

The best way to deal with this is to follow the dependency inversion principle. Making DatabaseService, a high-level class, depend on another high-level class, OrderService, is the problem. Instead, make it depend on an abstraction.

Refactor OrderService to depend on an abstraction via its constructor. First, create the abstraction for persisting orders:

interface OrderRepository {
  fun saveOrder(shoppingCart: ShoppingCart): Boolean
}

Then, create an implementation for SQLite:

class SqliteOrderRepository : OrderRepository {
  override fun saveOrder(shoppingCart: ShoppingCart): Boolean {
    println("Persist order amounting to ${shoppingCart.getTotalOrderPrice()} to an SQLite Database.")
    return true
  }
}

Then, instead of tight coupling SqliteOrderRepository with OrderService, provide it via its constructor. Remember to use the abstraction instead of the concrete implementation:

class OrderService(private val orderRepository: OrderRepository) {
  fun processOrder(shoppingCart: ShoppingCart) {
    println("Processing order...")
    orderRepository.saveOrder(shoppingCart)
  }
}

In main, update orderProcessor instantiation:

val orderProcessor = OrderService(SqliteOrderRepository())

Rerun the app:

Processing order...
Persist order amounting to 2400.0 to an SQLite Database.

It’s just as before. The main benefit of this design is that you can easily swap in an in-memory database implementation for your tests. Create the new OrderRepository implementation:

class InMemoryOrderRepository : OrderRepository {
  override fun saveOrder(shoppingCart: ShoppingCart): Boolean {
    println("Persist order amounting to ${shoppingCart.getTotalOrderPrice()} to an In-Memory Database.")
    return true // success
  }
}

Use it in main to see it in action:

val orderProcessor = OrderService(InMemoryOrderRepository())
orderProcessor.processOrder(shoppingCart)

And the result is:

Processing order...
Persist order amounting to 2400.0 to an In-Memory Database.

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

See forum comments
Cinema mode Download course materials from Github
Previous: Learning Dependency Inversion Principle Next: Conclusion