Protocolos y extensiones
Al terminar: Escribir protocolos, extensiones e implementaciones por defecto, y pensar en protocolos.
Hasta ahora el reglamento de puntos (3 por victoria, 1 por empate, 0 por derrota) estaba escrito dentro de una función. Al terminar esta lección vivirá en un protocolo Reglamento, y el torneo recibirá el suyo desde fuera: con ReglamentoLiga puntúa 3/1/0 y con un reglamento antiguo, 2/1/0, sin tocar la clase Torneo.
Temas: protocolos, extensiones, implementaciones por defecto y programación orientada a protocolos. En la lección siguiente, Equatable y Hashable, usarás las extensiones sobre tipos que ya existen y verás por qué Jugador y Equipo se pueden comparar y usar como clave de un diccionario.
Protocolo: un contrato sin cuerpo
Un protocolo (protocol) lista lo que un tipo debe ofrecer, sin decir cómo. Cada tipo que lo adopta rellena esa lista. struct, enum y class pueden adoptar protocolos; en Kotlin, una interfaz solo la implementan clases y objetos.
Extensiones: darle cuerpo al protocolo
Un protocolo no puede llevar código. Para eso están las extensiones (extension): añaden métodos y propiedades calculadas a un tipo, o a un protocolo, desde fuera. Una propiedad calculada no guarda ningún dato: cada vez que la lees, Swift hace la cuenta que le escribiste (var doble: Int { 2 * base }). Una extensión no puede añadir propiedades guardadas (con memoria propia). Y lo private de un tipo solo lo ve una extensión que esté en el mismo fichero que el tipo; una extensión escrita en otro fichero recibe 'x' is inaccessible due to 'private' protection level.
Los ejemplos y el ejercicio de esta lección se escriben en tu Playground, el de Tu primer paquete de Swift: reutiliza el mismo y borra lo anterior.
protocol Descriptible {
var nombre: String { get } // requisito: una propiedad que se pueda leer
func saludo() -> String // requisito: una función
}
extension Descriptible {
func saludo() -> String { "Hola, \(nombre)" } // valor por defecto del requisito
func despedida() -> String { "Adiós, \(nombre)" } // NO está declarado en el protocolo
var letras: Int { nombre.count } // propiedad calculada: cuenta las letras cada vez que se lee
}
struct Arbitro: Descriptible {
let nombre: String // solo falta `nombre`: el resto viene de la extensión
}
struct Entrenador: Descriptible {
let nombre: String
func saludo() -> String { "Míster \(nombre)" }
func despedida() -> String { "Hasta pronto, míster" }
}
let rosa = Arbitro(nombre: "Rosa")
let leo = Entrenador(nombre: "Leo")
print(rosa.saludo()) // sobre el tipo concreto
print(leo.saludo())
let comoProtocolo: [any Descriptible] = [rosa, leo] // a través del protocolo
for d in comoProtocolo { print(d.saludo(), "|", d.despedida()) }
print(leo.despedida()) // sobre el tipo concreto
print(rosa.letras) // la calculada, también gratis{ get } significa «hay que poder leerla»; la adopte una constante let o una propiedad calculada. any Descriptible es «algún tipo que adopta el protocolo». Al ejecutar los print, verás esto:
Hola, Rosa
Míster Leo
Hola, Rosa | Adiós, Rosa
Míster Leo | Adiós, Leo
Hasta pronto, míster
4Mira la cuarta línea: saludo() (un requisito) es el del Entrenador, pero despedida() no es un requisito, y a través de any Descriptible se ejecuta la de la extensión, no la que escribió Entrenador. La quinta línea, sobre el tipo concreto, sí muestra la del Entrenador. Es el error típico de esta lección.
Pensar en protocolos, no en herencia
Los structs y enums no heredan, y una clase solo hereda de una. La programación orientada a protocolos parte de otra idea: en vez de un árbol de clases, tipos pequeños que adoptan varios protocolos y reciben comportamiento de sus extensiones. Es también lo que hiciste en Kotlin con la composición: Torneo tiene un reglamento, no es un reglamento.
Cuatro reglas para elegir:
| Situación | Herramienta |
|---|---|
| Varios tipos hacen lo mismo de distinta manera | protocolo |
| Un comportamiento por defecto que casi todos comparten | extensión del protocolo |
| Un dato que debe cambiarse por otro desde fuera | propiedad de tipo any Protocolo |
| Hace falta identidad y estado compartido | class |
Error típico: el método que no era un requisito
protocol Reglamento {
var puntosPorVictoria: Int { get }
}
extension Reglamento {
func resumen() -> String { "victoria: \(puntosPorVictoria)" }
}
struct Personalizado: Reglamento {
let puntosPorVictoria = 2
func resumen() -> String { "el mío" }
}
let r: any Reglamento = Personalizado()
print(r.resumen()) // victoria: 2 (no «el mío»)protocol Reglamento {
var puntosPorVictoria: Int { get }
func resumen() -> String // ahora SÍ es un requisito
}
extension Reglamento {
func resumen() -> String { "victoria: \(puntosPorVictoria)" }
}
// Con el requisito declarado, a través de any Reglamento se ejecuta el resumen() del tipo concreto: «el mío».Por qué falla. Swift solo busca el método del tipo concreto para los requisitos del protocolo. Un método que vive solo en la extensión se resuelve por el tipo con el que lo miras: any Reglamento, y ahí solo existe el de la extensión. No hay error ni aviso: el programa hace algo distinto de lo que escribiste. Es como una función de extensión de Kotlin (fun Reglamento.resumen()): se resuelve por el tipo con el que miras el valor y no se sobrescribe. Un miembro de una interfaz de Kotlin, en cambio, siempre se resuelve por el objeto real.
Kotlin ↔ Swift
| Kotlin (el tuyo) | Swift |
|---|---|
interface Reglamento { fun puntosPor(…) = when {…} } | protocol Reglamento + extension Reglamento { … } con el cuerpo |
val puntosPorDerrota get() = 0 en la interfaz | var puntosPorDerrota: Int { 0 } en la extensión |
object ReglamentoLiga (una instancia) | struct ReglamentoLiga sin datos que cambian: todas las copias son iguales |
abstract class ReglamentoPorPuntos | no existe: struct ReglamentoPersonalizado que adopta el protocolo |
val reglamento: Reglamento | let reglamento: any Reglamento |
| Todo miembro de la interfaz se resuelve por el objeto real | Solo los requisitos; lo demás (como una función de extensión de Kotlin), por el tipo con el que lo miras |
Un protocolo público, con valores por defecto, y quien lo recibe
El paso de esta lección usa unas piezas más, que conviene ver antes en un ejemplo pequeño:
public: hace visible el protocolo y sus tipos desde otro módulo (el programa de consolatorneo-clies otro módulo). Unpublic init() {}vacío permite crear un struct desde fuera.var maximoDiario: Int { 20 }en una extensión: una propiedad calculada que siempre devuelve 20, y que cada tipo puede sustituir por la suya.condición ? a : b: el operador ternario, una forma corta deif: si la condición se cumple valeay, si no,b. Kotlin no lo tiene (allí se escribeif (c) a else b).CustomStringConvertible: protocolo con un solo requisito,description, el texto que sale conprint(valor).init(tarifa: any Tarifa = TarifaNormal()): un parámetro con valor por defecto cuyo tipo es el protocolo;final classes una clase que no admite herederas.
Todo junto, en otro dominio: las tarifas de un aparcamiento. No es tu código de Marcador; pruébalo en tu Playground.
// El contrato: lo que toda tarifa debe ofrecer.
public protocol Tarifa {
var precioPorHora: Int { get } // requisito sin valor por defecto
var maximoDiario: Int { get } // requisito con valor por defecto (abajo)
func precio(horas: Int) -> Int // requisito con valor por defecto (abajo)
}
// La extensión da el cuerpo por defecto.
extension Tarifa {
public var maximoDiario: Int { 20 } // propiedad calculada: siempre 20
public func precio(horas: Int) -> Int {
let total = horas * precioPorHora // usa el requisito: cada tipo pone el suyo
return total > maximoDiario ? maximoDiario : total // nunca más del máximo
}
}
// Un struct sin datos que cambian; también adopta CustomStringConvertible.
public struct TarifaNormal: Tarifa, CustomStringConvertible {
public let precioPorHora = 3
public init() {} // vacío y public: se crea desde otro módulo
public var description: String { "Tarifa normal (\(precioPorHora) €/h)" } // lo que imprime print
}
// Otro tipo que adopta el mismo protocolo, con datos que se dan al crearlo.
public struct TarifaNocturna: Tarifa {
public let precioPorHora: Int
public init(precioPorHora: Int) { self.precioPorHora = precioPorHora }
}
public final class Parking {
public let tarifa: any Tarifa // cualquier tipo que cumpla Tarifa
// Valor por defecto: Parking() usa la normal; se puede pasar otra.
public init(tarifa: any Tarifa = TarifaNormal()) {
self.tarifa = tarifa
}
}
print(TarifaNormal())
print(Parking().tarifa.precio(horas: 2))
print(Parking(tarifa: TarifaNocturna(precioPorHora: 1)).tarifa.precio(horas: 30))Salida:
Tarifa normal (3 €/h)
6
20La última línea es el máximo diario: 30 horas a 1 euro serían 30, pero la extensión no deja pasar de 20.
Reto · Ejercicio: sanciones con valor por defecto
En tu Playground:
-
Escribe un protocolo, la sanción, que pida un nombre de texto y un número entero de partidos de suspensión.
-
En una extensión del protocolo, añade una propiedad calculada, que sea verdadera si son 3 partidos o más (sanción grave), y un método que devuelva el nombre y los partidos con la forma «Roja: 3 partido(s)».
-
Crea dos structs, Amarilla (0 partidos) y Roja (3), que adopten el protocolo.
-
Guárdalas en una sola lista cuyo tipo de elemento sea el protocolo, imprime el resumen de cada una y, al final, cuántas son graves. Lo que debe salir:
Amarilla: 0 partido(s) Roja: 3 partido(s) 1 -
Pregunta extra: ¿qué cambia si declaras el método del resumen también dentro del protocolo, y Roja escribe el suyo?
Ver solución
protocol Sancion {
var nombre: String { get }
var partidosDeSuspension: Int { get }
}
extension Sancion {
var esGrave: Bool { partidosDeSuspension >= 3 }
func resumen() -> String { "\(nombre): \(partidosDeSuspension) partido(s)" }
}
struct Amarilla: Sancion {
let nombre = "Amarilla"
let partidosDeSuspension = 0
}
struct Roja: Sancion {
let nombre = "Roja"
let partidosDeSuspension = 3
}
let sanciones: [any Sancion] = [Amarilla(), Roja()]
for sancion in sanciones { print(sancion.resumen()) }
print(sanciones.filter { $0.esGrave }.count)Salida: Amarilla: 0 partido(s), Roja: 3 partido(s) y 1.
Pregunta extra: con resumen() declarado como requisito, el resumen() que escriba Roja se ejecuta también a través de any Sancion. Sin declararlo, se ejecuta el de la extensión.
Paso · Reglamento como protocolo
Objetivo. Sacar la regla de puntos a Reglamento.swift y hacer que el torneo reciba su reglamento. Al ejecutar torneo-cli verás Reglamento Liga (3/1/0) y la línea de las reglas antiguas.
El título de cada bloque de abajo es la ruta del fichero dentro de ~/Marcador-Swift, la misma que ves en el árbol de arriba.
Piezas del paso. case let (aFavor, enContra) where aFavor > enContra: puntosPorVictoria es el switch de Funciones y switch; aquí cada case es una expresión y el valor del switch es el de la rama elegida. Lo demás (public, la propiedad calculada, CustomStringConvertible y el parámetro con valor por defecto) lo viste arriba, con las tarifas.
Cómo crear el fichero. En el Project navigator (el panel de la izquierda de Xcode; se muestra u oculta con Cmd+1), despliega Sources, clic derecho sobre la carpeta Torneo (la de dentro de Sources) → New File from Template… → Swift File → Next, escribe el nombre (Reglamento) y pulsa Create. Si el menú de tu versión de Xcode se llama distinto, crea el fichero desde la Terminal, en ~/Marcador-Swift/Torneo: touch Sources/Torneo/Reglamento.swift.
Lo que exigen los tests que copiarás en la lección siguiente. Escribe estos nombres y textos tal cual:
Reglamento(protocolo), con estos cuatro requisitos:puntosPorVictoriaypuntosPorEmpate, sin valor por defecto;puntosPorDerrota(0) ypuntosPor, con las etiquetasgolesAFavorygolesEnContra, que traen un valor por defecto en la extensión;resumenno es un requisito: solo vive en la extensión y devuelve «3/1/0» (victoria, empate y derrota, separadas por barras).
ReglamentoLiga(struct sin datos que cambian): 3 y 1, unnombre, «Liga», se crea sin nada desde otro módulo y se imprime como «Reglamento Liga (3/1/0)».ReglamentoPersonalizado, que se crea con las etiquetaspuntosPorVictoriaypuntosPorEmpate.Torneo, que se crea connombre,equiposyreglamento(cualquier tipo que cumplaReglamento; el de liga si no se da).
Intenta escribirlo tú. Crea el protocolo Reglamento y lleva a su extensión el cálculo de los puntos que hacía puntos(para:contra:), ahora con los puntos del reglamento. Escribe ReglamentoLiga y ReglamentoPersonalizado. Haz que las funciones sueltas puntosPor y puntos calculen con el reglamento de liga y que Torneo reciba su reglamento. Y en main.swift: imprime el reglamento de liga; crea la «Copa Antigua» con Rayo y Toros y un reglamento personalizado de 2 puntos por victoria y 1 por empate, e imprime lo que vale en ella una victoria 2-0. Lo que debes ver:
Reglamento Liga (3/1/0)
Con reglas antiguas, una victoria vale 2Qué se sustituye y dónde (todo en el Project navigator, bajo Sources):
Puntos.swift(Sources → Torneo): las dos funcionespuntosPorypuntos(para:contra:)(con sus comentarios) se sustituyen por el bloque de abajo;encabezadoDeJornadano cambia.Torneo.swift(Sources → Torneo): el bloque de abajo sustituye desdepublic let nombrehasta el final delinit;registrarno cambia.main.swift(Sources → torneo-cli): el bloque va justo antes delet torneo = Torneo(…).
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// Un protocolo dice QUÉ se puede hacer, sin decir cómo (la `interface` de
// Kotlin). Quien lo adopte rellena lo que falta. Un protocolo lo pueden adoptar
// structs, enums y clases; una interfaz de Kotlin solo clases y objetos.
public protocol Reglamento {
var puntosPorVictoria: Int { get }
var puntosPorEmpate: Int { get }
// Estos dos también son REQUISITOS del protocolo, pero llevan implementación
// por defecto (en la extensión de abajo): quien adopte el protocolo puede no
// escribirlos y, si los escribe, se usa el suyo.
var puntosPorDerrota: Int { get }
func puntosPor(golesAFavor: Int, golesEnContra: Int) -> Int
}
// Implementación por defecto: una `extension` del protocolo. Es la forma de
// Swift de tener un método con cuerpo en la «interfaz» (Kotlin lo escribe
// directamente dentro de `interface Reglamento`).
extension Reglamento {
public var puntosPorDerrota: Int { 0 }
public func puntosPor(golesAFavor: Int, golesEnContra: Int) -> Int {
switch (golesAFavor, golesEnContra) {
case let (aFavor, enContra) where aFavor > enContra: puntosPorVictoria
case let (aFavor, enContra) where aFavor == enContra: puntosPorEmpate
default: puntosPorDerrota
}
}
// OJO: esto NO es un requisito del protocolo (no está declarado arriba). Si un
// tipo escribe su propio `resumen()` y se llama a través de `any Reglamento`,
// se ejecuta ESTE, no el del tipo. Solo los requisitos se despachan
// dinámicamente. Lo comprueba una prueba (`ReglamentoTests`).
public func resumen() -> String {
"\(puntosPorVictoria)/\(puntosPorEmpate)/\(puntosPorDerrota)"
}
}
// Kotlin usa `object ReglamentoLiga`: una sola instancia. En Swift no existe
// `object`: un `struct` sin datos que cambian hace el mismo papel, y da igual
// cuántas copias haya porque todas son iguales.
public struct ReglamentoLiga: Reglamento, CustomStringConvertible {
public let puntosPorVictoria = 3
public let puntosPorEmpate = 1
public let nombre = "Liga"
public init() {}
public var description: String { "Reglamento \(nombre) (\(resumen()))" }
}
// En Kotlin existía la clase abstracta `ReglamentoPorPuntos`; Swift no tiene
// clases abstractas. Aquí hace su papel un struct normal que adopta el
// protocolo y guarda los puntos que le dan al crearlo.
public struct ReglamentoPersonalizado: Reglamento {
public let puntosPorVictoria: Int
public let puntosPorEmpate: Int
public init(puntosPorVictoria: Int, puntosPorEmpate: Int) {
self.puntosPorVictoria = puntosPorVictoria
self.puntosPorEmpate = puntosPorEmpate
}
}Puntos.swift, las dos funciones que delegan:
public func puntosPor(golesAFavor: Int, golesEnContra: Int) -> Int {
ReglamentoLiga().puntosPor(golesAFavor: golesAFavor, golesEnContra: golesEnContra)
}
// Cada parámetro tiene DOS nombres: el de fuera (la etiqueta, que se escribe al
// llamar) y el de dentro (el que se usa en el cuerpo). `para golesAFavor: Int`
// se llama `puntos(para: 2, contra: 1)` y se lee como una frase.
public func puntos(para golesAFavor: Int, contra golesEnContra: Int) -> Int {
puntosPor(golesAFavor: golesAFavor, golesEnContra: golesEnContra)
}Torneo.swift, propiedades e inicializador:
public let nombre: String
public let equipos: [Equipo]
// `any Reglamento`: «algún tipo que cumple el protocolo». El torneo TIENE un
// reglamento (se lo dan de fuera) en vez de SER un tipo de reglamento:
// composición, como en Kotlin. Cambiar de reglas no obliga a cambiar de clase.
public let reglamento: any Reglamento
// `private(set) var` sobre un array: de fuera se lee, y lo que se lee es una
// COPIA (los arrays son de valor). Kotlin necesitaba una lista privada, un
// getter con `toList()` y explicar el `as MutableList`: aquí el lenguaje ya
// lo evita.
public private(set) var partidos: [Partido] = []
public init(nombre: String, equipos: [Equipo], reglamento: any Reglamento = ReglamentoLiga()) {
self.nombre = nombre
self.equipos = equipos
self.reglamento = reglamento
}main.swift, antes de let torneo = Torneo(…):
// Protocolos: el reglamento se puede cambiar por otro.
print(ReglamentoLiga())
let reglamentoAntiguo = ReglamentoPersonalizado(puntosPorVictoria: 2, puntosPorEmpate: 1)
let torneoAntiguo = Torneo(nombre: "Copa Antigua", equipos: [rayo, toros], reglamento: reglamentoAntiguo)
print("Con reglas antiguas, una victoria vale \(torneoAntiguo.reglamento.puntosPor(golesAFavor: 2, golesEnContra: 0))")Compila y ejecuta. Desde la Terminal, en ~/Marcador-Swift/Torneo:
xcrun swift build
xcrun swift run torneo-cli
xcrun swift testPunto de control
xcrun swift build(desde~/Marcador-Swift/Torneo) compila sin errores.xcrun swift run torneo-cliimprimeReglamento Liga (3/1/0)yCon reglas antiguas, una victoria vale 2.xcrun swift testtermina conTest run with 42 tests in 6 suites passed: los mismos de la lección anterior. Esta mitad no añade tests; los nueve deReglamentollegan en la lección siguiente, cuando ya existan las extensiones de formato que usan.- Provócalo: en
ReglamentoLiga, borra la líneapublic let puntosPorEmpate = 1y ejecutaxcrun swift build; verás algo comotype 'ReglamentoLiga' does not conform to protocol 'Reglamento'. Devuelve la línea. - Sabes decir por qué un método que solo está en la extensión se ejecuta por el tipo con el que miras el valor, y un requisito por el tipo real.
Sigue con Equatable y Hashable: extensiones sobre tipos que ya existen, comparar con == y usar un tipo como clave de un diccionario.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.¿Qué tipos pueden adoptar un protocolo en Swift?
2.¿Dónde se escribe el cuerpo de un método con implementación por defecto?
3.
resumen()está solo en la extensión del protocolo (no declarado en él) yPersonalizadoescribe su propioresumen(). ¿Qué devuelveresumen()sobre un valor de tipoany Reglamentoque contiene unPersonalizado?4.¿Por qué se prefiere composición y protocolos a una jerarquía de clases en Swift?