Protocol-Oriented Programming: The Best Tool of Expert iOS Developers

After the introduction of protocol extensions in Swift, Apple started pushing protocol-oriented programming.

Even if it was a new paradigm, it quickly got widespread adoption in Swift programming and iOS development. 

This is not a surprise. Protocol-oriented programming is a highly flexible paradigm with many benefits. One of my favorite applications is to create well-structured network requests.

Protocol-oriented programming solves many of the problems of object-oriented programming. Moreover, it works great with Swift’s value types, i.e., structures and enumerations.

Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Contents


Section 1

The Problems of Object-Oriented Programming


TOC 2

Section 2

Modeling the Business Domain with Protocol-Oriented Programming


TOC 3

Section 3

Extending Existing Types and Hierarchies Without Altering Them


TOC 4

Section 4

Implementing Reusable Generic Algorithms

Section 1:

The Problems of Object-Oriented Programming

Mapping JSON data to Swift types and automatic decoding

Object-oriented programming is the most common paradigm in modern software development. OOP is far from perfect though, and sports several problems a skilled developer needs to consider.

To fully appreciate the versatility of protocol-oriented programming, you first need to understand the problems object-oriented programming presents.

Protocol-oriented programming works well with any design patterns, including MVC

As an example for this article, we will create a small, turn-based fantasy game. You can find the complete Xcode project on GitHub.

the mockup for the sample game to show protocol oriented programming in practice

This little game offers a good example of protocol-oriented programming isolated from the rest of the iOS SDK. It will include many ideas that you can pick up even if you are not well-versed in iOS development.

But don’t be fooled.

Protocol-oriented programming is not just a paradigm that works well only for game development. In fact, it’s beneficial for common iOS development.

You can use protocol-oriented programming at any level of the MVC pattern. Here are some examples:

  • In the model layer, decoding JSON data with Codable is an example of protocol-oriented programming. And that’s not the only one. The whole Swift standard library is based on protocol-oriented programming. For example, widespread data structures like String, Array, and Dictionary conform to a complex network of protocols.
  • At the model controller level, I usually use protocol-oriented programming to represent network requests and resources. 
  • Protocol-oriented programming is also beneficial at the view controller level. Since in iOS, all view controllers must descend from the UIViewController class, it’s hard to share standard code between them using simple inheritance.
  • Finally, views present the same challenges of view controllers since they are also ensconced in a rigid inheritance structure. And SwiftUI, Apple’s new UI framework, is founded on protocol-oriented programming.

Before we proceed, a little disclaimer. What I will be showing is not necessarily the best approach to make a game. I am using this example to fit the didactical purpose of this article, not to teach you how to make games.

Using inheritance in object-oriented programming to share common functionality between classes

Most programming languages, including Swift, are object-oriented so this is the paradigm you pick first when you learn to program.

Object-oriented programming is a fundamental component of software development. So, even if paradigms like protocol-oriented programming and functional programming can be useful, object-oriented programming is not going away.

Still, it has its drawbacks.

To expose them, we will start modelling the characters for our game using classes.

The first type of character you find Dungeons-&-Dragons-style games is the warrior. Warriors are strong fighters that can use a vast array of weapons and armors.

struct Weapon: Equatable {
	let name: String
	let damage: Int
	
	static let sword = Weapon(name: "Sword", damage: 15)
	static let mace = Weapon(name: "Mace", damage: 10)
}

struct Armor: Equatable {
	let name: String
	let defense: Int
	
	static let breastPlate = Armor(name: "Breastplate", defense: 10)
	static let chainMail = Armor(name: "Chainmail", defense: 5)
}

class Warrior {
	static let maxHealth = 140
	
	let weapon: Weapon = .sword
	let armor: Armor = .breastPlate
	var health = maxHealth
}

The next type of character is the wizard. Wizards are bad at fighting, but they can cast powerful spells. They cannot wear armor, because it prevents them from making the movements necessary to cast spells.

Unfortunately, I don’t have a wizard icon. But I have one for witches, so Witch it is.

struct Spell: Equatable {
	let name: String
	let power: Int
	
	static let fireball = Spell(name: "Fireball", power: 30)
	static let heal = Spell(name: "Heal", power: 20)
}

class Witch {
	static let maxHealth = 110
	
	let spell: Spell = .fireball
	var health = maxHealth
}

So far, so good.

Now, in our game, characters need to interact with each other.

  • Warriors can attack both warriors and witches. The damage depends on the weapon used and on the armor of the attacked character if any.
  • Witches can also cast spells both warriors and witches. The damage depends on the power of the spell and the magic resistance of the attacked character.

Since we start having common characteristics, we need to create a Character superclass. This enables the Warrior and Witch subclasses to implement the methods for attacks and spells, respectively.

class Character {
	let maxHealth: Int
	let defense: Int
	let magicResistance: Int
	var health: Int
	
	init(maxHealth: Int, defense: Int, magicResistance: Int) {
		self.maxHealth = maxHealth
		self.health = maxHealth
		self.defense = defense
		self.magicResistance = magicResistance
	}
}

class Warrior: Character {
	let weapon: Weapon = .sword
	let armor: Armor = .breastPlate
	
