Доброго времени суток, уважаемые хабравчане. Давно меня подмывало написать подобный мануал, и вот, решил таки себя заставить сесть и написать его — поделиться некоторым опытом, который получил во время своих программистских изысканий в сфере маркетинга и о некоторых алгоритмах, заложенных в движок, на котором был реализован не один проект.
Казалось бы, что может быть проще ТЗ в виде плёвой Word'овской анкетки на 30-40 вопросов, которую заказчик хочет видеть в запрограммированном виде в окне своего браузера, да ещё и даёт на всё про всё целых четыре дня? Как говаривал мой товарищ, пока не начал работать вместе со мной:
— Да тут дел-то часа на четыре! Утром сел, а к вечеру готово всё и оттестировано. Не понимаю, чего ты колупаешься…
Но всё, к сожалению, далеко не так просто, как он представлял и первую свою анкету он программировал в аврале двое суток (с последующим докручиванием в течение двух-трёх дней), постоянно получая всё новые и новые комментарии по телефону и электронной почте. И дело тут не в переоценке своих возможностей или профессиональной несостоятельности (а товарищ, надо сказать, с богатым опытом сайтостроителя), а в самой области, которая весьма специфическая и имеет свои подводные камни и тонкие моменты. Как раз о некоторых из этих подводных камней и тонких моментов, а так же о том, как их избежать, я и хотел бы поведать.
Не знаю, почему так сложилось, но почему-то именно в маркетинге заказчики любят менять свои требования к анкете по несколько раз в день. Видимо, это связано с внутренней организацией, где левая рука не всегда знает, что делает правая и, порой, хочет совершенно другого. Но не это суть, а суть в том, что это всё выливается в головную боль программиста (к слову сказать, не так давно был случай, когда итоговая версия анкеты для CATI значилась как «V.13.3_final_final», и это уже после утверждения «финального» ТЗ).
Идея создания движка, который бы облегчал труд, сокращал сроки разработки и минимизировал влияние человеческого фактора на возможные ошибки, пришла где-то после четвертой анкеты и решено было программировать движок сразу и на века. За основу была взята наивная идея о возможности «программирования» анкет без знания этого самого программирования и простоте модификации существующих проектов.
Идея была, по сути своей, не такая уж и плохая, но ключевую роль тут сыграло отсутствие ресурсов для реализации (человеко-часов) и неверно выбранный подход: предполагалось, что анкета будет представлять собой Excel-файл, столбцы которого будут отвечать за определенные команды для модуля-конструктора:
Решено — сделано. Но первый блин, как водится, комом…
Обкатывался движок на небольших проектах, но уже после шестого или седьмого стало ясно, что выбранный путь развития тупиковый из-за безмерно разрастающегося функционала. Дошло до того, что я и мой напарник — разработчики — сами уже стали путаться в том, какой функционал заложен. Причины:
Были ещё и другие, но второстепенные причины.
Выходом виделось создание визарда, который бы в форме «опрос-ответ» формировал шаблон будущей анкеты, а шаблон бы в дальнейшем доводился до ума во встроенном редакторе. Но всё это новые человеко-часы, бесконечные модификации кода, постоянные обновления и… как результат, своеобразный монстр Франкенштейна. Так что от этого подхода было решено отказаться.
Рассматривались идеи написания внутреннего скриптового языка, программирования демона на С++ (ввиду более широких возможностей), парсера для Word'овских документов (написанных в определенном формате) и многие другие. Но все они были отметены либо из-за своей абсурдности, либо из-за трудоёмкости в реализации. Нужно было найти какое-то простое, но в тоже самое время гибкое решение…
Грамотная постановка задачи — это, если и не залог, то хорошее подспорье на пути к успешной реализации задумки.
Имея за плечами уже достаточный опыт программирования онлайн-исследований и столкнувшись с кучей подводных камней и нюансов в этом деле, я обозначил несколько основных (на мой взгляд) пунктов, которым должна была отвечать система:
Так же, чтобы минимизировать возможные трудности, было решено следовать нескольким простым правилам:
Как результат, на выходе получился не монолитный движок, а комплекс из модуля, выводящего вопросы пользователю, а также js- и php-библиотек функций.
Пришло время более подробно расписать некоторые из вышеобозначенных мной пунктов:
Возможность подключения различных дизайнов и их смена «на лету»
Заказчики часто требуют использования собственного дизайна в маркетинговых исследованиях, даже если заказывают само исследование в сторонней компании. Особенно актуально это для брендов или когда ссылка на анкету вывешивается на сайте клиента (чтобы создавалось впечатление, будто пользователь и не уходил с сайта).
Т.к. у подавляющего большинства опросов был замечен один общий момент — информация изменяется только в одной области, то решением было использование двух функций php: инициализация основных переменных при загрузке анкеты и вывод необходимых данных в div. Выглядит это следующим образом:
Этого хватает для выполнения требований 3/4 всех возможных вариаций опросов. Для оставшейся 1/4 ничего не мешает добавить пару тэгов/функций.
Проблема с изменением дизайна решена.
Максимальное ускорение процесса программирования
Из-за того, что реализовано всё в виде набора функций и основные задачи по обмену данными с сервером и их интерпретации решены и лежат на движке, программирование самих анкет сводится к написанию файла с логикой, прописыванию в js-скрипт соответствий какому вопросу какую проверку и полу-автоматическому созданию шаблонов вопросов в следующей форме:
В коде присутствуют несколько div'ов с классом «textAssociationDiv» — это используется для ассоциации клика по тексту, как по определенному чекбоксу или радиобаттону. Если кому интересно, то вот код js-функции, которая этим занимается:
Так же, в коде использованы одинаковые 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:
Если учесть тот факт, что в онлайн-анкетах вектор опроса всегда идёт в одном направлении, а возможных вариантов ветвления с точки зрения компьютера всего два (либо прошёл условие, либо нет), то вся логика упрощается до предела и сводится к следующему формату (php):
Как видно из приведенного выше кода, оба варианта почти идентичны и подобным образом выглядят условия для всех остальных проверок. Исходя из этого, мы просто подгружаем в соответствующий скрипт список вопросов в порядке их следования, а на выходе получаем сгенерированную логику, которую останется только немного подправить руками (раскомментировать соответствующие блоки в условиях и добавить, если требуется, какие-то дополнительные функции и/или условия). Если нужно поставить условие на целый блок вопросов — это так же решается if'ом (например, открыть у вопроса А1, а закрыть у А8).
Имея такой файл, мы просто добавляем в его начало функцию записи переданных из js данных в лог и всё. Логика готова и вопросы сменяются один за другим. А всё дело в том, что простоты ради, запрос от js направляется прямиком на этот скрипт, который записывает данные, а потом просто продолжает выполняться до тех пор, пока не встретит подходящее условие, при этом скрипт пошлёт в js нужный контент и завершит работу. Js в свою очередь полученные данные вставляет в вышеупомянутый div с id=«workDiv» и цикл повторяется: пользователь отмечает ответы, js отправляет их файлу логики, php записывает данные в лог и возвращает содержимое следующего вопроса, js заменяет содержимое рабочего div'а полученными данными.
После получения данных от сервера и замены содержимого рабочего div'а, js выполняет ещё одну функцию — вызывает функцию привязки проверки валидности заполнения вопроса.
Js-проверки заполнения вопросов
Большинство вопросов, которые используются в онлайн-исследованиях, можно разбить на несколько типов:
Исходя из этого, была написана библиотека функций, которые автоматически привязывают соответствующие проверки на каждый из элементов input.
При первом заходе на анкету, а так же при обновлении блока с вопросом, вызывается js-функция questsChecks(), суть которой, вкратце, сводится к следующему: блокируем кнопку «Далее» (ведь это новый вопрос и данные ещё не введены), получаем из php скрипта значение $_SESSION['user_position'] (т.е. текущего вопроса), запускаем переключатель:
В зависимости от кода вопроса, у нас вызывается соответствующая библиотечная функция, которая навешивает проверки. Вот пример одной из них (множественный выбор + текст) и двух связанных с ней:
Не буду комментировать каждую строку, т.к. это замёт слишком много текста, а просто поясню вкратце алгоритм: навешиваем на каждый элемент типа input по событию click() или keyup() вызов этой же самой функции, но с параметром «проверить» и передачей объекта (для текстовых полей). При вызове проверки (при клике), функция пробегается по всем элементам внутри рабочего div'a в соответствии с предопределенным алгоритмом (который исходит из типа вопроса), собирает значения полей, анализирует их и, если это требуется, вызывает более узкоспециализированные функции проверки.
В данном примере, функция проверки множественного выбора с текстом может инициировать запуск функции проверки простого множественного выбора, т.к. простой множественный выбор является упрощенным вариантом множественного выбора с текстом и включается в него. Все функции проверки примитивных типов (к ним относятся: множественный выбор, единичный выбор, открытый вопрос) спроектированы таким образом, что проверяют только типы полей примитивов. Таким образом, автоматически разбирая сложные типы вопросов на простые и производя проверки над ними, мы получаем искомый результат — true либо false — прошла проверка или нет. И, если да — снимаем блокировку с кнопки «Далее», если же нет — ничего не делаем, либо блокируем её, если она доступна.
Если проверка прошла успешна — начинается новый цикл: отправил, получил, заполнил, проверил, отправил и т.д.
Использование структурного программирования
Использование голых функций, вместо классов и объектов, в данном случае (на мой взгляд) более предпочтительно с точки зрения производительности. При большом потоке пользователей, создание объекта с кучей свойств только ради использования одного конкретного метода — по меньшей мере, не разумно. Манипуляции с пятью-десятью объектами более затратны с точки зрения ресурсов, чем аналогичные действия с функциями.
К тому, же в связи с простотой архитектуры движка, в ООП тут просто не вижу смысла.
Данная архитектура движка отработала на ура в одном из исследований, в котором число пользователей, получивших приглашения на участие, было порядка 120-130 тысяч человек, при чём лояльных пользователей — клиентов заказчика, да и сама рассылка была от лица заказчика — при этом было получено порядка 22k результативных интервью, а общее число заходов было около 45k.
Более мелких же исследований было проведено огромное множество.
Статистика говорит, что такая архитектура надёжна и высокопроизводительна. Помимо этого, она ещё и удобна (на мой взгляд) и достаточно гибка.
При наличии достаточного количества свободного времени я, возможно, ещё вернусь к доработке этого движка и, опять же, возможно, выложу все исходники в общий доступ, вместе с подробным описанием функционала и всякими плюшками. Но это всё только «возможно»…
В своё время, когда я только начинал погружаться в данную сферу, недостаток информации по теме был катастрофический и все задачи приходилось решать, зачастую, методом тыка — проб и ошибок.
Надеюсь, что данный мануал и мой личный опыт, который я отразил в нём, чем-то помогли вам.
А ещё я надеюсь, что этот материал как-то восполнит информационный вакуум вокруг такой темы, как программирование онлайн-анкет в маркетинговых исследованиях.
Но, к сожалению, остаются не охваченными такие темы, как программирование квот, конджойнт или программирование анкет и/или движка для CATI (собственно, как и организация технической базы самой CATI-студии). И многое другое…
Но это всё темы для отдельных FAQ и мануалов.
Спасибо за внимание.
Казалось бы, что может быть проще ТЗ в виде плёвой 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 и мануалов.
Спасибо за внимание.