Polimorfismo mediante interfaces
Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 41 de 107.
El polimorfismo permite que diferentes tipos sean tratados de manera uniforme a través de una interfaz compartida. En Go, esto se logra completamente a través de interfaces, sin herencia ni jerarquías de clases.
Cuando una función acepta un tipo de interfaz como parámetro, se puede pasar cualquier tipo concreto que implemente esa interfaz. La función no necesita conocer el tipo específico; solo le importa el comportamiento definido por la interfaz:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
Ahora MakeSound funciona con cualquier tipo que tenga un método Speak():
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // ¡Guau!
MakeSound(cat) // ¡Miau!
}
La misma llamada a la función produce un comportamiento diferente dependiendo del tipo real pasado. Esto es polimorfismo en acción. La función MakeSound se escribe una vez pero funciona con un número ilimitado de tipos, siempre que cumplan con la interfaz Speaker.
Este enfoque mantiene tu código flexible y extensible. Agregar un nuevo tipo que hable no requiere cambios en las funciones existentes; simplemente implementa la interfaz y funcionará automáticamente.
Desafío
FácilVamos a construir un sistema de descripción de vehículos que demuestre el polimorfismo en acción. Crearás diferentes tipos de vehículos que comparten un comportamiento común a través de una interfaz, y luego escribirás una única función que funcione con cualquier vehículo.
Organizarás tu código en dos archivos:
vehicles.go: Define una interfazDescriberque requiera un métodoDescribe() string. Luego crea tres tipos de vehículos que implementen cada uno esta interfaz a su manera:Carcon los camposBrandyModel; su métodoDescribe()devuelveCar: [Brand] [Model]Motorcyclecon los camposBrandyEngineCC(int); su métodoDescribe()devuelveMotorcycle: [Brand] [EngineCC]ccBicyclecon el campoType(como "Mountain" o "Road"); su métodoDescribe()devuelveBicycle: [Type]
main.go: Crea una función llamadaPrintDescriptionque acepte cualquierDescribere imprima el resultado de llamar aDescribe(). Lee los detalles del vehículo desde la entrada, crea uno de cada tipo de vehículo y pasa cada uno aPrintDescriptionpara demostrar que la misma función funciona con los tres tipos diferentes.
Se proporcionarán las siguientes entradas:
- Línea 1: Marca del coche (Car brand)
- Línea 2: Modelo del coche (Car model)
- Línea 3: Marca de la motocicleta (Motorcycle brand)
- Línea 4: CC del motor de la motocicleta (Motorcycle engine CC - entero)
- Línea 5: Tipo de bicicleta (Bicycle type)
Por ejemplo, dados Toyota, Camry, Honda, 600, y Mountain, tu salida debería ser:
Car: Toyota Camry
Motorcycle: Honda 600cc
Bicycle: MountainObserva cómo PrintDescription no necesita saber si está recibiendo un Car, Motorcycle o Bicycle; simplemente llama a Describe() y cada tipo responde con su propia salida única. Esto es el polimorfismo: una función, múltiples comportamientos.
Hoja de referencia
El polimorfismo permite que diferentes tipos sean tratados de manera uniforme a través de una interfaz compartida. En Go, esto se logra mediante interfaces sin herencia ni jerarquías de clases.
Cuando una función acepta un tipo de interfaz como parámetro, se puede pasar cualquier tipo concreto que implemente esa interfaz:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{ Name string }
func (c Cat) Speak() string { return "Meow!" }
func MakeSound(s Speaker) {
fmt.Println(s.Speak())
}
La misma función funciona con diferentes tipos:
func main() {
dog := Dog{Name: "Rex"}
cat := Cat{Name: "Whiskers"}
MakeSound(dog) // Woof!
MakeSound(cat) // Meow!
}
La función se escribe una vez pero funciona con tipos ilimitados, siempre que cumplan con la interfaz. Agregar nuevos tipos no requiere cambios en las funciones existentes; simplemente implemente la interfaz.
Pruébalo tú mismo
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
// TODO: Crea una función llamada PrintDescription que acepte cualquier Describer
// y que imprima el resultado de llamar a Describe()
func main() {
reader := bufio.NewReader(os.Stdin)
// Leer los detalles del coche
carBrand, _ := reader.ReadString('\n')
carBrand = strings.TrimSpace(carBrand)
carModel, _ := reader.ReadString('\n')
carModel = strings.TrimSpace(carModel)
// Leer los detalles de la motocicleta
motoBrand, _ := reader.ReadString('\n')
motoBrand = strings.TrimSpace(motoBrand)
motoEngineStr, _ := reader.ReadString('\n')
motoEngineStr = strings.TrimSpace(motoEngineStr)
motoEngine, _ := strconv.Atoi(motoEngineStr)
// Leer los detalles de la bicicleta
bicycleType, _ := reader.ReadString('\n')
bicycleType = strings.TrimSpace(bicycleType)
// TODO: Crea un Car, Motorcycle y Bicycle utilizando los valores de entrada
// TODO: Llama a PrintDescription para cada vehículo para demostrar el polimorfismo
fmt.Println("TODO: Print vehicle descriptions")
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de POO en Go
Archivos externosEspacio de trabajo y módulos de GoPaquetes e importacionesNombres exportados vs. no exportadosIntroducción a la POO en GoStructs como clasesDefinición de métodos en structsReceptores de puntero vs. de valorInicialización de structsFunciones constructorasResumen - Calculadora simple4Interfaces
Introducción a las interfacesImplementación implícitaLa interfaz como contratoInterfaz vacía (any)Type AssertionType SwitchComposición de interfacesInterfaces Stringer y ErrorResumen: Calculadora de figuras7Encapsulamiento
Campos exportados vs. no exportadosEncapsulamiento a nivel de paqueteMétodos Getter y SetterOcultamiento de información en GoResumen - Registros de estudiantes10Genéricos (Go 1.18+)
Introducción a los GenéricosParámetros de tipoRestricciones de tipoStructs genéricosSolución alternativa para métodos genéricosResumen - Colección genérica13Patrones de diseño - Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón Abstract FactoryPatrón ObserverPatrón Strategy2Inmersión profunda en Tipos y Structs
Tipos básicos y compuestosDefiniciones de tipos personalizadosEtiquetas de StructsStructs anónimosStructs anidadosValores cero y por defectoRepaso - Libreta de contactos5Composición sobre herencia
Por qué Go no tiene herenciaConceptos básicos de incrustación de structsPromoción de métodosIncrustación de múltiples structsIncrustación vs AgregaciónShadowing de métodos incrustadosResumen - Jerarquía de empleados8Manejo de Errores y OOP
La interfaz errorTipos de errores personalizadosEnvoltura de errores (fmt.Errorf)Errores centinelaerrors.Is() y errors.As()Panic, Defer y RecoverResumen - Analizador de archivos3Punteros y Memoria
Fundamentos de punteros en GoPunteros a StructsPaso por valor vs referenciaLa función new()Garbage Collection en GoResumen - Constructor de Linked List6Polimorfismo en Go
Polimorfismo mediante interfacesDuck Typing en GoReglas de satisfacción de interfacesColecciones polimórficasInyección de dependenciasResumen - Procesador de pagos9Concurrencia y POO
Fundamentos de GoroutinesChannels y comunicaciónChannels con buffer vs. sin bufferSentencia Selectsync.Mutex y sync.RWMutexsync.WaitGroupDiseño de estructuras Thread-SafeResumen - Worker Pool