Patrón Observer
Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 89 de 107.
El patrón Observer define una relación de uno a muchos entre objetos. Cuando un objeto (el sujeto) cambia de estado, todos sus dependientes (observadores) son notificados automáticamente. Esto es ideal para sistemas de eventos, notificaciones o cualquier escenario donde múltiples componentes necesiten reaccionar a los cambios.
En Go, implementamos este patrón utilizando una interfaz para los observadores y una estructura de sujeto (subject) que gestiona las suscripciones:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Cualquier tipo que implemente la interfaz Observer puede suscribirse para recibir notificaciones:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}El sujeto notifica a todos los observadores sin conocer sus tipos concretos:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Correo electrónico a user@example.com: Order placed
// Registro: Order placedEste patrón desacopla el sujeto de sus observadores, lo que facilita la adición de nuevos tipos de observadores sin modificar el código existente. Se utiliza comúnmente en arquitecturas orientadas a eventos, frameworks de GUI y sistemas de mensajería pub/sub.
Desafío
Fácil¡Vamos a construir un sistema de alerta de precios de acciones utilizando el patrón Observer! Crearás un sistema donde múltiples servicios de alerta reciben notificaciones automáticamente cada vez que cambia el precio de una acción, lo cual es perfecto para demostrar cómo los observadores pueden reaccionar a eventos sin que el sujeto conozca sus tipos concretos.
Organizarás tu código en tres archivos:
observer.go: Define tu interfaz de observador y los tipos de observadores concretos.Crea una interfaz
Observercon un métodoUpdate(stockName string, price float64)que reciba información de la acción cuando los precios cambien.Implementa dos tipos de observadores:
PriceAlertcon un campoThreshold(float64): cuando se actualiza, devuelveALERT: [stockName] at $[price] crossed threshold $[threshold]si el precio está por encima del umbral, oWATCHING: [stockName] at $[price] (threshold: $[threshold])si está por debajo.PriceLoggercon un campoLogName(string): cuando se actualiza, devuelve[logName] recorded [stockName]: $[price].
Ambos observadores deben tener un método que devuelva su cadena de respuesta formateada.
stock.go: Crea tu sujeto que gestiona a los observadores y les notifica los cambios de precio.Construye un struct
Stockcon campos paraName(string),Price(float64) y un slice de observadores. Añade estos métodos:Subscribe(o Observer)para añadir un observador.SetPrice(price float64) []stringque actualiza el precio y notifica a todos los observadores, devolviendo un slice con todas sus respuestas.
También crea un constructor
NewStock(name string, initialPrice float64) *Stock.main.go: Configura tu acción con observadores y simula cambios de precio.Lee el nombre de la acción y el precio inicial. Luego lee un recuento de observadores a crear. Para cada observador, lee su tipo (
alertologger) y su configuración (umbral para alertas, nombre de registro para loggers). Suscribe cada observador a la acción.Finalmente, lee un nuevo precio, actualiza la acción e imprime la respuesta de cada observador en su propia línea.
Se proporcionarán las siguientes entradas:
- Línea 1: Nombre de la acción
- Línea 2: Precio inicial
- Línea 3: Número de observadores
- Para cada observador: tipo (
alertologger), luego el valor de configuración - Línea final: Nuevo precio a establecer
Formatea todos los precios con dos decimales.
Por ejemplo, dado:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50Tu salida debería ser:
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00Y dado:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25Tu salida debería ser:
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)Observa cómo el Stock no sabe si está notificando a alertas o a loggers; simplemente llama a Update en cada observador. ¡Este desacoplamiento facilita la adición de nuevos tipos de observadores sin modificar el código de Stock!
Hoja de referencia
El patrón Observer define una relación de uno a muchos donde un sujeto notifica a múltiples observadores automáticamente cuando su estado cambia. Esto desacopla al sujeto de sus observadores, lo que lo hace ideal para sistemas de eventos y notificaciones.
Estructura básica
Define una interfaz de observador y un sujeto que gestione las suscripciones:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Implementación de observadores
Cualquier tipo que implemente la interfaz Observer puede suscribirse:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}Uso
Suscribe observadores y notifícales los cambios:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placedEl sujeto notifica a todos los observadores sin conocer sus tipos concretos, lo que facilita la adición de nuevos tipos de observadores sin modificar el código existente.
Pruébalo tú mismo
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Leer el nombre de la acción
stockName, _ := reader.ReadString('\n')
stockName = strings.TrimSpace(stockName)
// Leer el precio inicial
initialPriceStr, _ := reader.ReadString('\n')
initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)
// Leer el número de observadores
numObserversStr, _ := reader.ReadString('\n')
numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))
// TODO: Crear un nuevo Stock usando el constructor NewStock
// TODO: Iterar a través de cada observador
for i := 0; i < numObservers; i++ {
// Leer el tipo de observador ("alert" o "logger")
observerType, _ := reader.ReadString('\n')
observerType = strings.TrimSpace(observerType)
// Leer el valor de configuración
configValue, _ := reader.ReadString('\n')
configValue = strings.TrimSpace(configValue)
// TODO: Crear el observador apropiado basado en el tipo
// - Para "alert": convertir configValue a un umbral float64, crear PriceAlert
// - Para "logger": usar configValue como LogName, crear PriceLogger
// TODO: Suscribir el observador al stock
}
// Leer el nuevo precio
newPriceStr, _ := reader.ReadString('\n')
newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)
// TODO: Establecer el nuevo precio y obtener las respuestas
// TODO: Imprimir cada respuesta en su propia línea
}
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