	init() {
		super.init(maxHealth: 140, defense: armor.defense, magicResistance: 0)
	}
	
	func attack(_ other: Character) {
		let effect = max(0, weapon.damage - other.defense)
		other.health -= effect
	}
}

class Witch: Character {
	let spell: Spell = .fireball
	
	init() {
		super.init(maxHealth: 110, defense: 0, magicResistance: 25)
	}
	
	func castSpell(on other: Character) {
		let effect = max(0, spell.power - other.magicResistance)
		switch spell {
		case Spell.fireball: other.health -= effect
		case Spell.heal: other.health = min(other.health + effect, other.maxHealth)
		default: break
		}
	}
}

the object-oriented hierarchy for the character class

Problem: you can create empty instances of any superclass

At first sight, the code above looks fine.

The Character class encapsulates the common properties, which the Warrior and Witch class initialize independently. These properties allow us to define the attack(_:) method of Warrior and the castSpell(on:) method of Witch class.

But a trained eye can already spot a few problems.

The Character type is a full-fledged class. That means you can create instances of it, even if it does not make any sense in our game.

You probably think that you would never create such an instance in the first place.

Maybe.

But you often work on big projects with other developers. Do they also know that they should not do that? The compiler does not prevent that.

While it might be apparent it in this simple example, that’s less obvious with more complex superclasses. These empty instances can then creep into other parts of your code and create problems.

You can create instances of UIViewController, for example. But if you know iOS development, you also know that it would be pointless. UIViewController exists only to be subclassed.

Languages like Java use abstract classes to avoid this problem, but Swift does not have a similar construct.

Problem: subclasses get the burden of initializing stored properties with meaningful values

Another thing that is implicit in the code above is that the Warrior and Witch classes must provide values for maxHealth, health, defense and magicResistance.

But again, the compiler does not enforce it.

Instead, the compiler only forces us to put some useless code into the Character class. We must initialize all stored properties with meaningless values, or our code won’t compile. 

If we add new characters to our game and we forget to initialize any of these properties, the compiler will be okay with it. But our new character classes will inherit the meaningless values of Character, creating inconsistent states and strange bugs.

We can always try to work around these problems using a combination of computed properties, property observers, and assertions.

But that only adds extra complexity without providing any compile-time check. All we would get runtime crashes, which we might not catch in our testing.

Problem: subclasses can break superclass invariants

Another thing to pay attention to is that before introducing the Character class, the maxHealth property was conveniently implemented as a static constant.

That made sense. Each character class has its own, predefined value that never changes.

Now though, the maxHealth property is a variable. That means that we can change its value at any time in a subclass, even though that is conceptually wrong.

This is an example of a class invariant, which is a condition that needs to be true all the time.

The problem of inheritance is that subclasses don’t know anything about the invariants of a superclass.

Since they get access to inherited stored properties, they can change their value of at any time, potentially breaking the invariants of a superclass higher in the hierarchy.

With deeper hierarchies, this becomes more and more likely. And again, there is no compiler check. Our only defense is using assertions.

Problem: it’s not clear when it’s safe to override methods and how to do it

Let’s now introduce a new character type in our game, the cleric.

Clerics are holy fighters which can also cast spells they receive from their god. These are usually healing spells.

While clerics can fight, they have restrictions on their gear. They need to use lighter armor that allows them to cast spells, and they can only use blunt weapons (I never got this last one, but well).

class Cleric: Character {
	let weapon: Weapon = .mace
	let armor: Armor = .chainMail
	let spell: Spell = .heal
	
	init() {
		super.init(maxHealth: 120, defense: armor.defense, magicResistance: 10)
	}
}

A cleric can fight like a warrior, so it has a weapon, an armor, and needs the same attack(_:) method of the Warrior class. To share code, we then create a Fighter superclass.

class Fighter: Character {
	let weapon: Weapon
	let armor: Armor
	
	init(maxHealth: Int, defense: Int, magicResistance: Int, weapon: Weapon, armor: Armor) {
		self.armor = armor
		self.weapon = weapon
		super.init(maxHealth: maxHealth, defense: defense, magicResistance: magicResistance)
	}
	
	func attack(_ other: Character) {
		let effect = max(0, weapon.damage - other.defense)
		other.health -= effect
	}
}

class Warrior: Fighter {
	init() {
		let weapon = Weapon.sword
		let armor = Armor.breastPlate
		super.init(maxHealth: 140, defense: armor.defense, magicResistance: 0, weapon: weapon, armor: armor)
	}
}

class Cleric: Fighter {
	let spell = Spell.heal
	
	init() {
		let weapon = Weapon.mace
		let armor = Armor.chainMail
		super.init(maxHealth: 120, defense: armor.defense, magicResistance: 10, weapon: weapon, armor: armor)
	}
}

abstracting common functionality in a superclass in an object-oriented hierarchy

Here we can see another couple of problems of inheritance.

  • Adding more classes to our hierarchy adds new requirements to initializers. Each level adds more initialization burden for subclasses.
  • The Warrior and Cleric subclasses can now override the attack(_:) method. But should they? If they do, should they rely on the implementation of the superclass, or can they ignore it? And where does this call go, exactly? In the beginning, in the middle, or at the end of the override?

