Protocol Oriented Programming in Swift 5.1

Protocols are a fundamental property of Swift. They play an important role in standard Swift libraries and are a common way to code abstraction. In many ways, they are similar to interfaces in other programming languages.

In this tutorial, we will introduce you to an application development approach called protocol-oriented programming, which has become almost the core of Swift. This is really what you need to understand when learning Swift!

In this guide, you:

  • understand the difference between object-oriented and protocol-oriented programming;
  • understand the standard protocol implementations;
  • Learn how to extend the functionality of the standard Swift library;
  • Learn how to extend protocols with generics.

Getting started


Imagine that you are developing a racing game. Your players can drive cars, motorcycles and airplanes. And even flying on birds, this is a game, right? The main thing here is that there is a dofiga of all kinds of “things” on which you can drive, fly, etc.

A common approach to developing such applications is object-oriented programming. In this case, we enclose all the logic in some base classes, from which we inherit from thereafter. Base classes, therefore, must contain the logic of "lead" and "pilot."

We begin development by creating classes for each vehicle. We will postpone the “Birds” until later, we will return to them a little later.

We see that Car and Motorcyclevery similar in functionality, so we create the base class MotorVehicle . Car and Motorcycle will be inherited from MotorVehicle. In the same way, we create the Aircraft base class , from which we will create the Plane class .

You think that everything is fine, but - bam! - The action of your game takes place in the XXX century, and some cars can already fly .

So, we had a gag. Swift does not have multiple inheritance. How can your flying cars be inherited from both MotorVehicle and Aircraft? Create another base class that combines both functionalities? Most likely not, since there is no clear and simple way to achieve this.

And what will save our game in this terrible situation? Protocol-oriented programming is in a hurry to help!

What is protocol-oriented programming like?


Protocols allow you to group similar methods, functions, and properties for classes, structures, and enumerations. However, only classes allow you to use inheritance from the base class.

The advantage of protocols in Swift is that an object can conform to multiple protocols.

When using this method, your code becomes more modular. Think of protocols as building blocks of functionality. When you add new functionality to an object, making it conform to a certain protocol, you are not making a completely new object from scratch, it would be too long. Instead, you add different building blocks until the object is ready.

Moving from a base class to protocols will solve our problem. With protocols, we can create a FlyingCar class that matches both MotorVehicle and Aircraft. Nice one, huh?

Let's do the code


Run Xcode, create a playground, save it as SwiftProtocols.playground, add this code:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}


Compile with Command-Shift-Return to make sure everything is in order.

Here we define a simple Bird protocol , with the name and canFly properties . Then we define the Flyable protocol with the airspeedVelocity property .

In the “pre-protocol era”, the developer would start with the Flyable class as the base class, and then, using inheritance, would define Bird and everything else that could fly.

But in protocol-oriented programming, it all starts with a protocol. This technique allows us to encapsulate a sketch of a functional without a base class.

As you will now see, this makes the type design process much more flexible.

Determine the type corresponding to the protocol


Add this code at the bottom of the playground:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    3 * flappyFrequency * flappyAmplitude
  }
}


This code defines a new FlappyBird structure that conforms to both the Bird protocol and the Flyable protocol. Its airspeedVelocity property is a work of flappy Frequency and flappy Amplitude. The canFly property returns true.

Now add the definitions of two more structures:

struct Penguin: Bird {
  let name: String
  let canFly = false
}

struct SwiftBird: Bird, Flyable {
  var name: String { "Swift \(version)" }
  let canFly = true
  let version: Double
  private var speedFactor = 1000.0
  
  init(version: Double) {
    self.version = version
  }

  // Swift is FASTER with each version!
  var airspeedVelocity: Double {
    version * speedFactor
  }
}


Penguin is a bird but cannot fly. It's good that we do not use inheritance and did not make all the birds flyable !

When using protocols, you define the components of the functional and make all suitable objects conform to the protocol.

Then we define SwiftBird , but in our game there are several different versions of it. The larger the version number, the greater its airspeedVelocity , which is defined as a computed property.

However, there is some redundancy. Each type of Bird must define an explicit definition of the canFly property, although we have a definition of the Flyable protocol. It looks like we need a way to determine the default implementation of protocol methods. Well, there are protocol extensions for this.

Extending the protocol with default behavior


Protocol extensions let you set the default protocol behavior. Write this code immediately after the Bird protocol definition:

extension Bird {
  // Flyable birds can fly!
  var canFly: Bool { self is Flyable }
}


This code defines the extension of the Bird protocol . This extension determines that the canFly property will return true when the type conforms to the Flyable protocol . In other words, any Flyable bird no longer needs to explicitly set canFly.

Now remove let canFly = ... from the definitions of FlappyBird, Penguin, and SwiftBird. Compile the code and make sure everything is in order.

Let's do the transfers


Enumerations in Swift may conform to protocols. Add the following enumeration definition:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}


By defining the appropriate properties, UnladenSwallow conforms to two protocols - Bird and Flyable. In this way, the default definition for canFly is implemented.

Overriding the default behavior


Our type UnladenSwallow , according to the Bird protocol , automatically received an implementation for canFly . We, however, need UnladenSwallow.unknown to return false for canFly.

Add the following code below:

extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

Now only .african and .european will return true for canFly. Check it out! Add the following code at the bottom of our playground:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

Compile playground and check the received values ​​with the ones indicated in the comments above.

