Perguntas de Entrevista sobre Design Patterns em Zig
Design patterns em Zig diferem significativamente dos padrões clássicos de OOP. Zig favorece composição sobre herança, explicitação sobre magia, e simplicidade sobre sofisticação. Entrevistadores de posições seniores avaliam sua capacidade de projetar APIs e arquiteturas idiomáticas em Zig.
Padrões Idiomáticos de Zig
Explique o Allocator Pattern.
O padrão mais fundamental de Zig: funções que alocam memória recebem um Allocator como parâmetro, tornando alocação explícita e testável. Veja perguntas de memória para detalhes aprofundados.
const Parser = struct {
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) Parser {
return .{ .allocator = allocator };
}
pub fn parse(self: *Parser, input: []const u8) !Ast {
const nodes = try self.allocator.alloc(Node, 100);
errdefer self.allocator.free(nodes);
// ...
}
};
Como Zig implementa interfaces/polimorfismo?
Zig não tem interfaces ou classes abstratas. O polimorfismo é alcançado via:
1. Comptime duck typing (mais idiomático):
fn processar(writer: anytype) !void {
try writer.writeAll("dados");
}
// Aceita qualquer tipo que tenha .writeAll
2. Fat pointers (vtable manual):
const Writer = struct {
ptr: *anyopaque,
writeFn: *const fn (*anyopaque, []const u8) error{...}!usize,
pub fn write(self: Writer, data: []const u8) !usize {
return self.writeFn(self.ptr, data);
}
};
A biblioteca padrão usa esse padrão extensivamente (ex: std.mem.Allocator).
Explique o padrão Init/Deinit.
O equivalente de Zig ao RAII de C++ — explícito e sem magia:
const Recurso = struct {
dados: []u8,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) !Recurso {
return .{
.dados = try allocator.alloc(u8, 1024),
.allocator = allocator,
};
}
pub fn deinit(self: *Recurso) void {
self.allocator.free(self.dados);
}
};
// Uso:
var r = try Recurso.init(allocator);
defer r.deinit();
O que é o padrão “return error or value” (Builder pattern para erros)?
fn configurar() !Config {
var config = Config{};
config.host = try resolverHost();
errdefer config.host.deinit();
config.porta = try lerPorta();
config.tls = try configurarTLS();
errdefer config.tls.deinit();
return config;
}
Cada passo pode falhar, e errdefer garante cleanup de todos os passos anteriores.
Padrões de Composição
Como compor funcionalidade sem herança?
Zig usa composição de structs e embeddings:
const Logger = struct {
pub fn log(self: *Logger, msg: []const u8) void { ... }
};
const HttpServer = struct {
logger: Logger,
config: Config,
pub fn handleRequest(self: *HttpServer, req: Request) !Response {
self.logger.log("Recebida requisição");
// ...
}
};
Explique o padrão Sentinel.
Zig usa sentinels para marcar o fim de sequências — similar a null-terminated strings em C, mas generalizado:
// Slice com sentinel 0
const str: [:0]const u8 = "hello"; // null-terminated
const arr: [5:0]u8 = .{ 1, 2, 3, 4, 5 }; // sentinel 0 após o último
Sentinels são verificados em tempo de compilação, eliminando classes de bugs de C.
Padrões Avançados
Como implementar o padrão Observer sem herança?
Em Zig, o padrão Observer é implementado com callbacks armazenados em uma lista de function pointers, sem necessidade de classes abstratas:
const EventoTipo = enum { conectado, desconectado, mensagem_recebida };
const Listener = struct {
ptr: *anyopaque,
callbackFn: *const fn (*anyopaque, EventoTipo) void,
pub fn notificar(self: Listener, evento: EventoTipo) void {
self.callbackFn(self.ptr, evento);
}
};
const EventBus = struct {
listeners: std.ArrayList(Listener),
pub fn publicar(self: *EventBus, evento: EventoTipo) void {
for (self.listeners.items) |l| l.notificar(evento);
}
};
Esse padrão é mais explícito que herança virtual: o tamanho da vtable é visível, o overhead é previsível e não há despacho dinâmico oculto.
O que é o padrão “tagged union” para polimorfismo de dados?
Tagged unions em Zig permitem representar valores de tipos diferentes de forma segura e eficiente:
const Expressao = union(enum) {
numero: f64,
soma: struct { esq: *Expressao, dir: *Expressao },
produto: struct { esq: *Expressao, dir: *Expressao },
pub fn avaliar(self: Expressao) f64 {
return switch (self) {
.numero => |n| n,
.soma => |s| s.esq.avaliar() + s.dir.avaliar(),
.produto => |p| p.esq.avaliar() * p.dir.avaliar(),
};
}
};
O compilador garante que todos os casos do switch sejam tratados, eliminando a classe de bugs de “case não tratado” comum em linguagens com polimorfismo dinâmico. Este padrão é fundamental para implementar ASTs, parsers e interpretadores em Zig.
Dicas de Entrevista para Design Patterns
Em entrevistas seniores, as perguntas sobre design patterns em Zig raramente pedem a implementação do padrão em si — elas avaliam se você entende por que certos padrões do mundo OOP não se aplicam a Zig e quais são as alternativas idiomáticas. Quando o entrevistador perguntar sobre Singleton, por exemplo, a resposta esperada é que Zig tem variáveis globais explícitas ou constantes de arquivo que servem ao mesmo propósito sem o overhead e a complexidade de uma classe gerenciada. Quando perguntarem sobre Strategy Pattern, mostre como comptime e function pointers resolvem o problema de forma mais eficiente e sem despacho dinâmico implícito. Demonstre que você pensa em termos de composição de dados e funções, não de hierarquias de objetos — essa mentalidade é o que separa um desenvolvedor Zig idiomático de alguém que simplesmente traduz código OOP para Zig.
Preparação
Design patterns são testados em posições seniores. Estude o código-fonte da biblioteca padrão de Zig para exemplos de bons padrões. Combine com perguntas de error handling, memória e comptime. Explore o ecossistema para ver padrões em projetos reais e pratique com desafios.