Again, these problems might seem trivial in our example, because the initializers are still manageable, and the attack(_:) method’s implementation is small.

But, the deeper the hierarchy, the easier it is to break superclass invariants, to call methods of the superclass in the wrong place, or to forget it altogether.

And that gets worse when you don’t own the code of superclasses, and you cannot read their implementation.

All you have left is guessing, hoping to trigger some assertion, or relying on documentation, which is often poor or non-existent. The compiler does not help you in any way.

Problem: object-oriented programming does not support multiple inheritance

Since clerics can also cast spells, we also want to share code between the Cleric and Witch classes. So we can create a new Spellcaster class with the repeated code.

class Spellcaster: Character {
	let spell: Spell
	
	init(maxHealth: Int, defense: Int, magicResistance: Int, spell: Spell) {
		self.spell = spell
		super.init(maxHealth: maxHealth, defense: defense, magicResistance: magicResistance)
	}
	
	func castSpell(on other: Character) {
		let effect = max(0, spell.power - other.magicResistance)
		switch spell {
		case Spell.fireball: other.health -= effect
		case Spell.heal: other.health = min(other.health + effect, other.maxHealth)
		default: break
		}
	}
}

class Witch: Spellcaster {
	init() {
		super.init(maxHealth: 110, defense: 0, magicResistance: 25, spell: .fireball)
	}
}

And that’s where we get into yet another problem of object-oriented programming.

Swift allows a class to descend from only one other class. That’s because multiple inheritance creates ambiguity in case of common ancestors and overrides.

That’s called the diamond problem. Each language solves it in different ways. The solution of Swift and many other languages is to forbid multiple inheritance for classes.

So, the only solution we have to make the Cleric class descend from both Fighter and Spellcaster is to arrange these two in a hierarchy, making one descend from the other.

That creates another problem. No matter which one you pick to be at the top, the other will have to inherit methods it does not need.

extending an object-oriented hierarchy

For example, if we make Spellcaster descend from Fighter, it will inherit the weapon and armor properties, and the attack(_:) method. And, transitively, the Witch class will get these as well.

This problem is known as interface pollution.

But witches cannot fight, so we don’t want this functionality to be available at the Witch level.

  • For inherited properties, the only solution we have is to make them optional. In that way, the Witch class can set weapon and armor to nil, so we get some check.
  • For inherited methods like attack(_:), the only option we have is overriding and using assertions.

But these are not great solutions. And again, if later we add another class that descends from Spellcaster, we need to remember all that.

Section 2:

Modeling the Business Domain with Protocol-Oriented Programming

Modeling the Business Domain with Protocol-Oriented Programming

We have seen the problems object-oriented programming brings along.

While some solutions exist, they are less than ideal. They mostly rely on the programmer instead of using the compiler to check for the correctness of our code.

Protocol-oriented programming solves many of these problems, shifting the burden from the developer to the compiler.

The building blocks of protocol-oriented programming: protocols and protocol extensions

As the name implies, protocol-oriented programming is founded on protocols. In Swift, protocols define the desired interface a type must implement to suit a particular task or piece of functionality.

But protocols alone are not enough for protocol-oriented programming.

A second feature is necessary: protocol extensions.

In Swift, you can add code to any type through extensions, even to types you don’t own. Swift 3 extended this feature to protocols, which worked only for enumerations, structures, and classes in previous versions of the language.

This gives us a ton of flexibility in how we share code across types. 

With protocol extensions, we can attach method implementations to abstract protocols. All the types that conform to that protocol will then inherit those methods.

This is not dissimilar from inheritance in object-oriented programming. But thanks to the nature of protocols, we can get rid of many of the problems object-oriented programming has.

While protocol-oriented programming is a term that mostly refers to Swift, the concept is not new. In more general programming terms, protocol extensions are mixins, which are available in other languages as well.

Don’t listen to Apple: start with concrete types, not with protocols

One of the great features of protocols is that any Swift type can conform to them.

Swift’s value types, i.e., structures and enumerations, don’t have an inheritance mechanism like classes. But value types can conform to protocols. So, thanks to protocol extensions, they can still inherit method implementations.

That already solves one of the problems of object-oriented programming. When we pass values around, they are copied. With objects, instead, we pass around references to a shared instance. 

This will be useful later, but I had to mention it now since that’s where we will start to introduce protocol-oriented programming in our game. We will turn our classes into structures.

But how do we proceed? Shall we start from the structures or the protocols?

In its WWDC 2015 presentation, Apple recommended to always start with a protocol.

apple recommendation from WWDC 2015 presentation on protocol-oriented programming

But I often recommend the opposite approach and start from concrete types, creating protocols only when needed. Otherwise, you end up with a bunch of protocols that serve no purpose other than following a guideline.

After all, as we created the classes for our game, we did not start from the superclasses. We created the latter only when we needed to share code.

In fact, even Apple changed its mind in its Modern Swift API Design session at WWDC 2019.

apple new recommendation to not start with a protocol from moderns swift api design WWDC 2019

Still, in this section, we will start with protocols. But that’s only because we already did the abstraction work in the previous section. 

Otherwise, I always start with structures.