Thus, we redefine properties and methods in much the same way as using virtual methods in object-oriented programming.

Expanding protocol


You can also make your own protocol conform to another protocol from the Swift standard library and define default behavior. Replace the Bird protocol declaration with the following code:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

Compliance with the CustomStringConvertible protocol means that your type must have a description property. Instead of adding this property in the Bird type and in all its derivatives, we define the protocol extension CustomStringConvertible, which will be associated only with the Bird type.

Type UnladenSwallow.african at the bottom of the playground. Compile and you will see “I can fly”.

Protocols in Swift Standard Libraries


As you can see, protocols are an effective way to extend and customize types. In the Swift standard library, this property is also widely used.

Add this code to the playground:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()

let answer = reversedSlice.map { $0 * 10 }
print(answer)

You probably know what this code will output, but you might be surprised at the types used here.

For example, slice is not Array, but ArraySlice. This is a special “wrapper” that provides an efficient way to work with parts of the array. Accordingly, reversedSlice is a ReversedCollection <ArraySlice>.

Fortunately, the map function is defined as an extension to the Sequence protocol, which corresponds to all collection types. This allows us to apply the map function to both Array and ReversedCollection and not notice the difference. Soon you will take advantage of this useful trick.

On your marks


So far, we have identified several types that comply with the Bird protocol. Now we will add something completely different:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

This type has nothing to do with birds and flights. We want to arrange a motorcycle race with penguins. It's time to bring this strange company to the start.

Putting it all together


In order to somehow unite such different racers, we need a common protocol for racing. We can do all this without even touching all the types that we created before, with the help of a wonderful thing called retroactive modeling. Just add this to the playground:

// 1
protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension SwiftBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension Penguin: Racer {
  var speed: Double {
    42  // full waddle speed
  }
}

extension UnladenSwallow: Racer {
  var speed: Double {
    canFly ? airspeedVelocity : 0.0
  }
}

extension Motorcycle: Racer {}

// 3
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 5.1),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")]

Here's what we do here: first, define the Racer protocol. This is all that can participate in races. Then we cast all of our previously created types to the Racer protocol. And finally, we create an Array that contains instances of each of our type.

Compile the playground so that everything is in order.

Top speed


We write a function to determine the maximum speed of the riders. Add this code at the end of the playground:

func topSpeed(of racers: [Racer]) -> Double {
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers) // 5100

Here we use the max function to find the rider at maximum speed and return it. If the array is empty, then 0.0 is returned.

Making the function more general


Suppose that the Racers array is large enough, and we need to find the maximum speed not in the entire array, but in some part of it. The solution is to change topSpeed ​​(of :) so that it takes as an argument not specifically an array, but everything that conforms to the Sequence protocol.

Replace our implementation of topSpeed ​​(of :) as follows:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

  1. RacersType is the generic argument type of our function. It can be anything that conforms to the Sequence protocol.
  2. where determines that Sequence content must comply with the Racer protocol.
  3. The body of the function itself remains unchanged.

Check by adding this at the end of our playground:

topSpeed(of: racers[1...3]) // 42

Now our function works with any type that meets the Sequence protocol, including ArraySlice.

Making the function more “swift”


Secret: You can do even better. Add this at the very bottom:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

And now we have expanded the Sequence protocol itself with topSpeed ​​(). It is applicable only when Sequence contains the Racer type.

Protocol Comparators


Another feature of Swift protocols is how you define the equality operators of objects or their comparison. We write the following:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

With the Score protocol, you can write code that treats all elements of this type in one way. But if you get a very specific type, such as RacingScore, then you will not confuse it with other derivatives of the Score protocol.

We want the scores to be compared to see who has the highest score. Prior to Swift 3, developers needed to write global functions to define an operator for a protocol. Now we can define these static methods in the model itself. We do this by replacing the definitions of Score and RacingScore as follows:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

We have all the logic for RacingScore in one place. The Comparable protocol requires you to define an implementation only for the less than function. All other comparison functions will be implemented automatically, based on the implementation of the "less than" function that we created.

Testing:

RacingScore(value: 150) >= RacingScore(value: 130) // true

Making changes to the object


So far, each example has demonstrated how to add functionality. But what if we want to make a protocol that changes something in an object? This can be done using mutating methods in our protocol.

Add a new protocol:

protocol Cheat {
  mutating func boost(_ power: Double)
}

Here we define a protocol that enables us to cheat. How? Arbitrarily changing the contents of boost.

Now create a SwiftBird extension that conforms to the Cheat protocol:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

Here we implement the boost (_ :) function, increasing the speedFactor by the transmitted value. The mutating keyword makes the structure understand that one of its values ​​will be changed by this function.

Check it out!
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

Conclusion


Here you can download the full source code for the playground.

You learned about the possibilities of protocol-oriented programming by creating simple protocols and increasing their capabilities with the help of extensions. Using the default implementation, you give the protocols the appropriate “behavior”. Almost like with base classes, but only better, since all this also applies to structures and enumerations.

You also saw that the protocol extension applies to the underlying Swift protocols.

Here you will find the official protocol guide .

You can also watch an excellent WWDC lecture on protocol-oriented programming.

As with any programming paradigm, there is a danger of getting carried away and starting to use the protocols left and right. Here's an interesting note about the dangers of silver bullet style decisions.

Source: https://habr.com/ru/post/undefined/


All Articles