Observer em Zig
O padrão Observer define uma dependência um-para-muitos entre objetos, de modo que quando um objeto (o sujeito) muda de estado, todos os seus dependentes (observadores) são notificados automaticamente. Em Zig, esse padrão é implementado de forma leve usando slices de ponteiros de função ou interfaces via comptime.
Quando Usar
- Sistemas de eventos e callbacks
- Notificação de mudanças de estado (UI, dados, config)
- Logging e monitoramento desacoplados
- Comunicação entre módulos sem acoplamento direto
Implementação com Ponteiros de Função
const std = @import("std");
fn EventEmitter(comptime EventType: type) type {
return struct {
const Self = @This();
const Callback = *const fn (EventType) void;
listeners: std.ArrayList(Callback),
pub fn init(allocator: std.mem.Allocator) Self {
return .{
.listeners = std.ArrayList(Callback).init(allocator),
};
}
pub fn deinit(self: *Self) void {
self.listeners.deinit();
}
pub fn on(self: *Self, callback: Callback) !void {
try self.listeners.append(callback);
}
pub fn off(self: *Self, callback: Callback) void {
for (self.listeners.items, 0..) |cb, i| {
if (cb == callback) {
_ = self.listeners.orderedRemove(i);
return;
}
}
}
pub fn emit(self: *const Self, evento: EventType) void {
for (self.listeners.items) |callback| {
callback(evento);
}
}
};
}
// Eventos do sistema
const Evento = union(enum) {
usuario_logou: []const u8,
usuario_saiu: []const u8,
erro: []const u8,
};
fn logHandler(evento: Evento) void {
switch (evento) {
.usuario_logou => |nome| std.debug.print("[LOG] {s} entrou\n", .{nome}),
.usuario_saiu => |nome| std.debug.print("[LOG] {s} saiu\n", .{nome}),
.erro => |msg| std.debug.print("[ERRO] {s}\n", .{msg}),
}
}
fn metricasHandler(evento: Evento) void {
switch (evento) {
.usuario_logou => std.debug.print("[METRICS] +1 usuário online\n", .{}),
.usuario_saiu => std.debug.print("[METRICS] -1 usuário online\n", .{}),
.erro => std.debug.print("[METRICS] +1 erro registrado\n", .{}),
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var emitter = EventEmitter(Evento).init(gpa.allocator());
defer emitter.deinit();
try emitter.on(logHandler);
try emitter.on(metricasHandler);
emitter.emit(.{ .usuario_logou = "Ana" });
emitter.emit(.{ .usuario_saiu = "Carlos" });
emitter.emit(.{ .erro = "Conexão perdida" });
}
Observer com Contexto
const std = @import("std");
const Observador = struct {
ptr: *anyopaque,
notificarFn: *const fn (*anyopaque, []const u8) void,
pub fn notificar(self: Observador, mensagem: []const u8) void {
self.notificarFn(self.ptr, mensagem);
}
};
const Sujeito = struct {
observadores: std.ArrayList(Observador),
estado: []const u8,
pub fn init(allocator: std.mem.Allocator) Sujeito {
return .{
.observadores = std.ArrayList(Observador).init(allocator),
.estado = "",
};
}
pub fn deinit(self: *Sujeito) void {
self.observadores.deinit();
}
pub fn registrar(self: *Sujeito, obs: Observador) !void {
try self.observadores.append(obs);
}
pub fn setEstado(self: *Sujeito, novo_estado: []const u8) void {
self.estado = novo_estado;
self.notificarTodos();
}
fn notificarTodos(self: *const Sujeito) void {
for (self.observadores.items) |obs| {
obs.notificar(self.estado);
}
}
};
Observer com Filtro por Tipo de Evento
Em sistemas maiores, os listeners frequentemente se interessam apenas por certos tipos de evento. Evite que cada listener filtre manualmente:
const std = @import("std");
fn EventBus(comptime E: type) type {
return struct {
const Self = @This();
const Tag = std.meta.Tag(E);
const Callback = struct {
tag: Tag,
funcao: *const fn (E) void,
};
callbacks: std.ArrayList(Callback),
pub fn init(allocator: std.mem.Allocator) Self {
return .{ .callbacks = std.ArrayList(Callback).init(allocator) };
}
pub fn deinit(self: *Self) void {
self.callbacks.deinit();
}
pub fn on(self: *Self, tag: Tag, callback: *const fn (E) void) !void {
try self.callbacks.append(.{ .tag = tag, .funcao = callback });
}
pub fn emit(self: *const Self, evento: E) void {
const tag_atual = std.meta.activeTag(evento);
for (self.callbacks.items) |cb| {
if (cb.tag == tag_atual) cb.funcao(evento);
}
}
};
}
Agora cada listener registra interesse em um tipo específico de evento, e o bus despacha apenas para os listeners relevantes.
Considerações de Performance
- Lista de listeners com busca linear: o
emititera sobre todos os listeners para cada evento. Para poucos listeners (< 20), o custo é desprezível. Para sistemas de alta frequência com muitos listeners, considere um HashMap deTag -> []Callbackpara O(1) de lookup. - Ponteiros de função vs closures: Zig não tem closures nativas. Se o listener precisa de contexto (estado), use o padrão
Observadorcom*anyopaquee ponteiro de função — isso permite que o listener acesse seu próprio estado. - Evite emitir eventos dentro de handlers: se um handler emite outro evento durante o
emit, e este novo evento dispara mais handlers, você pode acumular uma stack profunda ou entrar em loop infinito. Enfileire eventos secundários para emissão depois que oemitatual terminar. - Thread safety:
std.ArrayListnão é thread-safe. Se produtores emitem eventos em uma thread enquanto outra thread registra listeners, adicione umMutexao redor das operações no ArrayList.
Erros Comuns
Não remover listeners após o objeto ser destruído: se um objeto se registra como observer e é destruído sem chamar off, o emitter vai chamar um ponteiro de função inválido. Sempre chame emitter.off(callback) no deinit do observer.
Modificar a lista de listeners durante o emit: se um callback chama emitter.on ou emitter.off durante o emit, o ArrayList pode ser realocado ou modificado enquanto está sendo iterado. Mantenha uma cópia do slice de listeners antes de iterar, ou use uma flag emitindo para adiar modificações.
Eventos com payloads grandes por valor: se o tipo de evento contém strings longas ou arrays grandes, cada emit copia o evento para cada callback. Use ponteiros (*const Evento) para eventos grandes ou prefira payloads pequenos e específicos.
Perguntas Frequentes
Qual é a diferença entre Observer e Pub/Sub?
Observer geralmente implica que o sujeito conhece seus observers diretamente (lista de callbacks). Pub/Sub introduz um broker intermediário que desacopla completamente publicador e assinante — o publicador não sabe quem assina. O EventBus acima é um Pub/Sub simples.
Como garantir ordem de notificação determinística?
Os observers são notificados na ordem de registro. Se a ordem importa, documente isso e respeite a ordem de chamadas a on. Alternativamente, adicione uma prioridade numérica ao callback e ordene a lista antes de emitir.
Como testar código que emite eventos?
Registre um callback de teste que armazena os eventos recebidos em um ArrayList, verifique a lista após o emit. Use std.testing.allocator para o ArrayList de listeners do emitter e o ArrayList de captura do teste — ambos vão detectar leaks automaticamente.
Quando Evitar
- Quando há apenas um listener (callback direto é mais simples)
- Em código que precisa de performance extrema no hot path
- Quando a ordem de notificação importa e é difícil de controlar
- Riscos de referência circular entre observer e subject
Veja Também
- Strategy — Trocar comportamento em runtime
- Command — Encapsular ações como objetos
- Producer-Consumer — Comunicação assíncrona
- Concorrência — Observer thread-safe
- Receitas — Exemplos práticos de eventos