لماذا اختبار A/B ليس فقط للويب والتسويق
\nعندما نسمع "اختبار A/B"، تتبادر إلى الأذهان الصفحات المقصودة، أزرار "شراء" ومعدلات التحويل في Google Analytics. ولكن ماذا لو كان منتجك ليس موقعًا إلكترونيًا، بل خادمًا عالي التحميل بلغة C++، أو نظامًا مضمنًا، أو محرك ألعاب؟ هل يمكن تطبيق اختبارات A/B هنا؟ بالتأكيد! وهذا ليس مجرد مصطلح عصري، بل أداة حقيقية لاتخاذ القرارات بناءً على البيانات، وليس على الحدس.
\nفي C++، يساعد اختبار A/B في الإجابة على أسئلة مثل: "هل خوارزمية الفرز الجديدة أسرع حقًا؟"، "هل يستحق إدخال تجمع الخيوط؟"، "هل سيحسن تنسيق التسلسل الجديد وقت استجابة API؟". بدون اختبار A/B، فإنك تخاطر بتطبيق "تحسين" قد يتحول في الواقع إلى تراجع. في هذه المقالة، سنشرح كيفية دمج آلية التجارب بشكل صحيح في كود C++، دون تحويله إلى فوضى من if-else.
\n\nبنية اختبار A/B في C++: بدون ألم ومعاناة
\nالخطر الرئيسي لاختبار A/B في C++ هو تلويث كود الإنتاج بالعبارات الشرطية. لتجنب ذلك، يجب فصل منطق التجربة عن منطق الأعمال. نستخدم نمط Feature Flag (علم الميزة) والاستراتيجية. دعنا ننظر إلى البنية الأساسية.
\n\nالخطوة 1: تكوين مركزي
\nجميع الأعلام ومعلمات التجارب تُخزن في مكان واحد. يمكن أن يكون ملف JSON، أو قاعدة بيانات، أو خادم تكوين عن بُعد. للتبسيط، سنستخدم 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}\nيتم تحديد توزيع الحركة (traffic split) بالنسب المئوية. في C++، نقوم بتحميل هذا التكوين مرة واحدة عند بدء تشغيل التطبيق.
\n\nالخطوة 2: فئة مدير التجارب
\nسننشئ فئة آمنة للخيوط ExperimentManager، والتي تحدد بناءً على مفتاح (مثل معرف المستخدم أو المعاملة) المجموعة التي يقع فيها الكائن.
#include <string>\n#include <unordered_map>\n#include <random>\n#include <shared_mutex>\n#include <nlohmann/json.hpp> // لتحليل JSON\n\nclass ExperimentManager {\npublic:\n explicit ExperimentManager(const std::string& config_path);\n \n // يعيد اسم المتغير لتجربة معينة ومفتاح معين\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; // الاسم، الوزن\n int total_weight;\n };\n \n std::unordered_map<std::string, Experiment> experiments_;\n mutable std::shared_mutex mutex_;\n \n // تجزئة حتمية لتوزيع مستقر\n size_t hashAndMod(const std::string& key, int mod) const;\n};\n\n// التنفيذ (مبسط)\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}\nهام: نستخدم التجزئة الحتمية - نفس المستخدم يقع دائمًا في نفس المجموعة. هذا أمر بالغ الأهمية لصحة الاختبار.
\n\n