← Biblioteca técnica
Architecture · APUNTE DE ESTUDIO

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.

Protocolo como abstracción
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.

Proteger invariantes
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.

Herencia entre clases
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.

Polimorfismo con protocolos
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.

Composición con repositorio intercambiable
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")]
    }
}
Comparación rápida
DiseñoCuándo encajaCoste
Protocolo + composiciónServicios y colaboradores sustituiblesMás tipos y ensamblaje
Herencia de claseRelación «es un» estableDependencia de la clase base
GenéricosColaborador fijo por tipoParámetros de tipo extra
any ProtocolImplementación elegida en ejecuciónIndirecció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.

FUENTES Y DOCUMENTACIÓN

Notas personales de estudio. Verifica los matices según la versión de Swift/iOS y la documentación oficial.