Refactor: Change Signature on a Protocol [Interface]

When a “raw” type appears in a protocol, it encourages it to spread all over.

Recently, I needed to encapsulate a type: I had used “array of integer” ([Int]) in a number of places, and I wanted to change this to a type SortSpec. (The integers represented the column numbers to sort by, but I also want to choose whether a column is ascending or descending.)

This “[Int]” type had migrated to a lot of places, but the tricky one was a decorator pattern, which has a Swift protocol (interface) and several classes that implement it.

A protocol with an unencapsulated type, and a bunch of classes that reflect that

Some IDEs can help with this change, but Xcode for Swift does not.

Why is it so tricky? Because if you change the type on the interface first, none of the classes will compile; if you change the type on the class first, it won’t compile.

Tiny Steps

This isn’t a huge refactoring, but it’s not a small one either. We could have an arbitrary number of classes implementing the protocol. When we refactor, we don’t want to leave the system “torn apart” for any length of time. We look for tiny steps, where we can check in and run tests along the way.

In the notes below, I’ll say “You can work one at a time…” I usually do the first one by itself, then a few at a time. If I find I make mistakes in the larger group, I’ll revert and work in smaller groups or one at a time.

Approach

We’re using an approach I call Expand-Flip-Contract. The idea is that we will first widen the interface, flip to the new approach, then narrow the interface (eliminating the original approach). (Other refactorings use Expand-Flip-Contract too. For example, if you want to encapsulate parameters x and y into a Point, you might first add an extra Point parameter, start using it, then remove the x and y parameters.)

The Steps – Expand

Preparation: provide a way to convert between the old signature type and the new one.

In my case, I started with a simple wrapper that let me write SortSpec([1,2,3]) to convert to SortSpec, and spec.value() to get back to [1,2,3].

  1. In each class implementing the protocol, add a definition for the new method. Make its implementation delegate to the original method. (You can do these one at a time, testing and checking in after each one)

The original method was sortBy(columnOrder: [Int]).

The new method: func sortBy(_ spec: SortSpec) { self.sortBy(spec.value()) }

  1. Add the new method to the protocol definition. The compiler will tell you if you missed any classes in the previous step. You can test and check in.

Flip

  1. “Flip” the code so that the new implementation uses the original code, and the original implementation delegates to the new one. (You can work one at a time, testing and checking in after each.)

I did this very mechanically: copy the body of the old code into the new method, then add a line in front of it: let columnOrder = spec.value(). The body of the original method became: sortBy(SortSpec(columnOrder)).

You could use even smaller steps: change the new code and old code separately. But I liked doing them together so that the new code is exercised by the original tests.

  1. Update the call sites: make any place that called the original code call the new code instead, wrapping its type as needed. (You can work one at a time, testing and checking in after each.)

Contract

  1. Delete the method from the protocol. (The compiler will warn you if you forgot to move any call sites to call the new method.) You can test and check in.
  1. Delete the original method from the classes, as there are no more callers. (You can work one at a time.) Test and check in.
  1. The refactoring is done, but it will usually open up other refactorings. It’s worth considering everywhere that converts back to the original type to see if they can be encapsulated too.

I found a few places that did SortSpec(spec.value()) and could simplify these to spec. Once the protocol was converted, I could tackle the places where code was manipulating the array, and move that code into the SortSpec class.

One goal of encapsulating the SortSpec is to limit exposure of the representation. There will be a moment where some other class needs to know which columns and which order, but nowhere else needs to know the “contents”.

Conclusion

With a refactoring like this, that may have to touch an arbitrary number of places, tool support is ideal. If you have to work manually, work in small safe steps.

You can avoid a refactoring like this by encapsulating types earlier. If I had asked, “What is [Int] really?” and encapsulated it early on, I wouldn’t have needed this refactoring.