Background:
Protocol abstraction appeared in Swift as a counter to classical object inheritance in OOP. While Objective-C and other OOP languages predominantly used the "general to specific" inheritance model, Swift has promoted protocols as the primary means of achieving abstraction from the beginning, emphasizing composition over inheritance.
Problem:
Classical inheritance implies a rigid hierarchy: a tree of subclasses with compulsory extensions through override. This limits flexibility, leads to "fragile" code, complicates refactoring, and bloats base ancestor classes. Moreover, Swift does not support multiple class inheritance, meaning that code reuse is only possible through other mechanics.
Solution:
Protocol abstraction allows declaring a "set of requirements" that a type must implement. Protocols can be extended (extension) to inject common logic, bringing them closer to the concept of "mixins":
Code example:
protocol Drawable { func draw() } extension Drawable { func draw() { print("Default drawing") } } struct Circle: Drawable {} let c = Circle() c.draw() // Will output "Default drawing"
Key features:
What is the difference between protocol extension and regular implementation in a class?
Extending a protocol via extension adds a default implementation only for cases when the user hasn't implemented this method in their type. If the method is explicitly implemented in the type, that one is called.
Example:
protocol Demo { func foo() } extension Demo { func foo() { print("default") } } struct X: Demo { func foo() { print("custom") } } X().foo() // "custom"
What happens if a type implementing a protocol and its extension is accessed as protocol data?
If the protocol declares a method as mandatory (requirement), the specific type's implementation will be used even when cast to the protocol type. However, if a new (previously undeclared in the protocol) property is added in the extension, it will only be available through the extension, not through the protocol type.
Can you store instances of different structs implementing the same protocol in an array?
Yes — thanks to "existential" types (e.g., [Drawable]) you can store heterogeneous collections:
struct Tri: Drawable { func draw() { print("Triangle") } } let arr: [Drawable] = [Circle(), Tri()] arr.forEach { $0.draw() }
In the company, there was a base superclass Shape, from which all shapes (Circle, Square, Polygon) inherited. The base class grew larger because each new shape had to be supported through override. It became increasingly difficult to expand the system — each new type broke ABI and forced rewriting of existing code.
Pros:
Cons:
They started using several protocols: Drawable, Colorable, Animatable. Now each shape can easily be made "animatable and colorable" simultaneously without changing the other structures. New functionality is added through extension.
Pros:
Cons: