Pull to refresh

Нюансы и алгоритмы программирования движка для маркетинговых онлайн-исследований

Reading time13 min
Views849
Доброго времени суток, уважаемые хабравчане. Давно меня подмывало написать подобный мануал, и вот, решил таки себя заставить сесть и написать его — поделиться некоторым опытом, который получил во время своих программистских изысканий в сфере маркетинга и о некоторых алгоритмах, заложенных в движок, на котором был реализован не один проект.

Казалось бы, что может быть проще ТЗ в виде плёвой Word'овской анкетки на 30-40 вопросов, которую заказчик хочет видеть в запрограммированном виде в окне своего браузера, да ещё и даёт на всё про всё целых четыре дня? Как говаривал мой товарищ, пока не начал работать вместе со мной:

— Да тут дел-то часа на четыре! Утром сел, а к вечеру готово всё и оттестировано. Не понимаю, чего ты колупаешься…

Но всё, к сожалению, далеко не так просто, как он представлял и первую свою анкету он программировал в аврале двое суток (с последующим докручиванием в течение двух-трёх дней), постоянно получая всё новые и новые комментарии по телефону и электронной почте. И дело тут не в переоценке своих возможностей или профессиональной несостоятельности (а товарищ, надо сказать, с богатым опытом сайтостроителя), а в самой области, которая весьма специфическая и имеет свои подводные камни и тонкие моменты. Как раз о некоторых из этих подводных камней и тонких моментов, а так же о том, как их избежать, я и хотел бы поведать.

«Хочу красное, но синее» или модифицируемость кода


Не знаю, почему так сложилось, но почему-то именно в маркетинге заказчики любят менять свои требования к анкете по несколько раз в день. Видимо, это связано с внутренней организацией, где левая рука не всегда знает, что делает правая и, порой, хочет совершенно другого. Но не это суть, а суть в том, что это всё выливается в головную боль программиста (к слову сказать, не так давно был случай, когда итоговая версия анкеты для CATI значилась как «V.13.3_final_final», и это уже после утверждения «финального» ТЗ).

Идея создания движка, который бы облегчал труд, сокращал сроки разработки и минимизировал влияние человеческого фактора на возможные ошибки, пришла где-то после четвертой анкеты и решено было программировать движок сразу и на века. За основу была взята наивная идея о возможности «программирования» анкет без знания этого самого программирования и простоте модификации существующих проектов.

Идея была, по сути своей, не такая уж и плохая, но ключевую роль тут сыграло отсутствие ресурсов для реализации (человеко-часов) и неверно выбранный подход: предполагалось, что анкета будет представлять собой Excel-файл, столбцы которого будут отвечать за определенные команды для модуля-конструктора:
  • Порядковый номер вопроса
  • Код вопроса
  • Возможные варианты ответов
  • Коды уникальных вариантов ответов
  • Сами варианты ответов
  • Тип вопроса (checkbox, radio, text и смешанные)
  • Условия задания вопроса
  • Условия остановки опроса
  • Дополнительные вызываемые функции

Решено — сделано. Но первый блин, как водится, комом…

Теория провалов


Обкатывался движок на небольших проектах, но уже после шестого или седьмого стало ясно, что выбранный путь развития тупиковый из-за безмерно разрастающегося функционала. Дошло до того, что я и мой напарник — разработчики — сами уже стали путаться в том, какой функционал заложен. Причины:
  • Возможных вариантов типов представления вопросов на странице (а так же их комбинаций) очень много и каждый проект в чём-то уникален (т.е. фантазия заказчика бурно играет)
  • Логика опроса, порой, бывает крайне замудрёной и её описание в ячейках Excel'я выливалось в сумашедшие неудобоваримые формулы

Были ещё и другие, но второстепенные причины.

Выходом виделось создание визарда, который бы в форме «опрос-ответ» формировал шаблон будущей анкеты, а шаблон бы в дальнейшем доводился до ума во встроенном редакторе. Но всё это новые человеко-часы, бесконечные модификации кода, постоянные обновления и… как результат, своеобразный монстр Франкенштейна. Так что от этого подхода было решено отказаться.

Рассматривались идеи написания внутреннего скриптового языка, программирования демона на С++ (ввиду более широких возможностей), парсера для Word'овских документов (написанных в определенном формате) и многие другие. Но все они были отметены либо из-за своей абсурдности, либо из-за трудоёмкости в реализации. Нужно было найти какое-то простое, но в тоже самое время гибкое решение…

Постановка задачи и решение


Грамотная постановка задачи — это, если и не залог, то хорошее подспорье на пути к успешной реализации задумки.

Имея за плечами уже достаточный опыт программирования онлайн-исследований и столкнувшись с кучей подводных камней и нюансов в этом деле, я обозначил несколько основных (на мой взгляд) пунктов, которым должна была отвечать система:
  • Возможность подключения различных дизайнов и их смена «на лету»
  • Максимальное ускорение процесса программирования
  • Простота записи и модификации логики
  • Быстрая расширяемость функционала
  • Стабильность работы при высоких нагрузках
  • Использование файловой системы для хранения данных

Так же, чтобы минимизировать возможные трудности, было решено следовать нескольким простым правилам:
  • Выносить логику в отдельный файл
  • Каждый вопрос также выносить в отдельный файл
  • Проверять корректность заполнения на стороне клиента
  • Для проверок заполнения всех необходимых полей использовать js-функции, минимально зависящие от структуры конкретного вопроса
  • Использовать структурное программирование

Как результат, на выходе получился не монолитный движок, а комплекс из модуля, выводящего вопросы пользователю, а также js- и php-библиотек функций.

Пришло время более подробно расписать некоторые из вышеобозначенных мной пунктов:

Возможность подключения различных дизайнов и их смена «на лету»

Заказчики часто требуют использования собственного дизайна в маркетинговых исследованиях, даже если заказывают само исследование в сторонней компании. Особенно актуально это для брендов или когда ссылка на анкету вывешивается на сайте клиента (чтобы создавалось впечатление, будто пользователь и не уходил с сайта).

Т.к. у подавляющего большинства опросов был замечен один общий момент — информация изменяется только в одной области, то решением было использование двух функций php: инициализация основных переменных при загрузке анкеты и вывод необходимых данных в div. Выглядит это следующим образом:
<?php
session_start();

include('./php/funcs.php'); // подключили библиотеку функций

if (!$_SESSION['user_ID']){ // если пользователь ранее не заходил на этот опрос
    $user_id = md5(date('y-m-d H:i:s').rand());
    $_SESSION['user_ID'] = $user_id;
    $_SESSION['user_position'] = setUserPosition($user_id); // посылая в эту функцию рандомный хэш, мы создаем новую запись в системе и функция возвращает дэфолтную позицию (обычно "start")
    }
else {
    $_SESSION['user_position'] = setUserPosition($_SESSION['user_ID']); // если ранее заходил, то по хэшированному айдишнику из сессии получаем позицию, на которой юзер закончил в прошлый раз
    }
?>

... // html-абракадабра

<div id="workDiv" class="workDiv" >
<?php
showQuest($_SESSION['user_position']); // собственно, вывод текущего вопроса внутри нужного div'а
?>
</div>

... // html-абракадабра и три кнопки ("Назад", "Далее", "Закончить")
<input type="button" style="width: 75px;" id="backBtn" onclick="request('back');" value="Назад">
<input type="submit" style="width: 75px;" id="submitBtn" onclick="request('forvard');" value="Далее">
<input type="submit" style="width: 75px; display: none;" id="endBtn" onclick="endOfSurvey();" value="Завершить">


Этого хватает для выполнения требований 3/4 всех возможных вариаций опросов. Для оставшейся 1/4 ничего не мешает добавить пару тэгов/функций.

Проблема с изменением дизайна решена.

Максимальное ускорение процесса программирования

Из-за того, что реализовано всё в виде набора функций и основные задачи по обмену данными с сервером и их интерпретации решены и лежат на движке, программирование самих анкет сводится к написанию файла с логикой, прописыванию в js-скрипт соответствий какому вопросу какую проверку и полу-автоматическому созданию шаблонов вопросов в следующей форме:
<form name="form" method="post">
<b>Укажите Ваш пол:</b>
<br /><br />
<input type="radio" name="Z0" id="Z0"  value="1"  /> <div class="textAssociationDiv" > Мужской </div> <br />
<input type="radio" name="Z0" id="Z0"  value="2"  /> <div class="textAssociationDiv" > Женский </div> <br />
</form>



В коде присутствуют несколько div'ов с классом «textAssociationDiv» — это используется для ассоциации клика по тексту, как по определенному чекбоксу или радиобаттону. Если кому интересно, то вот код js-функции, которая этим занимается:
function associateTextToElement(){
    $(".textAssociationDiv").click(function(){document.forms['form'].elements[$(".textAssociationDiv").index($(this)[0])].click();});
    }


Так же, в коде использованы одинаковые id, т.к. это упрощает анализ элементов страницы, если на неё выводится более одного вопроса. Если же необходимо вывести вопрос типа малтипл-сет (множественный выбор), то имена и id принимают следующий вид: S9.1, S9.2, S9.3, S9.4, S9.5, S9.97, где цифра после точки — код ответа.

При нажатии пользователем кнопок «Далее» или «Назад», javascript собирает все введенные данные и Ajax'ом отсылает их php-скрипту в следующем формате: код_переменной=значение. Код переменной берётся из id элемента, потому они в сингл-вопросах одинаковые, а в малтипл-сетах разные — так просто удобнее. Скрипт, получая их методом POST (это важно — скрыть от пользователя подобную информацию), записывает их в файл-лог с именем, хранимым в $_SESSION['user_ID'].

Использование файловой системы для хранения данных

Отказу от использования БД послужило несколько причин, основная из которых: необходимость оперативного добавления/удаления переменных в процессе опроса. Так же этот подход решил трудности с количеством полей в таблице ответов пользователей при использовании MySQL (общее число различных переменных в некоторых исследованиях иногда доходило до полутора-двух тысяч).

При использовании же файловой системы все трудности пропали, т.к. абсолютно все данные, которые отсылались на сервер, записывались в соответствующий файл-лог. А для того, чтобы добавить несколько новых вариантов ответов, достаточно просто добавить пару строк с нужными input'ами в соответствующий файл вопроса и… вауля — все данные сохраняются, а потрачено на это времени было всего несколько секунд. Для простоты интерпретации логов, как компьютером, так и человеком, они имеют следующий формат:

userID=1cb3761177ec8846fe8ae35392017e36
userIP=192.168.1.34
startTime=19.04.10 12:50:21
S4=33
S5=3
S9_8=8 // так записываются малтипл-сеты (S9.8, S9.10 и т.п.)

Вынесение логики в отдельный файл

Проблема с записью логики решилась сама собой, когда решено было, что программирует анкету программист, а менеджер всего лишь даёт ТЗ — т.е. не нужно «упрощать» кому-то жизнь визуальным редактором/визардом (усложняя жизнь программисту), а работать с движком и библиотеками будет человек имеющий представление о js и php. Даже изобретать велосипед не пришлось — при анализе тонны реализованных проектов, было замечено, что «прямая» логика может удовлетворять всем потребностям клиента и для реализации ветвлений любой сложности достаточно всего пары функций, двух переменных и оператора if:

  • $_SESSION['user_ID'] — содержит идентификатор пользователя, он же — имя его файла лога
  • $_SESSION['user_position'] — содержит код вопроса, на который только что ответил пользователь
  • checkAnswer($qCode, array(1, 2, 3)) — проверяет в логе наличие записи qCode=array[0], qCode=array[1] или qCode=array[n], где $qCode — код вопроса (например «S12»), а array содержит искомые значения, а т.к. в малтипл-сетах индекс ответа в переменной вопроса после точки всегда соответствует коду ответа, то при помощи автоподстановки "_n=n" к коду вопроса (где n — вариант ответа), мы получаем строку, которую и ищем в файле — если она есть, значит этот ответ был дан
  • checkQuotas($qCode) — проверяет заполненность квот по коду вопроса, на который только что ответил пользователь (т.к. квоты бывают связные и несвязные, функция имеет разные реализации и под каждый проект квоты программируются отдельно)


Если учесть тот факт, что в онлайн-анкетах вектор опроса всегда идёт в одном направлении, а возможных вариантов ветвления с точки зрения компьютера всего два (либо прошёл условие, либо нет), то вся логика упрощается до предела и сводится к следующему формату (php):
if ($_SESSION['user_position'] == 'S9' && checkAnswer('S9',1)){ // сейчас был вопрос S9 и в нём отмечен 1-й вариант
    $question_contents = file_get_contents('../qst/Z6.php');
    echo iconv("cp1251", "UTF-8", $question_contents);
    //include_once('../qst/Z6.php');
    //echo iconv("cp1251", "UTF-8", showQuest());
    $_SESSION['user_position'] = 'Z6';
    exit;
    }

