Home iOS & Swift Books Swift Apprentice

25
Protocol-Oriented Programming Written by Ehab Yosry Amer

Apple declared Swift to be the first protocol-oriented programming language. This declaration was made possible by the introduction of protocol extensions.

Although protocols have been in Swift since the very beginning, this announcement, and the protocol-heavy standard library changes Apple made, affects the way you think about your types. Extending protocols is the key to an entirely new style of programming!

In brief, protocol-oriented programming emphasizes coding to protocols, instead of specific classes, structs or enums. It does this by breaking the old rules of protocols and allowing you to write implementations for protocols on the protocols themselves.

This chapter introduces you to the power of protocol extensions and protocol-oriented programming. Along the way, you’ll learn how to use default implementations, type constraints, mixins and traits to simplify your code vastly.

Introducing protocol extensions

You’ve seen extensions in previous chapters. They let you add additional methods and computed properties to a type:

extension String {
  func shout() {
    print(uppercased())
  }
}

"Swift is pretty cool".shout()

Here, you’re extending the String type itself to add a new method. You can extend any type, including ones that you didn’t write yourself. You can have any number of extensions on a type.

You can define a protocol extension using the following syntax:

protocol TeamRecord {
  var wins: Int { get }
  var losses: Int { get }
  var winningPercentage: Double { get }
}

extension TeamRecord {
  var gamesPlayed: Int {
    wins + losses
  }
}

Like the way you extend a class, struct or enum, you use the keyword extension followed by the name of the protocol you are extending. Within the extension’s braces, you can define additional members on the protocol.

The most significant difference in the definition of a protocol extension, compared to the protocol itself, is that the extension includes the actual implementation of the member. In the example above, you define a new computed property named gamesPlayed that combines wins and losses to return the total number of games played.

Although you haven’t written code for a concrete type that’s adopting the protocol, you can use the protocol members within its extension. That’s because the compiler knows that any type conforming to TeamRecord will have all the members required by TeamRecord.

Now you can write a simple type that adopts TeamRecord and use gamesPlayed without the need to reimplement it.

struct BaseballRecord: TeamRecord {
  var wins: Int
  var losses: Int

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}

let sanFranciscoSwifts = BaseballRecord(wins: 10, losses: 5)
sanFranciscoSwifts.gamesPlayed // 15

Since BaseballRecord conforms to TeamRecord, you have access to gamesPlayed, defined in the protocol extension.

You can see how useful protocol extensions can be to define “free” behavior on a protocol — but this is only the beginning. Next, you’ll learn how protocol extensions can provide implementations for members of the protocol itself.

Default implementations

A protocol defines a contract for any type that adopts it. If a protocol defines a method or a property, any type that adopts the protocol must implement that method or property. Consider another example of a TeamRecord type:

struct BasketballRecord: TeamRecord {
  var wins: Int
  var losses: Int
  let seasonLength = 82

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}
extension TeamRecord {
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}
struct BasketballRecord: TeamRecord {
  var wins: Int
  var losses: Int
  let seasonLength = 82
}

let minneapolisFunctors = BasketballRecord(wins: 60, losses: 22)
minneapolisFunctors.winningPercentage
struct HockeyRecord: TeamRecord {
  var wins: Int
  var losses: Int
  var ties: Int

  // Hockey record introduces ties, and has
  // its own implementation of winningPercentage
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}
let chicagoOptionals = BasketballRecord(wins: 10, losses: 6)
let phoenixStridables = HockeyRecord(wins: 8, losses: 7, ties: 1)

chicagoOptionals.winningPercentage // 10 / (10 + 6) == 0.625
phoenixStridables.winningPercentage // 8 / (8 + 7 + 1) == 0.5

Mini-exercise

Write a default implementation on CustomStringConvertible that will simply remind you to implement description by returning Remember to implement CustomStringConvertible!.

struct MyStruct: CustomStringConvertible {}
print(MyStruct())
// should print "Remember to implement CustomStringConvertible!"

Understanding protocol extension dispatch

There’s a critical gotcha to keep in mind when defining protocol extensions. If a type defines a method or property in a protocol extension, without declaring it in the protocol itself, static dispatch comes into play. Static dispatch means the compiler chooses the method or property used at compile-time based on what it knows about the type. The compiler doesn’t account for dynamic runtime information.

protocol WinLoss {
  var wins: Int { get }
  var losses: Int { get }
}
extension WinLoss {
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}
struct CricketRecord: WinLoss {
  var wins: Int
  var losses: Int
  var draws: Int

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + draws)
  }
}
let miamiTuples = CricketRecord(wins: 8, losses: 7, draws: 1)
let winLoss: WinLoss = miamiTuples

miamiTuples.winningPercentage // 0.5
winLoss.winningPercentage // 0.53 !!!

Type constraints

