Cheatsheet: Observer em Zig

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 emit itera 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 de Tag -> []Callback para 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 Observador com *anyopaque e 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 o emit atual terminar.
  • Thread safety: std.ArrayList não é thread-safe. Se produtores emitem eventos em uma thread enquanto outra thread registra listeners, adicione um Mutex ao 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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.