Protocol-Oriented Programming in Swift

Updated Mar 17, 2023#paradigms#swift

Swift protocols play a leading role in the structure of standard library and a common method of abstraction. They provide a similar experience to interfaces that some other languages have.

Protocol-oriented programming has become somewhat a fundamental in Swift. It is important you need to grasp following:

  • Difference from object-oriented programming
  • Protocol extensions, protocol inheritance, protocol composition
  • Protocols with associated types
  • Defining protocol-conforming types
  • Extending protocols with default implementations
  • Extending protocols in Apple or third-party libraries
  • Extending protocols conditionally using generic where clauses
  • Checking for protocol conformance (is, as)
  • Define optional requirements for protocols (@objc optional)

Protocols allow you to group similar methods, functions and properties. Swift lets you specify these interface guarantees on class, struct and enum types. Only class types can use base classes and inheritance.

An advantage of protocols in Swift is that objects can conform to multiple protocols.

When writing an app this way, your code becomes more modular. Think of protocols as building blocks of functionality. When you add new functionality by conforming an object to a protocol, you don’t build a whole new object. That’s time-consuming. Instead, you add different building blocks until your object is ready.

Protocol extensions may seem quite similar to using a base class, but there are several benefits of using protocol extensions. These include, but are not necessarily limited to:

  • Since classes, structures and, enums can conform to more than one protocol, they can take the default implementation of multiple protocols. This is conceptually similar to multiple inheritance in other languages.
  • Protocols can be adopted by classes, structures, and enums, whereas base classes and inheritance are available for classes only.

Here is a list of advantages of protocols and how it differs with inheritance:

Features Inheritance Protocols
Interface reuse Inherit from superclass Protocol extensions
Customization Overriding + while maintaining invariants Implement requirements + override defaults
Implementation reuse Inherit from superclass Adopt protocols
Usable with value types No Yes
Modeling flexibility Upfront modeling + exclusive hierachies Retroactive modeling + ad-hoc hierachies

Retroactive Modeling

Rather than having to lock ourselves into inflexible inheritance hierarchies and being forced to settle on key abstractions early in the design process, Swift protocols and protocol extensions enable retroactive modeling. We can start with concrete code and work our way to abstractions when we need to assign a role or a persona to entities or concepts in the domain.

Retroactive modeling is the practice of using existing types to represent new concepts, without modifying those types. This technique is important for reusing existing structures, while maintaining compatibility with the current usage. Swift supports retroactive modeling through the use of extensions.

Extensions enable you to add new functionality to existing types, without the need to have access to the original source code. Swift extensions are similar to categories in Objective-C, and can be used to extend a class, struct, enum, or protocol.

Retroactive modeling saves us from having to decide on key abstractions early in the design process.

Protocol Composition

It can be useful to require a type to conform to multiple protocols at the same time. You can combine multiple protocols into a single requirement with a protocol composition. Protocol compositions behave as if you defined a temporary local protocol that has the combined requirements of all protocols in the composition. Protocol compositions don’t define any new protocol types.

Protocol compositions have the form SomeProtocol & AnotherProtocol. You can list as many protocols as you need, separating them with ampersands (&). In addition to its list of protocols, a protocol composition can also contain one class type, which you can use to specify a required superclass.

protocol Player {
    func play()
}

protocol Stoppable {
    func stop()
}

class MusicPlayer: Player, Stoppable {
    func play() {
        print("Playing music...")
    }
    
    func stop() {
        print("Stopping music...")
    }
}

class VideoPlayer: Player, Stoppable {
    func play() {
        print("Playing video...")
    }
    
    func stop() {
        print("Stopping video...")
    }
}

let musicPlayer = MusicPlayer()
let videoPlayer = VideoPlayer()

// Create a protocol composition of Player and Stoppable
var playerAndStoppable: Player & Stoppable = musicPlayer

// Call methods on the protocol composition
playerAndStoppable.play() // Output: Playing music...
playerAndStoppable.stop() // Output: Stopping music...

// Update the protocol composition to use the VideoPlayer
playerAndStoppable = videoPlayer

// Call methods on the updated protocol composition
playerAndStoppable.play() // Output: Playing video...
playerAndStoppable.stop() // Output: Stopping video...

Examples

This example demonstrates the power of protocol oriented in Swift. By defining behavior in a protocol and implementing it in a type-specific way, we can create flexible and extensible code that’s easy to maintain and test. We can also take advantage of Swift’s support for protocol extensions to provide default implementations for shared behavior, further reducing code duplication.

protocol Animal {
    var name: String { get }
    var numberOfLegs: Int { get }
    
    func makeSound()
}

extension Animal {
    func makeSound() {
        print("Unknown sound")
    }
}

struct Dog: Animal {
    var name: String
    var numberOfLegs: Int
    
    func makeSound() {
        print("Woof!")
    }
}

struct Cat: Animal {
    var name: String
    var numberOfLegs: Int
    
    func makeSound() {
        print("Meow!")
    }
}

let dog = Dog(name: "Fido", numberOfLegs: 4)
let cat = Cat(name: "Whiskers", numberOfLegs: 4)

dog.makeSound() // Output: Woof!
cat.makeSound() // Output: Meow!

In this example, we define a protocol called Animal that defines two properties (name and numberOfLegs) and a method (makeSound()). We also provide a default implementation for makeSound() in an extension of the Animal protocol, which prints "Unknown sound".

We then define two structs (Dog and Cat) that conform to the Animal protocol and provide their own implementation of makeSound(). Finally, we create instances of Dog and Cat and call their makeSound() methods, which print "Woof!" and "Meow!", respectively.