Cheatsheet: Dependency Injection em Zig

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

Continue aprendendo Zig

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