Protocol Oriented Programming in Swift

Protocol Oriented Programming in Swift

Protocol-Oriented Programming also known as POP is a powerful paradigm in Swift that is useful for code reusability, extensibility, and flexibility. POP focuses on protocols and protocol extensions to define code behavior. This article will dive deep into Protocol-Oriented Programming in Swift, exploring advanced techniques and real-world examples.

Why Protocol-Oriented Programming?

Using protocols, you can group similar methods, functions, and properties. Swift allows you to specify these interface guarantees on classes, structs, and enums. Base classes and inheritance are only possible for class types. Protocols in Swift have the advantage of allowing objects to conform to multiple protocols at the same time.

In this article, you will learn about:

  • Understanding protocols

  • Core Concepts of POP

  • Protocol Extensions and Default Implementations

  • Protocols Composition

  • Protocol Inheritance

  • Generics and Protocols

  • Where to go next?

Understanding Protocols :

Protocols define a blueprint for methods, properties, and other requirements. They don't provide an implementation but allow you to create conforming types. Let's start with a simple example:

protocol Vehicle {
    var numberOfWheels: Int { get }
    func start()
    func stop()
}

struct Car: Vehicle {

    var numberOfWheels: Int { return 4 }

    func start() {
        print("Car starting...")
    }

    func stop() {
        print("Car stopping...")
    }
}

Here, we defined a Vehicle protocol with three requirements: a read-only property numberOfWheels and two methods start() and stop(). In order to use Vehivle protocol, we create a struct called Car that conforms to this protocol.

Core Concepts of POP :

The Protocol-Oriented Programming approach encourages shifting mindsets from thinking in terms of classes and inheritance to thinking about behavior and composition. This approach results in cleaner, more modular, and highly maintainable code, enhancing the overall quality and extensibility of your iOS applications. By implementing the principles of protocols, extensions, composition, generic behaviour, and inheritance, you can unlock the full potential of Protocol-Oriented Programming and create more elegant and efficient iOS applications.

How does Protocol-Oriented Programming contribute to iOS applications?

  • Code Reusability and Modularity: POP encourages the creation of small, focused protocols that capture specific behaviors. This modular approach allows developers to easily mix and match behaviors across different parts of the application.

  • Flexibility and Adaptability: As usual, requirements can change frequently due to evolving user needs or platform updates. By designing your codebase around protocols, you can easily adapt to these changes. Adding new behavior or modifying existing behavior becomes more straightforward, as you can extend protocols and adopt them in various parts of your app.

  • Composition over Inheritance: POP promotes composition over class inheritance. In iOS applications, this means that you can combine behaviors from multiple protocols to create new and complex functionalities. This approach results in less tightly coupled code and reduces the limitations of single inheritance present in traditional OOP paradigm.

  • Unit Testing and Testability: POP greatly enhances the testability of iOS applications. You can use protocol extensions to create mock implementations for protocols, enabling more focused and isolated unit testing. This separation of concerns makes it easier to write thorough and effective tests for your application's components.

  • Enhanced Code Maintainability: In large iOS codebases, maintaining and extending code can become challenging. By designing with protocols, you create a structure that is less prone to bugs, easier to refactor, and more intuitive to understand. Everyone can quickly grasp the architecture and contribute effectively.

Protocol Extensions and Default Implementations :

Protocol extensions allow you to provide default implementations for protocol methods. This helps in reducing boilerplate code when multiple types conform to the same protocol. Let's extend our Vehicle protocol to include a default implementation for the start() method:

extension Vehicle {
    func start() {
        print("Vehicle starting...")
    }
}

Now, if a type conforms to the Vehicle protocol but doesn't provide its implementation for the start() method, it will use the default implementation:

struct Motorcycle: Vehicle {
    var numberOfWheels: Int { return 2 }

    // No need to implement the start() method, using the default implementation.

    func stop() {
        print("Motorcycle stopping...")
    }
}

