No substitutions for Liskov Substitution Principle (LSP)

March 15, 2019

In this post, we’ll explore the relationship between two concepts, ‘high level policy’ and ‘low level details’ and how the Liskov Substitution Principle bridges the gap. High level policy is a function or a set of functions that describes what needs to be done in a certain program path, whereas low level details are implementations of the dependencies that enable the high level policy. Let’s take a look at an example.

Here’s a high level policy for ordering a product from an online store:

This high level description of the steps that need to be done to place an order can be expressed in the following code

class OrderProductUseCase(private val inventoryService: InventoryService,
                          private val shippingService: ShippingService,
                          private val paymentService: PaymentService) {
    fun placeOrder(customer: Customer, order: ProductOrder) {
        val isInStock = inventoryService.isInStock(order.product.id)
        if(isInStock) {
            inventoryService.reserveProduct(order.product)
            paymentService.charge(customer, order)
            shippingService.ship(order)
        } else {
            throw OutOfStockException()
        }
    }
}

As you scan through the code, you might notice that there are no specific details about what is entailed in placing an order, rather what is described is how an order is placed. The how is the ‘low level details’ part. The details are hidden behind contracts, InventoryService, ShippingService, and PaymentService in our case.

As an example, the contract for the inventory service as dictated by the high level policy above is the following:

interface InventoryService {
    fun isInStock(productId: Long): Boolean

    fun reserveProduct(product: Product)
}

What would be an implementation of a contract? Here’s a really simple one for the inventory service:

class CompanyDataWarehouseInventoryService: InventoryService {
    override fun isInStock(productId: Long): Boolean {
        return true
    }

    override fun reserveProduct(product: Product) {
        println("Reserved product!")
    }
}

The high level policy above is only interested in the contract. It does not care about details.

If the high level policy does not care about the details (implementations), then what does? To connect the policy to the details, that is where the application is built up from a set of contract implementations. An example of driver code might be:

fun main() {
    val paymentService: PaymentService = CompanyPaymentService()
    val shippingService: ShippingService = CompanyShippingService()
    val inventoryService: InventoryService = CompanyDataWarehouseInventoryService()
    val orderProductUseCase: OrderProductUseCase = OrderProductUseCase(inventoryService, shippingService, paymentService)

    val customer = Customer()
    val order = ProductOrder(1L)
    orderProductUseCase.placeOrder(customer, order)
}

This is a very simplified implementation that illustrates the build up of the system from scratch. Most commonly you would use a dependency injection container such as Spring or Dagger to maintain object instances in the system.

What was described above is a perfect mold for talking about the Liskov Substitution Principle (LSP).

In effect, LSP states that when you implement an interface (e.g. CompanyDataWarehouseInventoryService implements InventoryService contract), the instance of the subclass object must be usable through the interface without the user knowing the difference. What we can see is that in our driver code above (i.e. the main function), we can inject any implementation of InventoryService and the high level policy (i.e. the ‘user’ aka OrderProductUseCase) will not care since it is only concerned with the contract, not the details.

Liskov Substitution Principle is a foundational principle in Object Oriented design and is the L in SOLID, the now famous acronym of a set of engineering principles. When framed against a backdrop of ‘high level policy’ and ‘low level details’, LSP stands out as a fundamental tenet to clean system design using contracts.

comments powered by Disqus