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 inclusión promueve campos y métodos a la estructura externa. Pero Go también admite la agregación, en la que incluyes otra estructura como un campo con nombre. Comprender cuándo usar cada enfoque es fundamental para un buen diseño.
Con la incrustación, omites el nombre del campo y los miembros del tipo interno se vuelven accesibles directamente:
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
Con la agregación, 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 nombrado
}
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 composición sugiere una relación de «es un», en la que el tipo externo adopta la identidad del tipo interno. La agregación sugiere una relación de «tiene un», en la que el tipo externo simplemente contiene el tipo interno.
Usa la incrustación 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 clara. Usa la agregación cuando el tipo interno sea un detalle de implementación que no debería exponerse, o cuando quieras un control explícito sobre cómo se accede al tipo interno.
Desafío
FácilConstruyamos un sistema de generación de informes que demuestre cuándo usar la incrustación frente a la agregación. Crearás un sistema en el que algunos componentes deberían ser directamente accesibles (incrustación), mientras que otros deberían permanecer como detalles de implementación internal (agregación).
Organizarás tu código en tres archivos:
formatter.go: Crea una estructuraFormattercon un campoStyle(cadena) y un métodoFormat(text string) stringque devuelve el texto envuelto con el estilo, como[Style] text [/Style]. Será un helper internal que no debería exponerse directamente.report.go: Crea dos estructuras que gestionen la generación de informes de forma diferente:- Una estructura
Metadatacon los camposAuthoryDate. Representa information pública que debería ser directamente accesible - Una estructura
Reportcon un campoTitleque embedsMetadata(para el access directo al autor y la fecha), pero utiliza aggregation paraFormatter(como detalle de implementación internal con un campo denominadoformatter)
Generate() stringaReportque utilice el formatter internal para dar formato al título e incluya los metadatos.- Una estructura
main.go: Lee los detalles del informe de la entrada, crea unReportcon Metadata embedded y un formatter agregado. Demuestra la diferencia accediendo al autor directamente en el informe (mediante embedding) y después generando el informe formatted (que utiliza el formatter oculto internamente).
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 Formatter
Tu método Format debería devolver:
[Style] text [/Style]Tu método Generate debería devolver:
[formatted title]
Author: [Author], Date: [Date]En tu archivo principal, imprime tres líneas:
- El autor accessed directamente en
Report(demostrando que embedding promueve el field) - La fecha accessed mediante el nombre del tipo embedded (
report.Metadata.Date) - El resultado de calling
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 access directamente en Report mediante embedding, mientras que el formatter permanece oculto. No puedes llamar directamente a report.Format() porque está agregado con un campo denominado formatter. Esto demuestra en la práctica la relación "is-a" frente a "has-a".
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: Crea un struct Metadata con el author y la date
// TODO: Crea un Formatter con el style dado
// TODO: Crea un Report que embebe Metadata y agrega Formatter
// TODO: Imprime el author accedido directamente en el Report (el embedding promueve el campo)
// TODO: Imprime la date accedida a través del nombre del tipo embebido (report.Metadata.Date)
// TODO: Imprime el resultado de llamar a Generate()
// Suprime las advertencias de variables no usadas (elimina estas cuando uses las 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 PoolPractica por tu cuenta: Compilador de Go online