Protocols declare requirements instead of providing stored properties

Implementing our game will need a substantial amount of code. While I will show it all below, I will gloss over its implementation details.

I will only focus on what is relevant to understand protocol-oriented programming or this article would turn into a book.

Let’s start defining a protocol for our characters. We know that a Character needs to have specific properties, so we express them as requirements in our protocol.

protocol Character {
	static var maxHealth: Int { get }
	var health: Int { get set }
	var magicResistance: Int { get }
	var defense: Int { get }
}

extension Character {
	var isDead: Bool {
		return health <= 0
	}
}

These are not stored properties like they were in our classes. Instead, they are requirements that any conforming type will have to meet, declaring its own properties. This makes it harder to break invariants because a protocol defines the appropriate access for each property.

The isDead property is the first example of a protocol extension. In our game, characters die when their health goes below 0. Any type conforming to Character will get this computed property.

Protocol hierarchies are shallower than class hierarchies

Proceeding down the class hierarchy of the previous example, we find the Fighter class. That also becomes a protocol in our new approach.

protocol Fighter {
	var weapon: Weapon { get }
	var armor: Armor { get }
}

extension Fighter {
	func attack(_ opponent: Character) -> Character {
		var result = opponent
		let effect = max(0, weapon.damage - opponent.defense)
		result.health -= effect
		return result
	}
}

Notice that Fighter does not descend from Character anymore.

Why? Because it does not have to.

Only the destination of an attack needs to be a character. The attacker only needs a weapon. This allows us to keep our hierarchy flat, removing the initialization burden from your types.

So, as a rule, don’t make a protocol descend from another one unless you need it. That increases composability.

This is not evident in our game, because after all, all fighters are also characters. But later, you might want to add new types to the game, things like monsters, artifacts, and other creatures.

If you want these to be able to attack characters, without behaving like them, you can make them conform to the Fighter protocol, avoiding the requirements of Character (the protocol might need to be renamed).

We can do the same for the Spellcaster protocol.

protocol Spellcaster {
	var spell: Spell { get }
}

extension Spellcaster {
	var isHealer: Bool {
		return spell == .heal
	}
	
	func castSpell(on opponent: Character) -> Character {
		var result = opponent
		let effect = spell.power - opponent.magicResistance
		result.health = isHealer
			? min(opponent.health + effect, type(of: opponent).maxHealth)
			: opponent.health - effect
		return result
	}
}

The isHealer property will be useful later when we implement the game engine.

Protocol conformance forces you to implement all the requirements

We can now finally implement our concrete Warrior, Witch and Cleric types.

struct Warrior: Character, Fighter {
	static let maxHealth = 140
	let weapon: Weapon = .sword
	let armor: Armor = .breastPlate
	let magicResistance = 0
	var health = maxHealth
	
	var defense: Int {
		return armor.defense
	}
}

struct Witch: Character, Spellcaster {
	static let maxHealth = 110
	let spell: Spell = .fireball
	let magicResistance = 25
	let defense = 0
	var health = maxHealth
}

struct Cleric: Character, Fighter, Spellcaster {
	static let maxHealth = 100
	let weapon: Weapon = .mace
	let armor: Armor = .chainMail
	let spell: Spell = .heal
	let magicResistance = 10
	var health = maxHealth
	
	var defense: Int {
		return armor.defense
	}
}

The most evident benefit here is that we are now able to use multiple inheritance. Each structure conforms to more than one protocol, inheriting all the properties and methods in their extensions.

multiple inheritance in protocol oriented programming

Moreover, there is no risk of forgetting to implement any of the protocol requirements like it happened with classes. If you forget one, the compiler will stop you.

Notice also that maxHealth is back to being a static property. We also have total freedom in declaring the other properties.

For example, in the Warrior and Cleric types, the defense property is computed and depends on the armor, while in the Witch struct it’s a constant stored property.

And finally, we can only create values from these three types. It’s not possible to create values with type Character, Fighter or Spellcaster, which would not make sense.

Section 3:

Extending Existing Types and Hierarchies Without Altering Them

Extending Existing Types and Hierarchies Without Altering Them

Protocol-oriented programming is not only useful to create new taxonomies. Another of the advantages of protocols and protocol extensions is that any hierarchy can be extended modularly, without altering the existing types.

Protocol-oriented programming is not a magical solution for every problem

We have characters, but we don’t have a game yet. For that, we need to create some rules.

I am not going to pretend that this will be the best game in the App Store. Again, my objective is to show you protocol-oriented programming in practice.

If you are interested in creating compelling games, check this website on game design.

The rules of our game will be simple:

  • The game is played by two teams of five characters each (one will be controlled by a simple AI algorithm that we will implement later).
  • The characters in a team are chosen randomly, but there will be at least one character per type in each team.
  • The game is played in alternating turns, in which each team can make a single move.
  • Each character gets a single action each round. A round ends when all the characters have moved.

From the rules above, it looks like the members of each team need some new properties.

  • We need a way to identify each team member individually since there can be more than one character of each type on each team.
  • We need to know when a team member has performed its move for the current round.

Since we are all pumped-up with protocol-oriented programming, the temptation might be to create a new protocol with these requirements.

