Protocol-Oriented Programming in Swift
Swift’s unique approach to programming offers developers a robust set of tools for building clean, maintainable applications. One of the most powerful concepts Swift introduces is Protocol-Oriented Programming (POP). Unlike traditional Object-Oriented Programming (OOP), which relies on class inheritance to share behavior, Protocol-Oriented Programming focuses on protocols as the foundation of design. This approach provides a more flexible, scalable, and safer way to structure code, especially in comparison to inheritance-based solutions.
The Problem with Abstract Classes in OOP
In traditional Object-Oriented Programming, when we want to define a base class with common behavior for its subclasses, we often use abstract classes. Abstract classes define methods that must be implemented by subclasses. However, this can lead to problems if a subclass forgets to implement a method or fails to provide its own implementation. In such cases, we typically use a fatalError() in the base class to force the subclasses to implement the required methods. This approach has serious drawbacks, especially when it comes to maintainability and safety.
For example, imagine a base class called Vehicle that has a method startEngine(). If we don’t provide a default implementation and rely on subclasses to implement this method, we could end up with a situation where the method isn’t implemented, resulting in a runtime crash.
class Vehicle {
func startEngine() {
fatalError("Must be overridden in a subclass")
}
}
class Car: Vehicle {
// startEngine is not implemented, causing a runtime crash
}
let car = Car()
car.startEngine() // This will crash at runtime with fatalErrorIn this example, because startEngine() is not implemented in the Car class, the program crashes when the method is called. The use of fatalError() creates a hard failure point in the code, which is not ideal in production environments. The rigid nature of inheritance in OOP often leads to tight coupling and limits flexibility.
Protocols Provide Flexibility and Reusability
Protocol-Oriented Programming changes this approach by moving away from the idea of inheritance and focusing on protocols. In Swift, protocols allow you to define behavior that can be adopted by any type — classes, structs, or enums — without the need for a class hierarchy. This makes protocols incredibly flexible and reusable across different parts of your application.
Unlike abstract classes, where behavior is defined in one base class and inherited by its subclasses, protocols are more versatile. A single type can conform to multiple protocols, and each protocol can define distinct sets of behaviors that the type adopts. This compositional model is much more modular and avoids the pitfalls of rigid inheritance.
protocol Drivable {
func startEngine()
func stopEngine()
}
extension Drivable {
func startEngine() {
print("Engine started")
}
func stopEngine() {
print("Engine stopped")
}
}
struct Car: Drivable {
// Uses the default implementation of startEngine and stopEngine from Drivable
}
let car = Car()
car.startEngine() // Output: Engine startedHere, the Car struct conforms to the Drivable protocol, and thanks to protocol extensions, it automatically inherits the startEngine() and stopEngine() methods without needing to implement them explicitly. This is a huge improvement over abstract classes, where each subclass would be forced to implement these methods manually or risk a runtime crash.
Protocols Are Even More Powerful with Structs and Enums
One of the most significant advantages of Protocol-Oriented Programming is that protocols can be adopted not only by classes but also by structs and enums, which are value types in Swift. This offers a major benefit over OOP, where abstract classes and inheritance are typically restricted to reference types (classes). With POP, structs and enums can also take advantage of default protocol implementations, enabling you to write more expressive and flexible code.
Get Max Sashcheka’s stories in your inbox
Join Medium for free to get updates from this writer.
For example, let’s consider a scenario where you need a type to have the ability to be “resizable”. With protocols, you can define this behavior and allow structs and enums to adopt it seamlessly:
protocol Resizable {
func resize(to size: Double)
}
extension Resizable {
func resize(to size: Double) {
print("Resizing to \(size) units.")
}
}
struct Rectangle: Resizable {
var width: Double
var height: Double
}
let rectangle = Rectangle(width: 5, height: 10)
rectangle.resize(to: 20) // Output: Resizing to 20 units.Here, the Rectangle struct adopts the Resizable protocol and gains the resize(to:) method with the default implementation. Unlike in OOP, where you’d typically have to create an inheritance chain, POP allows you to achieve the same result in a much cleaner and more flexible way, even with structs, which are value types.
Composability and Protocol Composition
A key feature of Protocol-Oriented Programming is the ability to compose protocols. In OOP, composition is often limited by the constraints of class inheritance. In POP, you can combine multiple protocols into one, allowing a type to conform to several protocols and thus adopt multiple behaviors. This increases the flexibility of your designs and enables more granular control over the functionality of types.
For example, imagine you have two protocols: one for being Drivable and another for being Flyable. In OOP, if you had to create a class hierarchy, you might struggle with how to model a FlyingCar or FlyingBoat. With POP, you can simply combine the two protocols to create a type that conforms to both behaviors.
protocol Flyable {
func fly()
}
protocol Drivable {
func drive()
}
struct FlyingCar: Flyable, Drivable {
func fly() {
print("Flying the car!")
}
func drive() {
print("Driving the car!")
}
}
let flyingCar = FlyingCar()
flyingCar.drive() // Output: Driving the car!
flyingCar.fly() // Output: Flying the car!In this case, FlyingCar can both fly and drive because it conforms to both the Flyable and Drivable protocols. This compositional approach is much more flexible and powerful than trying to force these behaviors into a rigid class hierarchy.
Conclusion
Protocol-Oriented Programming in Swift offers a more flexible, modular, and maintainable alternative to traditional Object-Oriented Programming. Unlike abstract classes, where we rely on fatalError() to force subclasses to implement methods, protocols provide default behavior through extensions, reducing the likelihood of runtime crashes. Additionally, protocols can be adopted by any type—classes, structs, or enums—allowing for greater flexibility and the ability to compose multiple behaviors. This makes Protocol-Oriented Programming particularly powerful when building scalable, maintainable systems in Swift.
By focusing on protocols and leveraging the power of protocol extensions and composition, developers can create clean, reusable, and flexible code. Whether you’re working with classes, structs, or enums, POP enables you to build software that’s not only more robust but also easier to extend and maintain over time.