// следующий вариант применяется, когда внутри файла вопроса используется php (например, для ротации ответов или подстановки одного из предыдущих ответов в текст вопроса)

if ($_SESSION['user_position'] == 'A1'){ // сейчас был вопрос A1
    //$question_contents = file_get_contents('../qst/A3.php');
    //echo iconv("cp1251", "UTF-8", $question_contents);
    include_once('../qst/A3.php');
    echo iconv("cp1251", "UTF-8", showQuest());
    $_SESSION['user_position'] = 'A3';
    exit;
    }


Как видно из приведенного выше кода, оба варианта почти идентичны и подобным образом выглядят условия для всех остальных проверок. Исходя из этого, мы просто подгружаем в соответствующий скрипт список вопросов в порядке их следования, а на выходе получаем сгенерированную логику, которую останется только немного подправить руками (раскомментировать соответствующие блоки в условиях и добавить, если требуется, какие-то дополнительные функции и/или условия). Если нужно поставить условие на целый блок вопросов — это так же решается if'ом (например, открыть у вопроса А1, а закрыть у А8).

Имея такой файл, мы просто добавляем в его начало функцию записи переданных из js данных в лог и всё. Логика готова и вопросы сменяются один за другим. А всё дело в том, что простоты ради, запрос от js направляется прямиком на этот скрипт, который записывает данные, а потом просто продолжает выполняться до тех пор, пока не встретит подходящее условие, при этом скрипт пошлёт в js нужный контент и завершит работу. Js в свою очередь полученные данные вставляет в вышеупомянутый div с id=«workDiv» и цикл повторяется: пользователь отмечает ответы, js отправляет их файлу логики, php записывает данные в лог и возвращает содержимое следующего вопроса, js заменяет содержимое рабочего div'а полученными данными.

После получения данных от сервера и замены содержимого рабочего div'а, js выполняет ещё одну функцию — вызывает функцию привязки проверки валидности заполнения вопроса.

Js-проверки заполнения вопросов

Большинство вопросов, которые используются в онлайн-исследованиях, можно разбить на несколько типов:
  • Единичный выбор (radio)
  • Единичный выбор (select)
  • Полузакрытый (radio + text)
  • Полузакрытый (checkbox + text)
  • Полузакрытый (checkbox + text + уникальные коды)
  • Открытый
  • Множественный выбор (checkbox)
  • Множественный выбор (select)
  • Множественный выбор с уникальными кодами (checkbox)
  • Множественный выбор (таблица высказываний/характеристик из checkbox)
  • Шкалы (таблица оценки тезисов из radio)

Исходя из этого, была написана библиотека функций, которые автоматически привязывают соответствующие проверки на каждый из элементов input.

При первом заходе на анкету, а так же при обновлении блока с вопросом, вызывается js-функция questsChecks(), суть которой, вкратце, сводится к следующему: блокируем кнопку «Далее» (ведь это новый вопрос и данные ещё не введены), получаем из php скрипта значение $_SESSION['user_position'] (т.е. текущего вопроса), запускаем переключатель:
    switch(quest){
        case 'S2':
            disableNext();
            radioMultCheck();
            progressBar(0);
            break;

        case 'S4':
            disableNext();
            checkAgeText(10, 99);
            progressBar(7);
            break;

        case 'S12':
            disableNext();
            radioTextCheck();
            progressBar(14);
            break;

// ... набор подобных условий

        default:
            break;
            }



В зависимости от кода вопроса, у нас вызывается соответствующая библиотечная функция, которая навешивает проверки. Вот пример одной из них (множественный выбор + текст) и двух связанных с ней:
// -----------------------------------------------------------------------------
function checkboxText(check,text){
    disableNext();
    if (!check){
        $("div[id='workDiv'] input[type='checkbox']").click(function(){checkboxText('true');});
        $("div[id='workDiv'] input[type='text']").keyup(function(){checkboxText('true',this);});
        }
    if (check){
        if (!text && $("div[id='workDiv'] input[type='text'][value!='']").length > 0)
            enableNext();
        if (!text && $("div[id='workDiv'] input[type='text'][value!='']").length == 0)
            checkCheckbox();
        if (text && !checkCheckbox()){
            checkText(text);
            }
        }
    }
