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.)
- 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.
- 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.
- 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.
- 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.
- 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.
- Create an empty protocol
protocol SortableTable { }
- 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.