Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Introducing Protocol-Oriented Programming in Swift 3

$
0
0
ProtocolOriented-feature

Protocol-Oriented Programming will help you fly!

Update Note: This tutorial has been updated to Swift 3.0 by Niv Yahel. The original tutorial was written by Erik Kerber.

Imagine you’re developing a racing game. You can drive a car, ride a motorcycle, or even fly a plane. A common approach to creating this type of application is by using object oriented design, encapsulating all of the logic inside of an object that gets inherited to all of those that share similarity.

This design approach works, but does come with some drawbacks. For example, if you add the ability to create machines that also require gas, birds that fly in the background, or anything else that may want to share game logic, there isn’t a good way to separate the functional components of vehicles into something reusable.

This scenario is where protocols really shine.

Swift has always let you specify interface guarantees on existing class, struct and enum types using protocols. This lets you interact with them generically. Swift 2 introduced a way to extend protocols and provide default implementations. Finally, Swift 3 improves operator conformance and uses these improvements for the new numeric protocols in the standard library.

Protocol are extremely powerful and can transform the way you write code. In this tutorial, you’ll explore the ways you can create and use protocols, as well as use protocol-oriented programming patterns to make your code more extensible.

You’ll also see how the Swift team was able to use protocol extensions to improve the Swift standard library itself, and how it impacts the code you write.

Getting Started

Begin by creating a new playground. In Xcode, select File\New\Playground… and name the playground SwiftProtocols. You can select any platform, since all the code in this tutorial is platform-agnostic. Click Next to choose where you would like to save it, and finally click Create.

Once your new playground is open, add the following code to it:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}
 
protocol Flyable {
  var airspeedVelocity: Double { get }
}

This defines a simple protocol Bird with properties name and canFly, as well as a Flyable protocol which defines airspeedVelocity.

In a pre-protocol world, you might have started with Flyable as a base class and then relied on object inheritance to define Bird as well as other things that fly, such as airplanes. Note that here, everything is starting out as a protocol! This allows you to encapsulate the functional concept in a way that doesn’t require a base class.

You’ll see how this makes the entire system more flexible when you start to define actual types next.

Defining Protocol-Conforming Types

Add the following struct definition to the bottom of the playground:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true
 
  var airspeedVelocity: Double {
    return 3 * flappyFrequency * flappyAmplitude
  }
}

This defines a new struct FlappyBird, which conforms to both the Bird and Flyable protocols. Its airspeedVelocity is calculated as a function of flappyFrequency and flappyAmplitude. Being flappy, it returns true for canFly. :]

Next, add the following two struct definitions to the bottom of the playground:

struct Penguin: Bird {
  let name: String
  let canFly = false
}
 
struct SwiftBird: Bird, Flyable {
  var name: String { return "Swift \(version)" }
  let version: Double
  let canFly = true
 
  // Swift is FASTER every version!
  var airspeedVelocity: Double { return version * 1000.0 }
}

A Penguin is a Bird, but cannot fly. A-ha — it’s a good thing you didn’t take the inheritance approach, and make all birds flyable after all! Using protocols allows you to define functional components and have any relevant object conform to them.

Already you can see some redundancies. Every type of Bird has to declare whether it canFly or not, even though there’s already a notion of Flyable in your system.

Extending Protocols With Default Implementations

With protocol extensions, you can define default behavior for a protocol. Add the following just below the Bird protocol definition:

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

This defines an extension on Bird that sets the default behavior for canFly to return true whenever the type is also Flyable. In other words, any Flyable bird no longer needs to explicitly declare so!

protocols-extend

Delete the let canFly = ... from FlappyBird, SwiftBird and Penguin struct declarations. You’ll see that the playground successfully builds since the protocol extension now handles that requirement for you.

Why Not Base Classes?

Protocol extensions and default implementations may seem similar to using a base class or even abstract classes in other languages, but they offer a few key advantages in Swift:

  • Because types can conform to more than one protocol, they can be decorated with default behaviors from multiple protocols. Unlike multiple inheritance of classes which some programming languages support, protocol extensions do not introduce any additional state.
  • Protocols can be adopted by classes, structs and enums. Base classes and inheritance are restricted to class types.

