Expand and contract

When we change the signature of a public method, we need to adjust all its client code accordingly. In big codebases this implies a single, time-consuming refactoring which can lead to version control conflicts (e.g. git merge conflicts) with other developers. However, we can break down this big refactoring into smaller steps by leveraging a technique called expand and contract. Let's take the following code as an example:

import java.math.BigDecimal

class Price(private val amount: BigDecimal) {

    fun discountedOf(percentage: BigDecimal): Price { 
        val discount = amount.multiply(percentage.divide(BigDecimal(100)))
        return Price(amount.subtract(discount))
    }
}

The percentage input parameter of the method discountedOf can be problematic as there is no guarantee for it to be between 0 and 100. For this reason, we want to introduce a class Percentage so that the signature of the method discountedOf can become discountedOf(percentage: Percentage). Let's see how we can do this in small steps with the expand and contract technique.

1. Expand

Instead of modifying the signature of the method discountedOf right away, we add a new method newDiscountedOf which has the desired new signature:

import java.math.BigDecimal

class Price(private val amount: BigDecimal) {

    fun newDiscountedOf(percentage: Percentage): Price {
        val discount = percentage.of(amount)
        return Price(amount.subtract(discount))
    }

    fun discountedOf(percentage: BigDecimal): Price {
        val discount = amount.multiply(percentage.divide(BigDecimal(100)))
        return Price(amount.subtract(discount))
    }
}

class Percentage(private val percentage: BigDecimal) {
    init {
        require(percentage in BigDecimal(0)..BigDecimal(100) ) {
            "Percentage must be between 0 and 100"
        }
    }

    fun of(amount: BigDecimal): BigDecimal {
        return amount.multiply(percentage.divide(BigDecimal(100)))
    }
}

At this point for the client code nothing has changed: everybody still use the method discountedOf while newDiscountedOf is dead code.

2. Migrate clients one by one

Now we can migrate the client code. However, instead of doing it all at once, we can refactor one client at a time. In this way we can gradually increase the usage of newDiscountedOf while reducing the one of discountedOf. Each client migration can be a standalone code revision (git commit), so the changes are small and incremental while the entire codebase keeps working as expected.

3. Contract

When all clients have been migrated, the method discountedOf is now dead code, so we can delete it and rename the method newDiscountedOf into discountedOf. The final result looks like the following:

import java.math.BigDecimal

class Price(private val amount: BigDecimal) {

    fun discountedOf(percentage: Percentage): Price {
        val discount = percentage.of(amount)
        return Price(amount.subtract(discount))
    }
}

class Percentage(private val percentage: BigDecimal) {
    init {
        require(percentage in BigDecimal(0)..BigDecimal(100) ) {
            "Percentage must be between 0 and 100"
        }
    }

    fun of(amount: BigDecimal): BigDecimal {
        return amount.multiply(percentage.divide(BigDecimal(100)))
    }
}

The end result is the same as we would have done it in one shot. However, we made the refactoring more maneageable by breaking it down into smaller steps.

Further applications

Expand and contract is very useful to avoid merge conflicts, no matter if you are using trunk base development or feature branches. Especially with trunk based development, it plays very well with the use of feature toggles.

Furthermore, despite the above example is about a class method, the same approach is applicable to api contracts. Let's assume for instance a POST REST API with the following body:

{
  "price" : 72.9,
  "discount" : 15
}

Then, let's assume we want to change "discount" from an integer to a percentage expressed as a decimal between 0 and 1. Using the expand and contract approach, we would first expand:

{
  "price" : 72.9,
  "discount" : 15,
  "discount_percentage" : 0.15
}

Then migrate all clients to use "discount_percentage" instead of "discount" and finally contract:

{
  "price" : 72.9,
  "discount_percentage" : 0.15
}

Last but not least, we can use expand and contract also for database schema migrations.


Recommended reads

Teach me back

I really appreciate any feedback about the book and my current understanding of software design.