Refactoring With Protocols

The classic text on refactoring by Martin Fowler just got updated to a 2nd edition. How relevant is it to iOS developers? The book uses Javascript for the code examples but the techniques are language independent so don’t let that put you off. What Swift developers might miss is the use of protocols.

Refactoring (2nd Edition) by Martin Fowler

Refactoring by Martin Fowler

I couldn’t resist rewriting the first example from the book in Swift and following along as the author refactors the code. This needs a bit of a compromise as I tried to stick more or less to the original (Javascript) code structure which was not always possible.

The example prints an invoice for a collection of performances of a play. A Play has a type and a name which I implemented with a struct and an enum for the two possible types of play:

struct Play {
  enum Genre {
    case tragedy
    case comedy
  }

  let name: String
  let type: Genre
}

A performance has a play identifier and an integer for the audience attending the performance:

struct Performance {
  let playID: String
  let audience: Int
}

The interesting function calculates the cost for the performance of a play which depends on the type of play (tragedy or comedy), the audience size and some magic numbers:

func amountFor(_ performance: Performance) -> Int {
  var result = 0
  switch playFor(performance).type {
  case .tragedy:
    result = 40000
    if performance.audience > 30 {
      result += 1000 * (performance.audience - 30)
    }
  case .comedy:
    result = 30000
    if performance.audience > 20 {
      result += 10000 + 500 * (performance.audience - 20)
    }
    result += 300 * performance.audience
  }
  return result
}

A similar function calculates volume credits for a performance which again depends on the type of play and the audience. A comedy performance produces a greater credit:

func volumeCreditsFor(_ performance: Performance) -> Int {
  var result = 0
  result += max(performance.audience - 30, 0)
  if playFor(performance).type == .comedy {
    result += performance.audience / 5
  }
  return result
}

The challenge in the book is to refactor this code so that it’s easier to add more types of play.

What Is Refactoring?

It’s worth quoting the definition from the book:

Refactoring (noun): a change made to the internal structure of software to make it easier to understand and cheaper to modify without changing its observable behavior.

The book identifies the conditional logic in the previous methods as a problem. Adding new types of play requires us to modify the switch statement and probably the if statement in the second method to handle the new play types. It’s easy to imagine how messy that could get as the number of types increases.

Replacing Conditional Code With Polymorphism

The suggested refactoring is to replace conditional with polymorphism. That’s a fancy way of saying that you take the complicated conditional logic and create classes for each condition. Any code common to each variation moves to a base class.

We can take this approach in Swift and create a base performance calculator class:

class PerformanceCalculator {
  let audience: Int
  var amount: Int {
    fatalError("subclass responsibility")
  }
  var volumeCredits: Int {
    return max(audience - 30, 0)
  }      
  init(audience: Int) {
    self.audience = audience
  }
}

The amount (cost) and the volume credits for a performance are computed properties. It’s questionable with only two play types, but the base class provides a default implementation for the volume credits.

Each type of play then gets a specific subclass. The calculator for a tragedy play only needs to provide the amount:

class TragedyCalculator: PerformanceCalculator {
  override var amount: Int {
    var result = 40000
    if audience > 30 {
      result += 1000 * (audience - 30)
    }
    return result
  }
}

The comedy calculator overrides both properties:

class ComedyCalculator: PerformanceCalculator {
  override var amount: Int {
    var result = 30000
    if audience > 20 {
      result += 10000 + 500 * (audience - 20)
    }
    result += 300 * audience
    return result 
  }

  override var volumeCredits: Int {
    return super.volumeCredits + audience / 5
  }
}

This is fine as far as it goes, I think we can agree it becomes easier to introduce new types of play with different calculation logic. But…

Protocols And Value Types

Apple has been telling us since at least 2015 that our Swift code should prefer protocols and value types over inheritance and classes. How might it look if we refactored this example in a Swift’ier way?

Following the advice to start with a protocol rather than a class. We can replace the base class with a protocol:

protocol PerformanceCalculator {
  var audience: Int { get }
  var amount: Int { get }
  var volumeCredits: Int { get }
}

Our original base class has a default implementation for volumeCredits which we can provide in a protocol extension:

extension PerformanceCalculator {
  var volumeCredits: Int {
    return max(audience - 30, 0)
  }
}

Then using a value type instead of a class we can implement a calculator for a tragedy play that conforms to our protocol:

struct TragedyCalculator: PerformanceCalculator {
  let audience: Int
  var amount: Int {
    var result = 40000
    if audience > 30 {
      result += 1000 * (audience - 30)
    }
    return result
  }
}

Note that this is now a struct (a value type) not a class. Since both the amount and volumeCredits are computed properties the default initializer only needs the audience value:

let calculator = TragedyCalculator(audience: performance.audience)

The calculator for a comedy play implements both properties replacing the default volumeCredits method (I’ll skip the details here):

struct ComedyCalculator: PerformanceCalculator {
  let audience: Int
  var amount: Int { ... }
  var volumeCredits: Int { ... }
}

Is it better than subclassing? Does it make the code easier to understand and cheaper to modify? It’s a trivial example, with room for improvement, but I do prefer the protocol approach in this case. You can see my rough code for both approaches in this Gist and decide for yourself.

Either way, I recommend reading the book, it’s full of useful advice.

Further Reading