Por que o teste A/B não é só sobre web e marketing
\nQuando ouvimos "teste A/B", vêm à mente landing pages, botões "Comprar" e conversões no Google Analytics. Mas e se o seu produto não for um site, mas um servidor de alta carga em C++, um sistema embarcado ou um motor de jogo? É possível aplicar testes A/B aqui? Com certeza! E isso não é apenas um termo da moda, mas uma ferramenta real para tomar decisões baseadas em dados, e não na intuição.
\nEm C++, o teste A/B ajuda a responder perguntas como: "O novo algoritmo de ordenação é realmente mais rápido?", "Vale a pena introduzir um pool de threads?", "O novo formato de serialização melhorará o tempo de resposta da API?". Sem um teste A/B, você corre o risco de implementar uma "otimização" que, na prática, se revela uma regressão. Neste artigo, vamos analisar como incorporar corretamente um mecanismo de experimentos no código C++, sem transformá-lo em um emaranhado de if-else.
\n\nArquitetura de teste A/B em C++: sem dor e sofrimento
\nO principal perigo do teste A/B em C++ é poluir o código de produção com operadores condicionais. Para evitar isso, é necessário separar a lógica do experimento da lógica de negócios. Usamos o padrão Feature Flag (flag de funcionalidade) e a estratégia. Vamos considerar a arquitetura básica.
\n\nPasso 1: Configuração centralizada
\nTodas as flags e parâmetros dos experimentos são armazenados em um único local. Pode ser um arquivo JSON, um banco de dados ou um servidor de configuração remoto. Para simplificar, usaremos JSON.
\n// experiment_config.json\n{\n "experiments": {\n "sort_algorithm_v2": {\n "enabled": true,\n "variants": {\n "control": 50,\n "treatment": 50\n }\n },\n "memory_pool_size": {\n "enabled": true,\n "variants": {\n "default": 33,\n "pool_256": 33,\n "pool_512": 34\n }\n }\n }\n}\nA divisão de tráfego (traffic split) é definida em porcentagens. Em C++, carregamos esta configuração uma vez na inicialização da aplicação.
\n\nPasso 2: Classe gerenciadora de experimentos
\nVamos criar uma classe thread-safe ExperimentManager, que, com base em uma chave (por exemplo, ID do usuário ou transação), determina em qual grupo o objeto se enquadra.
#include <string>\n#include <unordered_map>\n#include <random>\n#include <shared_mutex>\n#include <nlohmann/json.hpp> // para parsing de JSON\n\nclass ExperimentManager {\npublic:\n explicit ExperimentManager(const std::string& config_path);\n \n // Retorna o nome da variante para o experimento e chave fornecidos\n std::string getVariant(const std::string& experiment_name, \n const std::string& user_id) const;\n\nprivate:\n struct Experiment {\n bool enabled;\n std::vector<std::pair<std::string, int>> variants; // nome, peso\n int total_weight;\n };\n \n std::unordered_map<std::string, Experiment> experiments_;\n mutable std::shared_mutex mutex_;\n \n // Hashing determinístico para distribuição estável\n size_t hashAndMod(const std::string& key, int mod) const;\n};\n\n// Implementação (simplificada)\nstd::string ExperimentManager::getVariant(const std::string& exp_name,\n const std::string& user_id) const {\n std::shared_lock lock(mutex_);\n auto it = experiments_.find(exp_name);\n if (it == experiments_.end() || !it->second.enabled) {\n return "control"; // fallback\n }\n const auto& exp = it->second;\n size_t bucket = hashAndMod(user_id, exp.total_weight);\n int cumulative = 0;\n for (const auto& [name, weight] : exp.variants) {\n cumulative += weight;\n if (bucket < cumulative) {\n return name;\n }\n }\n return "control";\n}\nImportante: Usamos hashing determinístico — o mesmo usuário sempre cai no mesmo grupo. Isso é crítico para a correção do teste.
\n\n