As the saying goes, “when all you have is a hammer, everything looks like a nail.”

But protocol-oriented programming is not a magical solution for every problem. Our structures represent the behavior of our characters.

The rules of the game, instead, do not belong to characters. We could use these types in all sorts of games with different rules.

So, in this case, composition is better than inheritance.

struct Game {
	var turn = Team.player
	
	mutating func startNewRound() {
		for var member in members {
			member.didMove = false
			update(member)
		}
	}
}

extension Game {
	enum Team {
		case player
		case opponent
	}
	
	struct TeamMember {
		let id = UUID()
		let team: Team
		var character: Character
		var didMove = false
	}
}

The Game structure is where we will implement our game rules. The Team and TeamMember types are namespaced, to make it clear that they belong to the Game implementation.

The TeamMember has a character property for composition. We can use Character as a generic type for this property since that’s the only requirement we have for team members.

Using protocol conformance as a requirement for generic functions

Now we need to create the initial conditions for a game. For that, we need a function that randomizes the members in each team, making sure that each side gets at least one character per type.

struct Game {
	var turn = Team.player
	var members = makeTeam(.player) + makeTeam(.opponent)
	
	var playerTeam: [TeamMember] {
		return members.filter { $0.team == .player }
	}
	
	var opponentTeam: [TeamMember] {
		return members.filter { $0.team == .opponent }
	}
	
	mutating func startNewRound() {
		for var member in members {
			member.didMove = false
			update(member)
		}
	}
}

private extension Game {
	static func makeTeam(_ team: Team) -> [TeamMember] {
		let witches = (0 ..< Int.random(in: 1...2)).map { _ in Witch() }
		let clerics = (0 ..< Int.random(in: 1...2)).map { _ in Cleric() }
		let teamCount = 5
		let remaining = teamCount - witches.count - clerics.count
		let warriors: [Warrior] = (0 ..< Int.random(in: remaining...remaining)).map { _ in Warrior() }
		let allCharacters: [Character] = witches + clerics + warriors
		return allCharacters
			.shuffled()
			.map { TeamMember(team: team, character: $0, didMove: false) }
	}
}

This is a pretty uninteresting function, except for one thing.

Notice how the code to create a random amount of witches, warriors and clerics gets repeated. We need to abstract it to make it reusable.

The problem though is that each line explicitly uses a character type. To abstract this code, we need a way to use a type as a parameter.

That’s something we can do again with a protocol.

private extension Game {
	static func makeTeam(_ team: Team) -> [TeamMember] {
		func makeCharacters<C: Initializable>(from: Int, upTo: Int) -> [C] {
			return (0 ..< Int.random(in: from...upTo)).map { _ in C.init() }
		}
		
		let witches: [Witch] = makeCharacters(from: 1, upTo: 2)
		let clerics: [Cleric] = makeCharacters(from: 1, upTo: 2)
		let teamCount = 5
		let remaining = teamCount - witches.count - clerics.count
		let warriors: [Warrior] = makeCharacters(from: remaining, upTo: remaining)
		let allCharacters: [Character] = witches + clerics + warriors
		return allCharacters
			.shuffled()
			.map { TeamMember(team: team, character: $0, didMove: false) }
	}
}

protocol Initializable {
	init()
}

extension Warrior: Initializable {}
extension Witch: Initializable {}
extension Cleric: Initializable {}

The Initializable protocol requires conforming types to have an initializer without parameters. Thanks to it, we can then create the makeCharacters(from:upTo:) generic function, using a C Swift generic with an Initializable type constraint.

(Side note: I made makeCharacters(from:upTo:) a nested function because this is the only place where we need it. You can make it a usual private method if you prefer.)

Using extensions, we then make each of our character types conform to Initializable.

This is another advantage of protocol-oriented programming. We can make any already-defined type conform to a protocol, without altering its declaration.

This is useful for a few reasons:

  • We can add conformance to any type, even to types that we don’t own, whether they belong to the iOS SDK or a third-party framework.
  • We can explicitly specify which types conform to our protocol, enabling the compiler to make stricter checks. The makeCharacters(from:upTo:) does not accept any type with an init() initializer. It only accepts Initializable types.
  • We don’t need to alter the declaration of our types. This keeps their code simpler to read and more reusable across code bases. The Initializable protocol is only needed for our Game structure. We can export and reuse the character types without bringing it along.

Adding extra functionality to types through protocol conformance and extensions

The last thing we need in our Game definition is a way to perform moves.

A move needs:

  • an action;
  • the character acting;
  • a target.

Each character can perform different actions, expressed by various methods. But despite their different signature, each method is a function from a character to another. So, we can declare our actions to store a generic function in a stored property.

We can do that because in Swift functions are a first-class citizen, which means we can treat them like any other type. We can store them in variables and pass them as parameters to other functions. 

This is a functional programming concept, so it’s outside of the scope of this article.

struct Game {
	var turn = Team.player
	var members = makeTeam(.player) + makeTeam(.opponent)
	
	var playerTeam: [TeamMember] {
		return members.filter { $0.team == .player }
	}
	
	var opponentTeam: [TeamMember] {
		return members.filter { $0.team == .opponent }
	}
	