For the protocol extensions on TeamRecord, you were able to use members of the TeamRecord protocol, such as wins and losses, within the implementations of winningPercentage and gamesPlayed. Much like in an extension on a struct, class or enum, you write code as if you were writing inside the type you’re extending.

protocol PostSeasonEligible {
  var minimumWinsForPlayoffs: Int { get }
}

extension TeamRecord where Self: PostSeasonEligible {
  var isPlayoffEligible: Bool { 
    wins > minimumWinsForPlayoffs
  }
}
struct HockeyRecord: TeamRecord {
  var wins: Int
  var losses: Int
  var ties: Int

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}
protocol Tieable {
  var ties: Int { get }
}
extension TeamRecord where Self: Tieable {
  var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}
struct RugbyRecord: TeamRecord, Tieable {
  var wins: Int
  var losses: Int
  var ties: Int
}

let rugbyRecord = RugyRecord(wins: 8, losses: 7, ties: 1)
rugbyRecord.winningPercentage // 0.5

Mini-exercise

Write a default implementation on CustomStringConvertible that will print the win/loss record in the format Wins - Losses for any TeamRecord type. For instance, if a team is 10 and 5, it should return 10 - 5.

Protocol-oriented benefits

What exactly are the benefits of protocol-oriented programming?

Programming to Interfaces, not Implementations

By focusing on protocols instead of implementations, you can apply code contracts to any type — even those that don’t support inheritance. Suppose you were to implement TeamRecord as a base class.

class TeamRecordBase {
  var wins = 0
  var losses = 0

  var winningPercentage: Double {
    Double(wins) / Double(wins + losses)
  }
}

// Will not compile: inheritance is only possible with classes.
struct BaseballRecord: TeamRecordBase {

}
class HockeyRecord: TeamRecordBase {
  var ties = 0

  override var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}
class TieableRecordBase: TeamRecordBase {
  var ties = 0

  override var winningPercentage: Double {
    Double(wins) / Double(wins + losses + ties)
  }
}

class HockeyRecord: TieableRecordBase {
}

class CricketRecord: TieableRecordBase {
}
extension TieableRecordBase {
  var totalPoints: Int {
    (2 * wins) + (1 * ties)
  }
}

Traits, mixins and multiple inheritance

Speaking of supporting one-off features such as a divisional win or loss, one of the real benefits of protocols is that they allow a form of multiple inheritance.

protocol TieableRecord {
  var ties: Int { get }
}

protocol DivisionalRecord {
  var divisionalWins: Int { get }
  var divisionalLosses: Int { get }
}

protocol ScoreableRecord {
  var totalPoints: Int { get }
}

extension ScoreableRecord where Self: TieableRecord, Self: TeamRecord {
  var totalPoints: Int {
    (2 * wins) + (1 * ties)
  }
}

struct NewHockeyRecord: TeamRecord, TieableRecord,
       DivisionalRecord, CustomStringConvertible, Equatable {
  var wins: Int
  var losses: Int
  var ties: Int
  var divisionalWins: Int
  var divisionalLosses: Int

  var description: String {
    "\(wins) - \(losses) - \(ties)"
  }
}

Simplicity

When you write a computed property to calculate the winning percentage, you only need wins, losses and ties. When you write code to print a person’s full name, you only need a first and last name.

var winningPercentage: Double {
  var percent = Double(wins) / Double(wins + losses)

  // Oh no! Not relevant!
  above500 = percent > 0.5

  return percent
}

Why Swift is a protocol-oriented language

You’ve learned about the capabilities of protocols and protocol extensions, but you may be wondering: What exactly does it mean that Swift is a protocol-oriented language?

// From the Swift standard library
public struct Array<Element> : RandomAccessCollection, MutableCollection {
  // ...
}

Challenges

Before moving on, here are some challenges to test your knowledge of protocol-oriented programming. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.

Challenge 1: Protocol extension practice

Suppose you own a retail store. You have food items, clothes and electronics. Begin with an Item protocol:

protocol Item {
  var name: String { get }
  var clearance: Bool { get }
  var msrp: Double { get } // Manufacturer’s Suggested Retail Price
  var totalPrice: Double { get }
}

Challenge 2: Doubling values

Write a protocol extension on Sequence named double() that only applies to sequences of numeric elements. Make it return an array where each element is twice the element in the sequence. Test your implementation on an array of Int and an array of Double, then see if you can try it on an array of String.

Key points

  • Protocol extensions let you write implementation code for protocols, and even write default implementations on methods required by a protocol.
  • Protocol extensions are the primary driver for protocol-oriented programming and let you write code that will work on any type that conforms to a protocol.
  • Type constraints on protocol extensions provide additional context and let you write more specialized implementations.
  • You can decorate a type with traits and mixins to extend behavior without requiring inheritance.
  • Protocols, when used well, promote code reuse and encapsulation.
  • Start with value types and find the fundamental protocols.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.