Command em Zig
O padrão Command encapsula uma requisição como um objeto, permitindo parametrizar, enfileirar, registrar e desfazer operações. Em Zig, isso é implementado com tagged unions (para conjunto fixo de comandos) ou interfaces via ponteiros de função (para extensibilidade).
Quando Usar
- Sistemas de undo/redo (editores, jogos)
- Filas de tarefas e job queues
- Logging de operações para replay
- Macros e gravação de ações do usuário
- Transações que precisam ser revertíveis
Implementação com Tagged Union
const std = @import("std");
const Comando = union(enum) {
inserir: struct { posicao: usize, texto: []const u8 },
deletar: struct { posicao: usize, tamanho: usize },
substituir: struct { posicao: usize, antigo: []const u8, novo: []const u8 },
pub fn executar(self: Comando, buffer: *std.ArrayList(u8)) !void {
switch (self) {
.inserir => |cmd| {
try buffer.insertSlice(cmd.posicao, cmd.texto);
},
.deletar => |cmd| {
buffer.replaceRange(cmd.posicao, cmd.tamanho, &.{});
},
.substituir => |cmd| {
buffer.replaceRange(cmd.posicao, cmd.antigo.len, cmd.novo);
},
}
}
pub fn desfazer(self: Comando, buffer: *std.ArrayList(u8)) !void {
switch (self) {
.inserir => |cmd| {
buffer.replaceRange(cmd.posicao, cmd.texto.len, &.{});
},
.deletar => |cmd| {
_ = cmd;
// Precisaria do texto deletado salvo para desfazer
},
.substituir => |cmd| {
buffer.replaceRange(cmd.posicao, cmd.novo.len, cmd.antigo);
},
}
}
};
const EditorHistorico = struct {
historico: std.ArrayList(Comando),
posicao: usize = 0,
pub fn init(allocator: std.mem.Allocator) EditorHistorico {
return .{
.historico = std.ArrayList(Comando).init(allocator),
};
}
pub fn deinit(self: *EditorHistorico) void {
self.historico.deinit();
}
pub fn executar(self: *EditorHistorico, cmd: Comando, buffer: *std.ArrayList(u8)) !void {
try cmd.executar(buffer);
// Descartar comandos após posição atual (novo branch)
self.historico.shrinkRetainingCapacity(self.posicao);
try self.historico.append(cmd);
self.posicao += 1;
}
pub fn desfazer(self: *EditorHistorico, buffer: *std.ArrayList(u8)) !void {
if (self.posicao == 0) return;
self.posicao -= 1;
try self.historico.items[self.posicao].desfazer(buffer);
}
pub fn refazer(self: *EditorHistorico, buffer: *std.ArrayList(u8)) !void {
if (self.posicao >= self.historico.items.len) return;
try self.historico.items[self.posicao].executar(buffer);
self.posicao += 1;
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var buffer = std.ArrayList(u8).init(alloc);
defer buffer.deinit();
var editor = EditorHistorico.init(alloc);
defer editor.deinit();
try editor.executar(.{ .inserir = .{ .posicao = 0, .texto = "Olá Mundo" } }, &buffer);
std.debug.print("Após inserir: '{s}'\n", .{buffer.items});
try editor.desfazer(&buffer);
std.debug.print("Após desfazer: '{s}'\n", .{buffer.items});
try editor.refazer(&buffer);
std.debug.print("Após refazer: '{s}'\n", .{buffer.items});
}
Fila de Comandos
const std = @import("std");
const Tarefa = union(enum) {
enviar_email: struct { para: []const u8, assunto: []const u8 },
processar_imagem: struct { caminho: []const u8, largura: u32 },
gerar_relatorio: struct { periodo: []const u8 },
pub fn executar(self: Tarefa) void {
switch (self) {
.enviar_email => |t| std.debug.print("Enviando email para {s}: {s}\n", .{ t.para, t.assunto }),
.processar_imagem => |t| std.debug.print("Processando {s} ({d}px)\n", .{ t.caminho, t.largura }),
.gerar_relatorio => |t| std.debug.print("Gerando relatório: {s}\n", .{t.periodo}),
}
}
};
const FilaTarefas = struct {
fila: std.ArrayList(Tarefa),
pub fn init(allocator: std.mem.Allocator) FilaTarefas {
return .{ .fila = std.ArrayList(Tarefa).init(allocator) };
}
pub fn deinit(self: *FilaTarefas) void {
self.fila.deinit();
}
pub fn enfileirar(self: *FilaTarefas, tarefa: Tarefa) !void {
try self.fila.append(tarefa);
}
pub fn processarTodas(self: *FilaTarefas) void {
for (self.fila.items) |tarefa| {
tarefa.executar();
}
self.fila.clearRetainingCapacity();
}
};
Considerações de Performance
- Tagged unions vs ponteiros de função: a abordagem com
union(enum)é preferível quando o conjunto de comandos é fixo e conhecido em compile time. O compilador gera umswitcheficiente sem indireção de ponteiro. Ponteiros de função são mais flexíveis para conjuntos abertos de comandos, mas introduzem indireção por chamada. - Histórico de undo com allocator: cada
Comandono histórico pode carregar dados de tamanho variável (texto a ser desfeito, estado anterior). Use umArenaAllocatorpara o histórico — ao fazer “clear” do histórico, basta liberar a arena inteira em vez de desalocar comando por comando. - Fila de tarefas: ao usar o padrão como job queue, dimensione o
ArrayListcomensureTotalCapacityna inicialização para evitar realocações durante a execução. Em sistemas de alta frequência, considere uma fila circular estática (comptimetamanho).
Erros Comuns
Não salvar o estado anterior para desfazer: a operação desfazer de um comando deletar precisa ter os dados que foram deletados. Se você não salvar o conteúdo antes da deleção, o undo é impossível. Armazene o estado anterior no próprio comando no momento da criação.
Histórico ilimitado causando uso excessivo de memória: em editores de longa sessão, um histórico sem limite pode consumir gigabytes. Implemente um limite (ex: últimos 100 comandos) e descarte os mais antigos ao exceder.
Executar comandos diretamente sem passar pelo histórico: se o código chamar a operação diretamente (sem EditorHistorico.executar), o histórico fica dessincronizado e o undo vai desfazer o comando errado.
Perguntas Frequentes
Qual é a diferença entre Command e Strategy? O Strategy substitui um algoritmo inteiro que é executado repetidamente. O Command representa uma operação única que pode ser armazenada, enfileirada ou desfeita. Um Command pode usar uma Strategy para decidir como executar.
Como serializar comandos para persistência (ex: salvar sessão)?
Com union(enum), todos os campos têm tipos concretos conhecidos — use std.json.stringify diretamente. Para deserializar, leia o campo tag primeiro e faça switch para reconstruir o union correto.
É possível compor múltiplos comandos em um macro-comando?
Sim. Crie um variant macro: std.ArrayList(Comando) na union. O executar do macro itera e executa cada sub-comando. O desfazer itera na ordem inversa.
Quando Evitar
- Operações simples sem necessidade de undo ou enfileiramento
- Quando o overhead de encapsular cada ação não se justifica
- Sistemas onde a ordem de execução não importa
Veja Também
- State Machine — Transições de estado estruturadas
- Observer — Notificar após executar comandos
- Pipeline — Processamento sequencial de etapas
- Producer-Consumer — Fila assíncrona de tarefas
- Error Handling — Erros em comandos