	mutating func perform(_ move: Move) {
		func update(_ member: TeamMember) {
			guard let index = members.firstIndex(where: { $0.id == member.id }) else { return }
			members[index] = member
		}
		
		func startNewRound() {
			for var member in members {
				member.didMove = false
				update(member)
			}
		}
		
		var teamMember = move.performer
		teamMember.didMove = true
		update(teamMember)
		var target = move.target.id == teamMember.id ? teamMember : move.target
		target.character = move.action.closure(target.character)
		update(target)
		turn = turn.other
		let roundEnded = members.filter({ !$0.didMove }).isEmpty
		if roundEnded {
			startNewRound()
		}
	}
}

extension Game {
	enum Team {
		case player
		case opponent
	}
	
	struct TeamMember {
		let id = UUID()
		let team: Team
		var character: Character
		var didMove = false
	}
	
	struct Action {
		let name: String
		let closure: (Character) -> Character
		let isAttack: Bool
	}
	
	struct Move {
		let performer: TeamMember
		let target: TeamMember
		let action: Action
	}
}

We now need to extract the available actions from each character. For that, we will use protocol-oriented programming again.

First, we define a protocol that requires a conforming type to return an array of actions. Then, we can make our types conform to that protocol through extensions.

protocol Actionable {
	var actions: [Game.Action] { get }
}

extension Fighter {
	var attackAction: Game.Action {
		return Game.Action(name: "Attack", closure: attack(_:), isAttack: true)
	}
}

extension Spellcaster {
	var spellAction: Game.Action {
		return Game.Action(name: spell.name, closure: castSpell(on:), isAttack: !isHealer)
	}
}

extension Warrior: Actionable {
	var actions: [Game.Action] {
		return [attackAction]
	}
}

extension Witch: Actionable {
	var actions: [Game.Action] {
		return [spellAction]
	}
}

extension Cleric: Actionable {
	var actions: [Game.Action] {
		return [attackAction, spellAction]
	}
}

Notice that, again, this solves some of the problems of object-oriented programming.

  • Actionable forces any conforming type to implement the actions property. You can’t forget to implement when you add new types.
  • actions is a requirement, not an inherited function like it would be in a superclass, so we don’t have the problems of overriding. We don’t need to wonder if we can override a requirement or if and where we need to call the super implementation.
  • Protocol requirements do not create interface pollution. Any type conforming to Actionable needs the actions property, so it never ends in types that don’t need it.
  • We can still add methods to Fighter and Spellcaster, which will be inherited by conforming types. All the considerations about overriding still apply.

This completes the implementation of our game logic.

Notice that now our structures belong to a much broader inheritance hierarchy.

extending a protocol hierarchy to add new functionality with protocol oriented programming

This hierarchy is flatter and more flexible than an object-oriented one. Moreover, we created parts of it using extensions.

This means we can move only part of the hierarchy to other projects without bringing every protocol along. This would not be possible with a class hierarchy.

Section 4:

Implementing Reusable Generic Algorithms

Implementing Reusable Generic Algorithms

Another great use of protocol-oriented programming is the ability to implement standard generic algorithms that are completely decoupled from concrete data structures and implementations. All an algorithm needs is a few protocol requirements. It is then possible to use these algorithm implementations on any type, by conforming to such provisions.

Implementing the game AI with the minimax algorithm

We will now write a simple AI algorithm to make our game playable even if you don’t have a friend.

The typical game AI algorithm is minimax, which is used in all sorts of adversarial games, e.g., chess.

Conceptually, minimax is not hard to understand. All it does is try all possible moves for a player and pick the best one. To evaluate how good a move is, minimax uses a heuristic function to know how good the game state after such move.

Of course, in any strategic game, no move is good in isolation. If you ever played chess, you know that there is no point in capturing the opponent’s queen if he is going to checkmate you on his next move.

So, the algorithm evaluates moves recursively. After making a move, it considers all the possible replies, then all the possible responses to those replies, and so on, until it finds a winning position.

Of course, the algorithm assumes that each player will always pick the best move available. Hoping to win because your opponent makes a stupid move is a bad strategy.

This means that, at each step, the algorithm tries to maximize the heuristic value for one player and to minimize it for the other one. That’s where the name of the algorithm comes from.

The algorithm can be summarized in a few lines of pseudocode, which I took from its Wikipedia page:

extension Minimaxable {
	func minimax(atDepth depth: Int, maximizingPlayer: Bool) -> EvaluatedPly<Ply> {
		if depth == 0 || isTerminal {
			return EvaluatedPly(value: heuristicValue(maximizingPlayer: maximizingPlayer), ply: nil)
		}
		var bestPly: EvaluatedPly<Ply> = EvaluatedPly(value: maximizingPlayer ? Int.min : Int.max, ply: nil)
		for ply in possiblePlies() {
			let state = self.state(performing: ply)
			let evaluatedPly = state.minimax(atDepth: depth - 1, maximizingPlayer: !maximizingPlayer)
			let currentPly = EvaluatedPly(value: evaluatedPly.value, ply: ply)
			bestPly = maximizingPlayer ? max(currentPly, bestPly) : min(currentPly, bestPly)
		}
		return bestPly
	}
	
