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, todas sus dependencias (observadores) reciben una notificación automáticamente. Esto es ideal para sistemas de eventos, notificaciones o cualquier escenario en el que varios componentes deban reaccionar a los cambios.
En Go, implementamos este patrón utilizando una interfaz para los observadores y una estructura subject que administra 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 a user@example.com: Order placed
// Log: Order placedEste patrón desacopla el sujeto de sus observadores, lo que facilita agregar nuevos tipos de observadores sin modificar el código existente. Se usa habitualmente en arquitecturas controladas por eventos, frameworks de GUI y sistemas de mensajería pub/sub.
Desafío
Fácil¡Construyamos un sistema de alertas de precios de acciones usando el patrón Observer! Crearás un sistema en el que varios servicios de alertas reciben automáticamente notificaciones cada vez que cambia el precio de una acción, 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 concretos de observadores.Crea una interfaz
Observercon un métodoUpdate(stockName string, price float64)que reciba la información de la acción cuando cambien los precios.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 debajoPriceLoggercon 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 administra los observadores y les notifica los cambios de precio.Construye una estructura
Stockcon campos paraName(string),Price(float64) y un slice de observadores. Añade estos métodos:Subscribe(o Observer)para añadir un observadorSetPrice(price float64) []string, que actualiza el precio y notifica a todos los observadores, devolviendo un slice con todas sus respuestas
Crea también 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. Después, lee la cantidad de observadores que se crearán. Para cada observador, lee su tipo (
alertologger) y su configuración (el umbral para las alertas, el nombre de registro para los registradores). 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), después el valor de configuración - Línea final: Nuevo precio que se establecerá
Formatea todos los precios con dos decimales.
Por ejemplo, dada la siguiente entrada:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50Tu salida debe ser:
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00Y dada la siguiente entrada:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25Tu salida debe ser:
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)Observa cómo Stock no sabe si está notificando a alertas o a registradores. Simplemente llama a Update en cada observador. ¡Este desacoplamiento facilita añadir nuevos tipos de observadores sin modificar el código de Stock!
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: Recorrer 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 según el tipo
// - Para "alert": analizar configValue como 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: Establece el nuevo precio y obtén las respuestas
// TODO: Imprime 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 PoolPractica por tu cuenta: Compilador de Go online