Protocol extensions allow you to define default implementations for protocol methods and properties. This means that any type conforming to the protocol automatically gains access to the default behavior without needing to provide its own implementation.

Protocols Composition

Protocol composition is a better approach to split large protocols into smaller ones, and provide a distinct property (like a delegate) for each one. These methods have clear semantic groupings. It could easily be broken up into multiple protocols and your code could have a property for each one.

This allows you to create highly modular and flexible code. Consider the following example:

protocol Flying {
    func takeOff()
    func land()
}

protocol Swimming {
    func dive()
    func surface()
}

// Protocol Composition: AdvancedVehicle
typealias AdvancedVehicle = Vehicle & Flying & Swimming

struct Seaplane: AdvancedVehicle {

    // MARK: - Vehicle protocol
    var numberOfWheels: Int { return 4 }

    func start() {
        print("Seaplane starting...")
    }

    func stop() {
        print("Seaplane stopping...")
    }

    // MARK: - Flying protocol
    func takeOff() {
        print("Seaplane taking off...")
    }

    func land() {
        print("Seaplane landing...")
    }

    // MARK: - Swimming protocol
    func dive() {
        print("Seaplane diving...")
    }

    func surface() {
        print("Seaplane surfacing...")
    }
}

In this example, we have defined two more protocols, Flying and Swimming in addition with Vehicle protocol, each describing the behavior for flying and diving behaviours, respectively. We then create a new protocol AdvancedVehicleby combining the three protocols using protocol composition.

The Seaplane class implements the AdvancedVehicle protocol, which means the class can now handle basic, flying and swimming functionalities seamlessly.

Protocol Inheritance :

A protocol can inherit from other protocols and then add further requirements on top of the requirements it inherits. In the following example, the protocol ElectricVehicle inherits from the Vehicle protocol. Here, ElectricVehicle adding new requirements for battery management.

protocol ElectricVehicle: Vehicle {
    var batteryLevel: Double { get }
    func recharge()
}

struct ElectricCar: ElectricVehicle {

    var numberOfWheels: Int { return 4 }
    var batteryLevel: Double = 100.0

    func start() {
        print("Electric car starting...")
    }

    func stop() {
        print("Electric car stopping...")
    }

    func recharge() {
        print("Electric car recharging...")
    }
}

Protocol inheritance allows for specialization of behavior. By creating protocols that inherit from a base protocol, you can define specialized requirements that are relevant to specific use cases. This specialization keeps the codebase clean and prevents unnecessary requirements in unrelated types.

Generics and Protocols :

Protocols can also be used in combination with generics to create more flexible and reusable code.

For instance, you could create a generic function that performs sorting on an array of elements conforming to a Comparable protocol. This function works with any type that can be compared, providing a flexible and generic solution.

Let's consider an example where we define a protocol that requires a generic type:

protocol Stackable {
    associatedtype Element
    mutating func push(_ item: Element)
    mutating func pop() -> Element?
}

struct Stack<T>: Stackable {
    private var items = [T]()

    mutating func push(_ item: T) {
        print("pushed item: \(item)")
        items.append(item)
    }

    mutating func pop() -> T? {
        let item = items.popLast()
        print("popped item: \(String(describing: item))")
        return item
    }
}

var numberStack = Stack<Int>()
numberStack.push(10) // Output: pushed item: 10
numberStack.push(20) // Output: pushed item: 20
numberStack.pop() // Output: popped item: Optional(20)

By using associated types, we made the Stack protocol generic, allowing it to work with any data type.

Where to go next?

Protocol-Oriented Programming (POP) in Swift is a powerful and modern paradigm that emphasizes protocol composition and extensions to promote code reusability, flexibility, and maintainability. Unlike traditional Object-Oriented Programming (OOP) with class inheritance, POP favours composition over inheritance, allowing types to conform to multiple protocols and share behaviour in a more modular and extensible manner.

Moreover, we recommend that you compare POPs with OOPs to gain a deeper understanding of the protocols approach.