// -----------------------------------------------------------------------------
function checkCheckbox(uniqCodes){
    if (!uniqCodes){
        uniqText = getUniqTextObj();
        if ($("div[id='workDiv'] input[type='checkbox'][name!='toggleOther']:checked").length > 0){
            enableNext();
            return true;
            }
        if ($("div[id='workDiv'] input[type='checkbox']:checked").length == 0 && !uniqText)
            disableNext();
        if ($("div[id='workDiv'] input[type='checkbox']:checked").length == 0 && uniqText)
            checkText(uniqText);
        }
    if (uniqCodes){
        for (var key in uniqCodes){
            $(document.getElementById(getQuest()+'.'+uniqCodes[key])).attr("checked", "");
            }
        checkCheckbox();
        }
    }
// -----------------------------------------------------------------------------
function checkText(text,uniqCodes){
    if (!uniqCodes){
        if (text.value)
            enableNext();
        else disableNext();
        }
    if (uniqCodes){
        for (var key in uniqCodes){
            $(document.getElementById(getQuest()+'.'+uniqCodes[key])).attr("checked", "");
            }
        if (!checkCheckbox())
            checkText(text);
        }
    }


Не буду комментировать каждую строку, т.к. это замёт слишком много текста, а просто поясню вкратце алгоритм: навешиваем на каждый элемент типа input по событию click() или keyup() вызов этой же самой функции, но с параметром «проверить» и передачей объекта (для текстовых полей). При вызове проверки (при клике), функция пробегается по всем элементам внутри рабочего div'a в соответствии с предопределенным алгоритмом (который исходит из типа вопроса), собирает значения полей, анализирует их и, если это требуется, вызывает более узкоспециализированные функции проверки.

В данном примере, функция проверки множественного выбора с текстом может инициировать запуск функции проверки простого множественного выбора, т.к. простой множественный выбор является упрощенным вариантом множественного выбора с текстом и включается в него. Все функции проверки примитивных типов (к ним относятся: множественный выбор, единичный выбор, открытый вопрос) спроектированы таким образом, что проверяют только типы полей примитивов. Таким образом, автоматически разбирая сложные типы вопросов на простые и производя проверки над ними, мы получаем искомый результат — true либо false — прошла проверка или нет. И, если да — снимаем блокировку с кнопки «Далее», если же нет — ничего не делаем, либо блокируем её, если она доступна.

Если проверка прошла успешна — начинается новый цикл: отправил, получил, заполнил, проверил, отправил и т.д.

Использование структурного программирования

Использование голых функций, вместо классов и объектов, в данном случае (на мой взгляд) более предпочтительно с точки зрения производительности. При большом потоке пользователей, создание объекта с кучей свойств только ради использования одного конкретного метода — по меньшей мере, не разумно. Манипуляции с пятью-десятью объектами более затратны с точки зрения ресурсов, чем аналогичные действия с функциями.

К тому, же в связи с простотой архитектуры движка, в ООП тут просто не вижу смысла.

Подводя итоги



Данная архитектура движка отработала на ура в одном из исследований, в котором число пользователей, получивших приглашения на участие, было порядка 120-130 тысяч человек, при чём лояльных пользователей — клиентов заказчика, да и сама рассылка была от лица заказчика — при этом было получено порядка 22k результативных интервью, а общее число заходов было около 45k.

Более мелких же исследований было проведено огромное множество.

Статистика говорит, что такая архитектура надёжна и высокопроизводительна. Помимо этого, она ещё и удобна (на мой взгляд) и достаточно гибка.

При наличии достаточного количества свободного времени я, возможно, ещё вернусь к доработке этого движка и, опять же, возможно, выложу все исходники в общий доступ, вместе с подробным описанием функционала и всякими плюшками. Но это всё только «возможно»…

В своё время, когда я только начинал погружаться в данную сферу, недостаток информации по теме был катастрофический и все задачи приходилось решать, зачастую, методом тыка — проб и ошибок.

Надеюсь, что данный мануал и мой личный опыт, который я отразил в нём, чем-то помогли вам.

А ещё я надеюсь, что этот материал как-то восполнит информационный вакуум вокруг такой темы, как программирование онлайн-анкет в маркетинговых исследованиях.

Но, к сожалению, остаются не охваченными такие темы, как программирование квот, конджойнт или программирование анкет и/или движка для CATI (собственно, как и организация технической базы самой CATI-студии). И многое другое…

Но это всё темы для отдельных FAQ и мануалов.

Спасибо за внимание.
Tags:
Hubs:
Total votes 25: ↑17 and ↓8+9
Comments0

Articles