In other words, protocol extensions provide the ability to define default behavior for value types and not just classes.

You’ve already seen this in action with a struct. Next, add the following enum definition to the end of the playground:

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!")
    }
  }
}

As with any other value type, all you need to do is define the correct properties so UnladenSwallow conforms to the two protocols. Because it conforms to both Bird and Flyable. It also gets the default implementation for canFly!

Did you really think this tutorial involving airspeedVelocity wouldn’t include a Monty Python reference? :]

Overriding Default Behavior

Your UnladenSwallow type automatically got an implementation for canFly by virtue of conforming to the Bird protocol. However, you really want UnladenSwallow.unknown to return false for canFly. Is it possible to override the default implementation? Yes, it is. Add this to the end of your playground.

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

Now only .african and .european will return true for canFly. Test it out by adding the following to the end of your playground:

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

In this way, it is possible to override properties and methods much like you can with virtual methods in object oriented programming.

Extending Protocols

You can utilize protocols from the standard library and also define default behaviors.

Modify the Bird protocol declaration to conform to the CustomStringConvertible protocol:

protocol Bird: CustomStringConvertible {

Conforming to CustomStringConvertible means your type needs to have a description property so it acts like a String. Does that mean you now have to add this property to every current and future Bird type?

Of course, there’s an easier way with protocol extensions. Add the code underneath the Bird definition:

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

This extension will make the canFly property represent each Bird type’s description value.

To try it out, add the following to the bottom of the playground:

UnladenSwallow.african

You should see “I can fly!” appear in the assistant editor. But more notably, you just extended your own protocol!

Effects on the Swift Standard Library

You’ve seen how protocol extensions are a great way to customize and extend the capabilities. What may surprise you is how the Swift team was able to use protocols to improve the way the Swift standard library is written as well.

Add the following code to the end of your 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)

This should look pretty straightforward, and you might even be able to guess the answer that is printed. What might be surprising are the types involved. slice, for example, is not an Array of integers but an ArraySlice<Int>. This special wrapper type acts as a view into the original array and avoids costly memory allocations that can quickly add up. Similarly, reversedSlice is actually a ReversedRandomAccessCollection<ArraySlice<Int>> which is again just a wrapper type view into the original array.

secrets

Fortunately, the geniuses developing the standard library defined the map method as an extension to the Sequence protocol and all of the collection wrappers (of which there are dozens) to conform to this protocol. This makes it possible to call map on Array just as easily as it is ReversedRandomAccessCollection and not notice the difference. You will borrow this important design pattern shortly.

Off to the Races

So far you defined several Bird conforming types. Now add something totally different to the end of your playground.

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200
  }
  var name: String
  var speed: Double
}

This class that has nothing to do with birds or flying things you have defined so far. But you want to race motorcycles as well as penguins. Time to bring all of the pieces together.

Bringing it Together

It is time to unify all of these disparate types with a common protocol for racing. You can do this with out even going back and touching the original model definitions. The fancy term for this is retroactive modeling. Just add the following to your playground:

protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}
 
extension FlappyBird: Racer {
  var speed: Double {
    return airspeedVelocity
  }
}
 
extension SwiftBird: Racer {
  var speed: Double {
    return airspeedVelocity
  }
}
 
extension Penguin: Racer {
  var speed: Double {
    return 42  // full waddle speed
  }
}
 
extension UnladenSwallow: Racer {
  var speed: Double {
    return canFly ? airspeedVelocity : 0
  }
}
 
extension Motorcycle: Racer {}
 
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 3.0),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")
  ]

In this code, you first define the protocol Racer and then you make all of the different types conform. Some types, such as Motorcycle conform trivially. Others, such as UnladenSwallow need a bit more logic. In the end, you have a bunch of conforming Racer types.

With all of the types conforming, you then create an array of racers.

Top Speed

