Por qué Go no tiene herencia
Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 34 de 107.
Los lenguajes orientados a objetos tradicionales como Java y C++ utilizan la herencia para compartir código entre tipos. Una clase hija extiende a una clase padre, heredando todos sus campos y métodos. Go omite deliberadamente esta característica.
La herencia crea un acoplamiento fuerte entre tipos. Cuando una clase padre cambia, todas las clases hijas se ven afectadas.
Las jerarquías de herencia profundas se vuelven difíciles de entender y mantener. El "problema de la clase base frágil" ocurre cuando las modificaciones en una clase base rompen inesperadamente las clases derivadas.
Los diseñadores de Go eligieron un camino diferente: composición sobre herencia. En lugar de decir "un Dog es un Animal", Go te anima a decir "un Dog tiene comportamientos similares a los de un Animal". Este sutil cambio conduce a un código más flexible y fácil de mantener.
Go logra la reutilización de código a través de dos mecanismos que ya has aprendido:
- Interfaces definen contratos de comportamiento sin detalles de implementación
- La incrustación de estructuras (Struct embedding) permite que los tipos incluyan otros tipos y reutilicen sus métodos
Considera esta comparación. En la POO tradicional, podrías escribir class Dog extends Animal. En Go, incrustas un struct Animal dentro de Dog e implementas interfaces compartidas. El resultado es una funcionalidad similar con un acoplamiento más débil entre los tipos.
Este capítulo explora la incrustación de structs en profundidad, mostrando cómo Go logra los beneficios de la herencia sin sus inconvenientes.
Desafío
FácilVamos a construir un sistema de notificación que demuestre el enfoque de composición de Go en lugar de la herencia. Crearás tipos que comparten comportamiento a través de interfaces e incrustación de estructuras (struct embedding) en lugar de jerarquías de clases.
Organizarás tu código en tres archivos:
notifier.go: Define una interfazNotifiercon un único métodoNotify(message string) string. También crea una estructuraBaseNotifiercon un campoNameque será incrustado por otros tipos. Dale aBaseNotifierun método llamadoFormat(message string) stringque devuelva el mensaje prefijado con el nombre del notificador entre corchetes.channels.go: Crea dos tipos de canales de notificación que incrustenBaseNotifiere implementen la interfazNotifier:EmailNotifiercon un campo adicionalAddressSMSNotifiercon un campo adicionalPhone
Notifyde cada tipo debe usar el métodoFormatincrustado e incluir la información específica de su canal en la salida.main.go: Crea una función llamadaSendAlertque acepte cualquierNotifiery un mensaje, y luego devuelva el resultado de llamar aNotify. Lee los detalles de la notificación de la entrada, crea ambos tipos de notificadores y demuestra cómo se pueden usar indistintamente a través de la interfaz.
Se proporcionarán las siguientes entradas:
- Línea 1: Nombre del notificador para el correo electrónico
- Línea 2: Dirección de correo electrónico
- Línea 3: Nombre del notificador para SMS
- Línea 4: Número de teléfono
- Línea 5: Mensaje de alerta
Tu método Format en BaseNotifier debe devolver:
[Name] messageTus métodos Notify deben devolver:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Por ejemplo, dados Alerts, user@mail.com, Urgent, 555-1234, y Server down, tu salida debería ser:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downObserva cómo ambos tipos de notificador reutilizan el método Format de BaseNotifier a través de la incrustación, mientras que cada uno proporciona su propia implementación de Notify. La función SendAlert funciona con cualquier Notifier sin conocer el tipo concreto; esto es la composición sobre la herencia en acción.
Hoja de referencia
Go utiliza la composición sobre la herencia para compartir código entre tipos, evitando el acoplamiento fuerte y los problemas de la clase base frágil de la herencia tradicional orientada a objetos.
En lugar de jerarquías de clases, Go logra la reutilización de código a través de:
- Interfaces: definen contratos de comportamiento sin detalles de implementación.
- Incrustación de estructuras (Struct embedding): permite que los tipos incluyan otros tipos y reutilicen sus métodos.
En lugar de decir "un Perro es un Animal" (herencia), Go fomenta "un Perro tiene comportamientos similares a los de un Animal" (composición).
Ejemplo de incrustación de estructuras
Define una estructura base que será incrustada:
type BaseNotifier struct {
Name string
}
func (b BaseNotifier) Format(message string) string {
return "[" + b.Name + "] " + message
}Incrusta la estructura base en otros tipos para reutilizar sus campos y métodos:
type EmailNotifier struct {
BaseNotifier // embedded struct
Address string
}
func (e EmailNotifier) Notify(message string) string {
// Can call embedded Format method directly
return "Email to " + e.Address + ": " + e.Format(message)
}Define una interfaz que múltiples tipos puedan implementar:
type Notifier interface {
Notify(message string) string
}Usa la interfaz para trabajar con diferentes tipos concretos de forma intercambiable:
func SendAlert(n Notifier, message string) string {
return n.Notify(message)
}Este enfoque proporciona una funcionalidad similar a la herencia pero con un acoplamiento más débil entre los tipos.
Pruébalo tú mismo
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert acepta cualquier Notifier y un mensaje, devuelve el resultado de llamar a Notify
// TODO: Implementar la función SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Leer el nombre del notificador de correo electrónico
scanner.Scan()
emailName := scanner.Text()
// Leer la dirección de correo electrónico
scanner.Scan()
emailAddress := scanner.Text()
// Leer el nombre del notificador de SMS
scanner.Scan()
smsName := scanner.Text()
// Leer el número de teléfono
scanner.Scan()
phoneNumber := scanner.Text()
// Leer el mensaje de alerta
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Crear un EmailNotifier con emailName y emailAddress
// TODO: Crear un SMSNotifier con smsName y phoneNumber
// TODO: Usar SendAlert para enviar el alertMessage a través de ambos notificadores
// y mostrar los resultados
// Suprimir las advertencias de variables no utilizadas (elimina esto cuando uses las variables)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
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