	func nextPly() -> Ply {
		let evaluatedPly = minimax(atDepth: 3, maximizingPlayer: true)
		return evaluatedPly.ply!
	}
}

There is one last caveat.

Minimax has exponential time complexity. Exponential-time algorithms are expensive and practical only on small sets of data.

So, the algorithm cannot always go all the way down to a victory position. For games where players have many possible moves, getting to an end state is impossible even on the fastest computer.

So the algorithm takes an extra depth parameter, which it decreases at each step. When the depth reaches 0, the algorithm stops and returns the heuristic value of the current state.

Protocols are useful to abstract the requirements of generic algorithms

When we deal with generic algorithms like minimax, it is useful to keep code generic and free from implementation details. This makes code more readable, and the next time you create a game, you can reuse the algorithm directly.

So, despite all my advice above, this is a case where it’s better to start with a protocol (in software development, no rule is absolute).

To be able to implement the minimax algorithm, we need to:

  • know if a position is terminal, i.e., when a player wins, and the game is over;
  • find a heuristic function to evaluate game states;
  • find all the possible moves for a player (called plies) in any given state;
  • apply each ply to a game state, getting a new, resulting state.

Again, we express all these requirements in a protocol.

protocol Minimaxable {
	associatedtype Ply
	var isTerminal: Bool { get }
	func heuristicValue(maximizingPlayer: Bool) -> Int
	func possiblePlies() -> [Ply]
	func state(performing move: Ply) -> Self
}

Notice that the requirements of this protocol are all generic. All the types are either common types like Bool and Int, or generic types like Self and Ply. I did not use any specific type coming from our Game structure.

We still have no idea how we will implement these requirements for our Game type, but that does not matter. We can already rely on these requirements to implement the minimax algorithm.

Implementing generic algorithms relying only on protocol requirements

The pseudocode for the minimax algorithm above is missing an important detail.

In that form, all it does is return the heuristic value for a game state. But when playing a game, we don’t care about that value alone. We want to know which move is best.

If we follow that blueprint, we would then have to generate all the possible plies for the first step, call the algorithm on each one, and then pick the one with the highest value.

That works, but it would be a bit redundant since the minimax algorithm already contains a loop that does precisely that. So I prefer to adapt the algorithm to return not only the value of a move but also the move.

For that, we need a new type.

struct EvaluatedPly<P> {
	let value: Int
	let ply: P?
}

extension EvaluatedPly: Comparable {
	static func < (lhs: EvaluatedPly<P>, rhs: EvaluatedPly<P>) -> Bool {
		return lhs.value < rhs.value
	}
	
	static func == (lhs: EvaluatedPly<P>, rhs: EvaluatedPly<P>) -> Bool {
		return lhs.value == rhs.value
	}
}

This is another instance of protocol-oriented programming.

The minimax algorithm uses the min(_:_:) and max(_:_:) functions, which in Swift only work on types that conform to Comparable. Many types in the Swift standard library already conform to it, including Int, String, and Date.

Making EvaluatedPly conform to Comparable will allow us to use the min(_:_:) and max(_:_:) functions, as well any other function that works with comparable types.

And finally, implementing the minimax algorithm is just a matter of translating the above pseudocode into Swift code.

extension Minimaxable {
	func minimax(atDepth depth: Int, maximizingPlayer: Bool) -> EvaluatedPly<Ply> {
		if depth == 0 || isTerminal {
			return EvaluatedPly(value: heuristicValue(maximizingPlayer: maximizingPlayer), ply: nil)
		}
		var bestPly: EvaluatedPly<Ply> = EvaluatedPly(value: maximizingPlayer ? Int.min : Int.max, ply: nil)
		for ply in possiblePlies() {
			let state = self.state(performing: ply)
			let evaluatedPly = state.minimax(atDepth: depth - 1, maximizingPlayer: !maximizingPlayer)
			let currentPly = EvaluatedPly(value: evaluatedPly.value, ply: ply)
			bestPly = maximizingPlayer ? max(currentPly, bestPly) : min(currentPly, bestPly)
		}
		return bestPly
	}
	
	func nextPly() -> Ply {
		let evaluatedPly = minimax(atDepth: 3, maximizingPlayer: true)
		return evaluatedPly.ply!
	}
}

My code’s structure is slightly different because the two branches of the if-else statement looked redundant, and I merged them into one. Other than that, my code matches the pseudocode one-to-one.

The minimax(atDepth:maximizingPlayer:) method is a recursive method. I talk about recursion in-depth in my article on functional programming.

Making a type conform to a protocol to inherit a generic algorithm implementation

We can now make our Game type conform to Minimaxable and take advantage of the generic minimax implementation.

Two of its requirements are straightforward to implement:

  • A game state is terminal when either team has no alive members left.
  • In our game, a ply has is a Game.Move structure, and our Game type already has a method to perform a move, which we can use.
extension Game: Minimaxable {
	var isTerminal: Bool {
		return playerTeam.count == 0 || opponentTeam.count == 0
	}
	
	func state(performing ply: Game.Move) -> Game {
		var state = self
		state.perform(move)
		return state
	}
}

