Dependency Injection em Zig
Dependency Injection (DI) é o princípio de fornecer dependências a um módulo de fora em vez de criá-las internamente. Em Zig, DI é uma prática natural e idiomática — o sistema de allocators já é DI puro, e a linguagem incentiva passar dependências explicitamente como parâmetros.
Quando Usar
- Permitir testes unitários com mocks/stubs
- Desacoplar módulos para manutenção e evolução
- Permitir diferentes implementações (dev, test, prod)
- Seguir o princípio de inversão de dependência
O Allocator como Dependency Injection
O exemplo mais pervasivo de DI em Zig é o sistema de allocators:
const std = @import("std");
// A função NÃO cria seu próprio allocator — recebe de fora
fn processarDados(allocator: std.mem.Allocator, entrada: []const u8) ![]u8 {
var resultado = std.ArrayList(u8).init(allocator);
defer resultado.deinit();
for (entrada) |byte| {
try resultado.append(byte);
}
return resultado.toOwnedSlice();
}
pub fn main() !void {
// Em produção: GeneralPurposeAllocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const dados = try processarDados(gpa.allocator(), "hello");
defer gpa.allocator().free(dados);
_ = dados;
}
test "processarDados com testing allocator" {
// Em testes: testing.allocator (detecta leaks)
const dados = try processarDados(std.testing.allocator, "test");
defer std.testing.allocator.free(dados);
try std.testing.expectEqualStrings("test", dados);
}
DI via Parâmetros de Struct
const std = @import("std");
// Interface de logger
const Logger = struct {
ptr: *anyopaque,
logFn: *const fn (*anyopaque, []const u8) void,
pub fn log(self: Logger, msg: []const u8) void {
self.logFn(self.ptr, msg);
}
};
// Implementação real
const ConsoleLogger = struct {
pub fn logger(self: *ConsoleLogger) Logger {
return .{
.ptr = @ptrCast(self),
.logFn = @ptrCast(&logImpl),
};
}
fn logImpl(self: *ConsoleLogger, msg: []const u8) void {
_ = self;
std.debug.print("[LOG] {s}\n", .{msg});
}
};
// Implementação para testes
const NullLogger = struct {
mensagens: std.ArrayList([]const u8),
pub fn init(allocator: std.mem.Allocator) NullLogger {
return .{ .mensagens = std.ArrayList([]const u8).init(allocator) };
}
pub fn deinit(self: *NullLogger) void {
self.mensagens.deinit();
}
pub fn logger(self: *NullLogger) Logger {
return .{
.ptr = @ptrCast(self),
.logFn = @ptrCast(&logImpl),
};
}
fn logImpl(self: *NullLogger, msg: []const u8) void {
self.mensagens.append(msg) catch {};
}
};
// Serviço que recebe logger injetado
const Servico = struct {
logger: Logger,
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator, log: Logger) Servico {
return .{
.logger = log,
.allocator = allocator,
};
}
pub fn processar(self: *Servico) !void {
self.logger.log("Iniciando processamento");
// ... lógica de negócio ...
self.logger.log("Processamento concluído");
}
};
pub fn main() !void {
var console = ConsoleLogger{};
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var servico = Servico.init(gpa.allocator(), console.logger());
try servico.processar();
}
test "servico com null logger" {
var null_logger = NullLogger.init(std.testing.allocator);
defer null_logger.deinit();
var servico = Servico.init(std.testing.allocator, null_logger.logger());
try servico.processar();
try std.testing.expectEqual(@as(usize, 2), null_logger.mensagens.items.len);
}
DI via comptime
const std = @import("std");
fn Repositorio(comptime Backend: type) type {
return struct {
const Self = @This();
backend: Backend,
pub fn init(backend: Backend) Self {
return .{ .backend = backend };
}
pub fn salvar(self: *Self, chave: []const u8, valor: []const u8) !void {
try self.backend.set(chave, valor);
}
pub fn buscar(self: *Self, chave: []const u8) !?[]const u8 {
return self.backend.get(chave);
}
};
}
// Em produção: RedisBackend
// Em testes: MemoryBackend
const MemoryBackend = struct {
dados: std.StringHashMap([]const u8),
pub fn init(allocator: std.mem.Allocator) MemoryBackend {
return .{ .dados = std.StringHashMap([]const u8).init(allocator) };
}
pub fn deinit(self: *MemoryBackend) void {
self.dados.deinit();
}
pub fn set(self: *MemoryBackend, chave: []const u8, valor: []const u8) !void {
try self.dados.put(chave, valor);
}
pub fn get(self: *MemoryBackend, chave: []const u8) !?[]const u8 {
return self.dados.get(chave);
}
};
test "repositorio com memory backend" {
var backend = MemoryBackend.init(std.testing.allocator);
defer backend.deinit();
var repo = Repositorio(MemoryBackend).init(backend);
try repo.salvar("chave", "valor");
const resultado = try repo.buscar("chave");
try std.testing.expectEqualStrings("valor", resultado.?);
}
Quando Evitar
- Módulos extremamente simples sem dependências externas
- Quando o overhead de abstrair a dependência não se justifica
- Funções puras sem efeitos colaterais (não precisam de DI)
Veja Também
- Singleton — Alternativa com instância global
- Factory — Criar implementações dinâmicamente
- Strategy — Trocar algoritmos (forma de DI)
- Testing — Testes com mocks via DI
- Allocators — O DI mais fundamental de Zig