¿Qué es la programación orientada a protocolos en Swift y por qué Apple la impulsa frente a la herencia clásica?
Los protocolos más las extensiones le dan a Swift comportamiento compartido sin las clases base frágiles, la reutilización de un solo eje y la semántica de referencia forzada de la herencia — al precio de algunas trampas de despacho estático.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Qué es la programación orientada a protocolos en Swift y por qué Apple la impulsa frente a la herencia clásica?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.Apple le puso nombre a esto en la WWDC de 2015 — "Protocol-Oriented Programming in Swift" — y no fue una simple preferencia de estilo. Redefinió cuál debía ser la columna vertebral de un código en Swift: no un árbol de clases base de las que heredas para compartir comportamiento, sino un conjunto de protocolos a los que te adhieres, con el comportamiento compartido servido por extensiones de protocolo en lugar de por herencia. Un candidato senior debería poder explicar los problemas reales que esto resuelve, no solo recitar "prefiere la composición sobre la herencia."
#Lo que te cuesta la herencia clásica
Una jerarquía de clases te da exactamente un eje de reutilización — solo puedes heredar de una superclase — y cada subclase depende silenciosamente de los detalles de implementación de la superclase, no solo de su interfaz. Cambias un método de la clase base y cualquier subclase del árbol puede romperse de formas que el compilador no detecta; ese es el problema de la clase base frágil. Las clases también son tipos de referencia por defecto, así que un diseño centrado en la herencia tiende a arrastrar semántica de referencia y estado mutable compartido hacia lugares donde un tipo valor simple, barato y thread-safe habría sido perfectamente feliz — un Point, un Money, un struct UserPreferences no tiene por qué ser una clase solo porque quisieras que compartiera código con un tipo hermano.
#Lo que te dan en su lugar los protocolos más extensiones
Un protocolo declara un contrato; una extensión de protocolo puede aportar una implementación por defecto de ese contrato, que todo tipo conforme recibe gratis a menos que decida sobreescribirla. Como la conformidad no es herencia, tanto clases como tipos valor — structs y enums — pueden adoptar el mismo protocolo y compartir el mismo comportamiento por defecto, sin forzar nada a ser tipo referencia:
protocol Greetable {var name: String { get }func greet() -> String}extension Greetable {// Implementación por defecto: todo conformante la recibe gratis.func greet() -> String {"Hello, \(name)!"}}struct Visitor: Greetable {let name: String}enum SystemUser: Greetable {case admin(name: String)case guestvar name: String {switch self {case .admin(let name): return namecase .guest: return "Guest"}}}let attendees: [Greetable] = [Visitor(name: "Ada"), SystemUser.admin(name: "Grace"), SystemUser.guest]attendees.forEach { print($0.greet()) }
Fíjate en que un struct y un enum comparten un comportamiento sin que intervenga ninguna herencia. Los protocolos también se componen, algo que la herencia simple no permite: una función puede exigir varias capacidades no relacionadas a la vez sin forzarlas a una sola jerarquía —
protocol UniquelyIdentified {var id: String { get }}protocol Auditable {var lastModified: Date { get }}func logChange(_ item: some UniquelyIdentified & Auditable) {print("\(item.id) changed at \(item.lastModified)")}
Cualquier tipo que satisfaga ambos protocolos funciona aquí, sin importar en qué punto de una jerarquía de clases (si es que tiene alguna) se encuentre — incluyendo tipos que no son tuyos, mediante conformidad retroactiva en una extensión.
#Tipos asociados, some y any
Los protocolos que necesitan hablar de un tipo marcador de posición usan associatedtype, lo cual convierte al protocolo en una restricción genérica en lugar de una interfaz plana:
protocol Container {associatedtype Itemvar items: [Item] { get }mutating func add(_ item: Item)}struct Stack<Element>: Container {private(set) var items: [Element] = []mutating func add(_ item: Element) { items.append(item) }}// `some Container` — un tipo de retorno opaco. El compilador conoce el tipo// concreto en tiempo de compilación, así que no hay indirección ni coste// de despacho dinámico.func makeStack() -> some Container {Stack<Int>()}// `any Container` — un existencial encapsulado. Necesario cuando el tipo// concreto puede variar en tiempo de ejecución, a costa de una capa extra// de indirección.let boxed: any Container = Stack<String>()
Un protocolo con associatedtype no puede usarse como un existencial plano (any Container de hecho era ilegal antes de las mejoras de Swift 5.7 a los existenciales para muchos de estos casos) de la misma forma que un protocolo simple sí puede, precisamente porque "un Container de algo" no es un tipo lo bastante concreto como para encapsularlo con seguridad sin más maquinaria.
#Por qué Apple realmente impulsa esto
La semántica de valor elimina toda una categoría de errores de estado mutable compartido, porque las copias son independientes por defecto. La conformidad a un protocolo se puede añadir a tipos ya existentes — incluidos los que no son tuyos — sin tocar su jerarquía de clases ni recurrir a una subclase que no puedes crear. Simular (mockear) para tests pasa de ser "hereda de esta clase concreta y sobreescribe lo suficiente para interceptar el comportamiento" a ser simplemente "escribe un tipo pequeño que conforme al protocolo." Y SwiftUI en sí mismo es el mayor anuncio del paradigma: View, Shape y ViewModifier son todos protocolos con tipos asociados, no clases base, y cada vista de SwiftUI que has escrito es un tipo valor que conforma a un protocolo, nunca una subclase.
🧠 Protocol-Oriented Programming
0/2 · 0/2 answered1. Un método se añade a un protocolo solo mediante una EXTENSIÓN de protocolo (no aparece en los requisitos del protocolo), y un struct conformante define su propio método con el mismo nombre y firma. Al llamar a ese método sobre una variable tipada como el protocolo, ¿qué implementación se ejecuta?
2. ¿Cuál es la diferencia práctica principal entre `some Container` y `any Container` para un protocolo con un tipo asociado?