Factory em Zig
O padrão Factory encapsula a lógica de criação de objetos, permitindo que o código cliente crie instâncias sem conhecer os detalhes de implementação. Em Zig, esse padrão é elegantemente implementado usando comptime, tagged unions e funções de inicialização.
Quando Usar
- Criação de objetos depende de configuração ou parâmetros de runtime
- Diferentes implementações de uma mesma interface
- Encapsular lógica complexa de inicialização
- Criar objetos para diferentes plataformas ou backends
Factory Function Simples
const std = @import("std");
const Transporte = union(enum) {
carro: Carro,
bicicleta: Bicicleta,
onibus: Onibus,
const Carro = struct { velocidade_max: u32 = 120, assentos: u8 = 5 };
const Bicicleta = struct { velocidade_max: u32 = 30, assentos: u8 = 1 };
const Onibus = struct { velocidade_max: u32 = 80, assentos: u8 = 40 };
pub fn criar(tipo: []const u8) !Transporte {
if (std.mem.eql(u8, tipo, "carro")) return .{ .carro = .{} };
if (std.mem.eql(u8, tipo, "bicicleta")) return .{ .bicicleta = .{} };
if (std.mem.eql(u8, tipo, "onibus")) return .{ .onibus = .{} };
return error.TipoDesconhecido;
}
pub fn velocidadeMax(self: Transporte) u32 {
return switch (self) {
.carro => |c| c.velocidade_max,
.bicicleta => |b| b.velocidade_max,
.onibus => |o| o.velocidade_max,
};
}
pub fn assentos(self: Transporte) u8 {
return switch (self) {
.carro => |c| c.assentos,
.bicicleta => |b| b.assentos,
.onibus => |o| o.assentos,
};
}
};
pub fn main() !void {
const t = try Transporte.criar("carro");
std.debug.print("Velocidade máx: {d} km/h\n", .{t.velocidadeMax()});
std.debug.print("Assentos: {d}\n", .{t.assentos()});
}
Factory Genérica com comptime
const std = @import("std");
fn Serializador(comptime formato: []const u8) type {
if (std.mem.eql(u8, formato, "json")) {
return struct {
pub fn serializar(dados: anytype, writer: anytype) !void {
try std.json.stringify(dados, .{}, writer);
}
};
} else if (std.mem.eql(u8, formato, "csv")) {
return struct {
pub fn serializar(dados: anytype, writer: anytype) !void {
const fields = std.meta.fields(@TypeOf(dados));
inline for (fields, 0..) |campo, i| {
if (i > 0) try writer.writeAll(",");
try writer.print("{any}", .{@field(dados, campo.name)});
}
try writer.writeAll("\n");
}
};
} else {
@compileError("Formato não suportado: " ++ formato);
}
}
// Uso — tipo resolvido em comptime
const JsonSerializer = Serializador("json");
const CsvSerializer = Serializador("csv");
Factory com Allocator
const std = @import("std");
const Conexao = struct {
host: []const u8,
porta: u16,
allocator: std.mem.Allocator,
pub fn criar(allocator: std.mem.Allocator, config: Config) !*Conexao {
const conn = try allocator.create(Conexao);
conn.* = .{
.host = try allocator.dupe(u8, config.host),
.porta = config.porta,
.allocator = allocator,
};
return conn;
}
pub fn destruir(self: *Conexao) void {
self.allocator.free(self.host);
self.allocator.destroy(self);
}
};
const Config = struct {
host: []const u8 = "localhost",
porta: u16 = 5432,
tipo: []const u8 = "postgres",
};
fn criarConexao(allocator: std.mem.Allocator, config: Config) !*Conexao {
// Factory pode escolher implementação baseada no config
return Conexao.criar(allocator, config);
}
Considerações de Performance
- Factory com comptime é custo zero:
Serializador("json")resolve o tipo inteiro em tempo de compilação. O código final é idêntico ao que você escreveria manualmente — sem overhead de despacho dinâmico. - Factory com allocator: cada chamada a
Conexao.criarfaz uma alocação. Se você cria conexões frequentemente, considere um Pool de Objetos como backend — a factory pode checar o pool antes de alocar. - Tagged union como factory result: retornar uma
union(enum)em vez de um ponteiro alocado é frequentemente mais eficiente. A union vive na stack, não precisa dedestroy, e o compilador pode otimizar os casos estáticos.
Erros Comuns
Factory que retorna *T quando poderia retornar T: alocar um objeto com allocator.create(T) quando o objeto poderia viver na stack do caller adiciona overhead desnecessário. Retorne o valor diretamente (T) quando possível — use ponteiros apenas quando o objeto precisa sobreviver ao escopo da função criadora ou quando é genuinamente grande demais para a stack.
Não implementar deinit correspondente: toda factory que usa allocator.create ou allocator.dupe deve ter uma função correspondente de destruição (destruir, deinit, destroy). Documente claramente quem é responsável por liberar o objeto criado.
Usar @compileError com mensagem ruim: ao validar parâmetros comptime, escreva mensagens de erro claras: @compileError("Formato não suportado: '" ++ formato ++ "'. Use \"json\" ou \"csv\"."). Uma mensagem vaga vai custar tempo de depuração.
Perguntas Frequentes
Quando usar Factory vs construção direta .{ .campo = valor }?
Use a construção direta quando os campos são simples e o caller pode fornecer todos os valores sem lógica adicional. Use Factory quando há validação, defaults complexos, lógica de decisão baseada em parâmetros, ou quando o tipo exato depende de condições de runtime.
Como implementar um “Abstract Factory” em Zig?
Combine comptime com tagged unions. Um tipo Backend pode ser uma union(enum) com variantes para cada implementação. Uma função factory recebe um parâmetro de configuração e retorna o Backend correto. O switch sobre a union garante que todos os casos sejam tratados.
Factory pode retornar tipos diferentes dependendo dos parâmetros?
Com comptime, sim — if (std.mem.eql(u8, formato, "json")) return JsonType else return CsvType. Em runtime, a forma idiomática é retornar uma union(enum) que contém um dos tipos possíveis.
Quando Evitar
- Objetos simples que não precisam de lógica de criação complexa
- Quando a construção direta com
.{ ... }é clara o suficiente - Abstrações desnecessárias que adicionam complexidade sem benefício
Veja Também
- Builder — Construção passo a passo de objetos complexos
- Singleton — Garantir instância única
- Comptime — Factory usando metaprogramação
- Structs — Inicialização de structs em Zig
- Enums e Unions — Tagged unions para polimorfismo