Extract Protocol Refactoring (Swift) – Elevate Common Behavior

Extract Protocol lets us generalize our types.

Context: You have a class (or other entity), and want to let another class stand in for it in some cases.

You could make the other class a subclass. But this couples the two classes together very tightly. Further, if there are only a few methods in common, it may not even make sense to be a subclass.

Or, you could introduce a protocol that both classes will implement. A protocol defines methods (and possibly properties) that the type must implement.

Mechanics

(The description for “Extract Protocol” is based on a similar approach to “Extract Interface” described by Martin Fowler in Refactoring.)

Extract Protocol from an existing class
  1. Create an empty protocol:
protocol P {}

or, if you want to restrict it to class implementations (as opposed to struct or enum’s):

protocol P : AnyObject {}

You can run tests, but there should be no problems.

  1. Make A implement P:
class A : P {…}

or (if it already has parents):

class A: ExistingParentsOrProtocols, P {…}

You can run tests; there should be no problems.

  1. Copy any required variables from the class to the protocol
var v : Type {get set}

and methods

func f(args) -> ReturnType

You can run tests after moving any of these.

  1. Convert usages of A to be of type P.

Example:

func f(something: A)
     =>
func f(something: P)

You can run tests after moving any client to the new protocol.

  • You can work one call site at a time. If you get any warnings about missing (fields or) methods, go back to step 3 and add the missing declaration to the protocol.
  • You may have some client sites that still need to retain the type of A, but consider carefully whether the protocol may be missing something.
  1. Run the tests and make sure all is well.

This refactoring “leans on the compiler” to tell you if you omitted anything from your protocol interface.

Example

SortedTable is a two-dimensional array that can be sorted. (Trimmed from a larger example.)

class SortedTable {
    init([[String]]) {…} 
    func rows() -> Int {…}
    func columns() -> Int {…}
    subscript(r: Int, c: Int) -> String {…}
    func sort() {…}
}

We’d like new versions of this class, e.g., one that reads from a database, or one that wraps this class with extra features.

  1. Create an empty protocol
protocol SortableTable { }
  1. Make the class implement it.
class SortedTable: SortableTable {
    // rest as before
}

3. Copy method definitions from the class to the protocol:

protocol SortableTable {
    func rows() -> Int
    func columns() -> Int
    func sort()
}

We “accidentally” omitted the subscript.

4. Change places that refer to the class to use the protocol instead:

var model = SortedTable(array2d)
    =>
var model: SortableTable = SortedTable(array2d)

We see a syntax error further down:

    model[r,c]    <---- no such method

Go back to step 3 and add the subscript to the protocol.

protocol SortingTable {
    . . .
    subscript(r: Int, c: Int) -> String
}

Now everything builds and runs correctly. If you had a new class that supports this interface, you could drop it in without changing anything other than the constructor call.