Incrustación vs Agregación
Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 38 de 107.
Has aprendido que la incrustación (embedding) promueve campos y métodos al struct externo. Pero Go también admite la agregación, donde incluyes otro struct como un campo con nombre. Comprender cuándo usar cada enfoque es clave para un buen diseño.
Con la incrustación (embedding), omites el nombre del campo y los miembros del tipo interno se vuelven directamente accesibles:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // incrustación - sin nombre de campo
}
d := Document{}
d.Write() // acceso directo
Con la agregación, le das al campo un nombre explícito, lo que requiere ese nombre para acceder al tipo interno:
type Document struct {
writer Writer // agregación - campo con nombre
}
d := Document{writer: Writer{}}
d.writer.Write() // debe usar el nombre del campo
La diferencia clave es la relación que estás expresando. La incrustación (embedding) sugiere una relación "es-un" (is-a) donde el tipo externo asume la identidad del tipo interno. La agregación sugiere una relación "tiene-un" (has-a) donde el tipo externo simplemente contiene al tipo interno.
Usa el embedding cuando quieras que el tipo externo satisfaga las interfaces implementadas por el tipo interno, o cuando el acceso directo a los métodos haga que la API sea más limpia. Usa la agregación cuando el tipo interno sea un detalle de implementación que no deba ser expuesto, o cuando quieras un control explícito sobre cómo se accede al tipo interno.
Desafío
FácilVamos a construir un sistema de generación de informes que demuestre cuándo usar la incrustación (embedding) frente a la agregación (aggregation). Crearás un sistema donde algunos componentes deben ser directamente accesibles (incrustación) mientras que otros deben permanecer como detalles de implementación interna (agregación).
Organizarás tu código en tres archivos:
formatter.go: Crea una estructuraFormattercon un campoStyle(string) y un métodoFormat(text string) stringque devuelva el texto envuelto con el estilo, como[Style] text [/Style]. Este será un ayudante interno que no debería estar expuesto directamente.report.go: Crea dos estructuras que manejen la generación de informes de manera diferente:- Una estructura
Metadatacon los camposAuthoryDate; esto representa información pública que debería ser directamente accesible. - Una estructura
Reportcon un campoTitleque incrusta (embeds)Metadata(para el acceso directo al autor y la fecha) pero utiliza agregación paraFormatter(como un detalle de implementación interna con un campo con nombreformatter).
Generate() stringaReportque utilice el formateador interno para dar formato al título e incluya los metadatos.- Una estructura
main.go: Lee los detalles del informe desde la entrada, crea unReportcon metadatos incrustados y un formateador agregado. Demuestra la diferencia accediendo al autor directamente en el informe (a través de la incrustación) y luego generando el informe formateado (que utiliza internamente el formateador oculto).
Se proporcionarán las siguientes entradas:
- Línea 1: Título del informe
- Línea 2: Nombre del autor
- Línea 3: Fecha
- Línea 4: Estilo del formateador
Tu método Format debe devolver:
[Style] text [/Style]Tu método Generate debe devolver:
[formatted title]
Author: [Author], Date: [Date]En tu archivo principal, imprime tres líneas:
- El autor accedido directamente en el
Report(demostrando que la incrustación promueve el campo). - La fecha accedida a través del nombre del tipo incrustado (
report.Metadata.Date). - El resultado de llamar a
Generate().
Por ejemplo, dados Quarterly Results, Jane Doe, 2024-01-15 y BOLD, tu salida debería ser:
Jane Doe
2024-01-15
[BOLD] Quarterly Results [/BOLD]
Author: Jane Doe, Date: 2024-01-15Observa cómo Author y Date son accesibles directamente en el Report a través de la incrustación, mientras que el formateador permanece oculto; no puedes llamar a report.Format() directamente porque está agregado con un campo con nombre. Esto demuestra la relación "es-un" (is-a) frente a "tiene-un" (has-a) en la práctica.
Hoja de referencia
Go admite dos formas de incluir un struct dentro de otro: embedding (incrustación) y aggregation (agregación).
El Embedding omite el nombre del campo, promocionando los campos y métodos del tipo interno al struct externo:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // embedding - sin nombre de campo
}
d := Document{}
d.Write() // acceso directo al método incrustado
La Aggregation utiliza un nombre de campo explícito, lo que requiere ese nombre para acceder al tipo interno:
type Document struct {
writer Writer // aggregation - campo con nombre
}
d := Document{writer: Writer{}}
d.writer.Write() // debe usar el nombre del campo
Cuándo usar cada uno:
- Embedding: Expresa una relación "es-un" (is-a). Úselo cuando el tipo externo deba satisfacer las interfaces del tipo interno, o cuando el acceso directo a los métodos mejore la API.
- Aggregation: Expresa una relación "tiene-un" (has-a). Úselo cuando el tipo interno sea un detalle de implementación que no deba exponerse, o cuando desee un control explícito sobre el acceso.
Con el embedding, aún puede acceder a los campos a través del nombre del tipo incrustado si es necesario:
type Metadata struct {
Author string
}
type Report struct {
Metadata // embedded (incrustado)
}
r := Report{Metadata: Metadata{Author: "Jane"}}
r.Author // acceso directo mediante promoción
r.Metadata.Author // acceso explícito a través del nombre del tipo
Pruébalo tú mismo
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
title := scanner.Text()
scanner.Scan()
author := scanner.Text()
scanner.Scan()
date := scanner.Text()
scanner.Scan()
style := scanner.Text()
// TODO: Create a Metadata struct with the author and date
// TODO: Create a Formatter with the given style
// TODO: Create a Report that embeds Metadata and aggregates Formatter
// TODO: Print the author accessed directly on the Report (embedding promotes the field)
// TODO: Print the date accessed through the embedded type name (report.Metadata.Date)
// TODO: Print the result of calling Generate()
// Suppress unused variable warnings (remove these when you use the variables)
_ = title
_ = author
_ = date
_ = style
}
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