Here we can see another of the advantages of protocol-oriented programming which I did not discuss yet.

Since our Game structure is a value type, we can quickly create copies in the state(performing:) method and then apply a move to it. 

If the Game type were a class instead, any move would change the state of the actual game. To then be able to implement the state(performing:) method, we would have to write code to perform a deep copy of a Game instance.

The heuristic value of a node depends on the rules of the game. For our game, we can sum the health of all characters in a team and subtract the health of the characters in the other.

extension Game: Minimaxable {
	var isTerminal: Bool {
		return playerTeam.count == 0 || opponentTeam.count == 0
	}
	
	func heuristicValue(maximizingPlayer: Bool) -> Int {
		var healthScore = 0
		for member in members.filter({ !$0.character.isDead }) {
			let maximizingTeam = maximizingPlayer ? self.turn : self.turn.other
			let sign = member.team == maximizingTeam ? 1 : -1
			healthScore += sign * member.character.health
		}
		return healthScore
	}
	
	func state(performing ply: Game.Move) -> Game {
		var state = self
		state.perform(move)
		return state
	}
}

This is not a perfect metric, but it works since the algorithm will try to maximize the damage it makes to the opponent.

Thanks to the heuristic, the algorithm does not need to know about all the rules. Damage is based on attack, defense, power and magicResistance values, but they are not in the heuristic implementation. Trying all the moves and checking the results accounts for those game rules already.

A more subtle heuristic would also consider other factors, for example, how many characters are alive, since fewer characters mean fewer moves available to a player in a round.

If you are interested in balancing and evaluating game rules, check this website.

The last requirement of Minimaxable is the possiblePlies() method. Here we can leverage the fact that our character structures conform to the Actionable protocol to write a generic method.

extension Game: Minimaxable {
	var isTerminal: Bool {
		return playerTeam.count == 0 || opponentTeam.count == 0
	}
	
	func heuristicValue(maximizingPlayer: Bool) -> Int {
		var healthScore = 0
		for member in members.filter({ !$0.character.isDead }) {
			let maximizingTeam = maximizingPlayer ? self.turn : self.turn.other
			let sign = member.team == maximizingTeam ? 1 : -1
			healthScore += sign * member.character.health
		}
		return healthScore
	}
	
	func state(performing ply: Game.Move) -> Game {
		var state = self
		state.perform(move)
		return state
	}
	
	func possiblePlies() -> [Game.Move] {
		let attacker = turn == .player ? playerTeam : opponentTeam
		let defender = turn == .player ? opponentTeam : playerTeam
		var moves: [Game.Move] = []
		for member in attacker {
			guard !member.didMove else { continue }
			for action in member.actions {
				moves += action.isAttack
					? defender.map { Move(performer: member, target: $0, action: action) }
					: attacker.map { Move(performer: member, target: $0, action: action) }
			}
		}
		return moves
	}
}

Without Actionable, we would have to downcast each character to its concrete type and then call the appropriate method. 

That would mean a lengthy conditional statement, violating the Open-closed principle — yet another advantage of protocol-oriented programming.

Conclusions

I won’t go over the UI implementation for the game since there is nothing exciting in it related to protocol-oriented programming. If you are interested in seeing the code, you can find it in the full Xcode project on GitHub.

Once the UI is implemented, you can play our little game against the AI.

 

Admittedly, this is not the most exciting iOS game you’ll ever play, but considering how little code we wrote, it’s already impressive that it is playable at all.

In this article, I showed you that protocol-oriented programming solves many of the problems of object-oriented programming. For this reason, it’s a necessary tool in the toolbox of any serious iOS developer.

That does not mean you can toss object-oriented programming out of the window, though.

A big part of the iOS SDK is object-oriented, including UIKit and Foundation, the most used frameworks in iOS development. Views, view controllers, user defaults, URL sessions, and many other types in Apple’s frameworks are classes.

SwiftUI slightly improves the situation. Its declarative syntax composed of structures is wholly based on protocol-oriented programming. But even if you use it instead of UIKit, you still need to create observed and environment objects, and you still need to use Foundation and other frameworks.

There are always parts in an app’s architecture that need to be shared and globally accessible. These can only be implemented with reference types. So you will still have to use classes in your projects.

Nevertheless, protocol-oriented programming can also be used with classes. It’s not just limited to value types like structures. So, even if you have to use classes for some tasks, you can still alleviate some of the problems of object-oriented programming using protocols and extensions before you reach for class inheritance.

Architecting SwiftUI apps with MVC and MVVM

It's easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

3 thoughts on “Protocol-Oriented Programming: The Best Tool of Expert iOS Developers”

  1. Not completely clear how TeamMember start to support “actions” property. Part of the code just missed. Context of article is not consistent.

    Reply
  2. Very advanced article, but what drag my attention is that for ex. a Fighter has a function attack, that modifies other character’s health, which is kind of a red light for a good architecture ?

    Reply
    • Yes, good observation. When using classes, I would not do anything like that.

      A method that modifies its parameters is a bad code smell. But this article was already quite packed, so I took a shortcut there for the sake of the explanation.

      The one in the Fighter protocol though is not a problem, since it works with a local copy and then returns it.

      Reply

Leave a Comment