POO aplicada a Swift: abstracción, encapsulación, herencia y polimorfismo
Los pilares de la POO con ejemplos Swift y una explicación práctica de composición frente a herencia.
Cuatro ideas, cuatro preguntas
Abstracción: qué capacidades necesita el cliente. Encapsulación: qué estado interno e invariantes protegemos. Herencia: cuándo una clase especializa a otra. Polimorfismo: distintas implementaciones que cumplen el mismo contrato. En Swift los protocolos también permiten polimorfismo sin herencia.
Abstracción
Un protocolo expresa una capacidad sin obligar a conocer cómo se implementa. Checkout depende de PaymentProcessing, no de un proveedor de pagos concreto.
import Foundation
protocol PaymentProcessing {
func pay(amount: Decimal) async throws
}
struct Checkout {
let payment: any PaymentProcessing
func confirm(total: Decimal) async throws {
try await payment.pay(amount: total)
}
}Encapsulación
La finalidad es proteger invariantes, no solo ocultar campos. private(set) permite leer desde fuera y restringe cambios a los métodos controlados.
struct Cart {
private(set) var items: [String] = []
var itemCount: Int { items.count }
mutating func add(_ item: String) {
guard !item.isEmpty else { return }
items.append(item)
}
}Herencia
En Swift solamente heredan las clases. Tiene sentido con una relación real «es un», como HomeScreen es una Screen. Las jerarquías profundas aumentan el acoplamiento y dificultan seguir el comportamiento.
class Screen {
func title() -> String { "Screen" }
}
final class HomeScreen: Screen {
override func title() -> String { "Home" }
}Polimorfismo
Permite que CardPayment y BankTransfer respondan al contrato PaymentProcessing sin que Checkout sepa cómo funcionan. No requiere clases ni herencia.
import Foundation
struct CardPayment: PaymentProcessing {
func pay(amount: Decimal) async throws { /* Card gateway */ }
}
struct BankTransfer: PaymentProcessing {
func pay(amount: Decimal) async throws { /* Transfer */ }
}
func complete(_ payment: any PaymentProcessing) async throws {
try await payment.pay(amount: 10)
}Composición frente a herencia: la idea clave
Con herencia, HomeScreen ES UNA Screen: depende de una clase base y hereda sus decisiones. Con composición, AccountsViewModel TIENE UN AccountsRepository: delega el acceso a datos en un colaborador. Puedes sustituir el repositorio real por uno de pruebas sin crear una nueva subclase de ViewModel. Por eso la composición suele facilitar testing, cambios y separación de responsabilidades. La herencia sigue siendo razonable si existe una especialización estable y real; no hay que prohibirla por principio.
struct Account { let name: String }
protocol AccountsRepository {
func load() async throws -> [Account]
}
struct AccountsViewModel<Repository: AccountsRepository> {
let repository: Repository
func names() async throws -> [String] {
try await repository.load().map(\.name)
}
}
struct PreviewRepository: AccountsRepository {
func load() async throws -> [Account] {
[Account(name: "Checking")]
}
}| Diseño | Cuándo encaja | Coste |
|---|---|---|
| Protocolo + composición | Servicios y colaboradores sustituibles | Más tipos y ensamblaje |
| Herencia de clase | Relación «es un» estable | Dependencia de la clase base |
| Genéricos | Colaborador fijo por tipo | Parámetros de tipo extra |
| any Protocol | Implementación elegida en ejecución | Indirección del existencial |
Trampas típicas
Abstraer no significa crear protocolos para todo. Encapsular es proteger invariantes. Composición no significa ausencia de dependencias: significa hacerlas explícitas. Adoptar un protocolo no equivale a heredar de una clase.
Notas personales de estudio. Verifica los matices según la versión de Swift/iOS y la documentación oficial.