Now it’s time to write a function that determines the top speed of the racers. Add this to the end of your playground:

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

This function uses the standard library max to find the racer with the largest speed and return that. You return 0 if the user passes in an empty array in for racers.

Looks like it’s Swift 3 FTW. As if it were ever in doubt! :]

Making it more generic

There is a problem though. Suppose you want to find the top speed for a subset (slice) of racers. Adding this to your playground you get an error:

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

Swift complains it cannot subscript a value of type [Racer] with an index of type CountableClosedRange. Slicing returns one of those wrapper types.

The solution is to write your code against a common protocol instead of the concrete Array. Add the following before the topSpeed(of:) call.

func topSpeed<RacerType: Sequence>(of racers: RacerType) -> Double
    where RacerType.Iterator.Element == Racer {
  return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}

This might look a bit scary, so let’s break it down. RacerType is the generic type for this function and it can be any type that conforms to the Swift standard library’s Sequence protocol. The where clause specifies that the element type of the sequence must conform to your Racer protocol. All Sequence types have an associated type named Iterator that can loop through types of Element. The actual method body is mostly the same as before.

This method works for any Sequence type including array slices.

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

Making it More Swifty

You can do even a little better. Borrowing from the standard library play book, you can extend Sequence type itself so that topSpeed() is readily discoverable. Add the following to the end of your playground:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    return self.max(by: { $0.speed < $1.speed })?.speed ?? 0
  }
}
 
racers.topSpeed()        // 3000
racers[1...3].topSpeed() // 42

Now you have a method that is easily discoverable but only applies (and autocompletes) when you are dealing with sequences of racers.

Protocol Comparators

One Swift 3 improvement to protocols is how you create operator requirements.

Add the following to the bottom of the playground:

protocol Score {
  var value: Int { get }
}
 
struct RacingScore: Score {
  let value: Int
}

Having a Score protocol means that you can write code that treats all scores the same way. However, by having different concrete types such as RacingScore you are sure not to mix up these scores with style scores or cuteness scores. Thanks compiler!

You really want scores to be comparable so you can tell who has the high score. Before Swift 3, you needed to add global operator functions to conform to these protocols. Now you can define these static method that is part of the model. Do so now by replacing the definition of Score and RacingScore with the following:

protocol Score: Equatable, Comparable {
  var value: Int { get }
}
 
struct RacingScore: Score {
  let value: Int
 
  static func ==(lhs: RacingScore, rhs: RacingScore) -> Bool {
    return lhs.value == rhs.value
  }
 
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    return lhs.value < rhs.value
  }
}

You just encapsulated all of the logic for RacingScore in one place. Now you can compare scores, and, with the magic of protocol extension default implementations, even use operators such as greater-than-or-equal-to that you never explicitly defined.

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

Where To Go From Here?

You can download the complete playground with all the code in this tutorial
here.

You’ve seen the power of protocol-oriented programming by creating your own simple protocols and extending them using protocol extensions. With default implementations, you can give existing protocols common and automatic behavior, much like a base class but better since it can apply to structs and enums too.

In addition, protocol extensions can not only be used to extend your own protocols, but can extend and provide default behavior to protocols in the Swift standard library, Cocoa, Cocoa Touch, or any third party library.

To continue learning more about protocols, you should read the official Apple documentation.

You can view an excellent WWDC session on Protocol Oriented Programming on Apple’s developer portal for a more in-depth look into the theory behind it all.

The rationale for operator conformance can be found on the Swift evolution proposal. You might also want to learn more about Swift collection protocols and learn how to build your own.

Finally, as with any “new” programming paradigm, it is easy to get overly exuberant and use it for all the things. This interesting blog post by Chris Eidhof reminds us that we should beware of silver bullet solutions and using protocols everywhere “just because”.

Have any questions? Let us know in the forum discussion below!

The post Introducing Protocol-Oriented Programming in Swift 3 appeared first on Ray Wenderlich.


Viewing all articles
Browse latest Browse all 4370

Trending Articles