Functional Scope (In-Scope)
- Dynamic Form Schemas: Create surveys composed of diverse question types (Choice, Text, Scale).
- Skip-Logic Rule Routing: Define skip rules (e.g., if Q1 is 'No', skip to Q5) to navigate forms dynamically.
- Strict Path Validation: Verify that submitted survey responses follow a valid logical path on form completion.
- Analytics Aggregation: Aggregate answers by question type (choice percentages, mean rating scores) using the Visitor Design Pattern.
Explicit Boundaries (Out-of-Scope)
- No Third-Party Email Dispatchers: Excludes direct integration with Mailchimp or transactional SMTP client configurations.
- No Machine Learning Sentiment Extractors: Bypasses automatic NLP sentiment analysis for text-based answers.
Clean reference designs demonstrating dynamic skip logic in Java and Python:
// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.*;
enum QuestionType { CHOICE, SCALE, TEXT }
// Visitor Pattern for dynamic analytics aggregation
interface QuestionVisitor {
void visit(ChoiceQuestion q);
void visit(ScaleQuestion q);
void visit(TextQuestion q);
}
abstract class Question {
private final String id;
private final String text;
private final QuestionType type;
public Question(String id, String text, QuestionType type) {
this.id = id;
this.text = text;
this.type = type;
}
public String getId() { return id; }
public String getText() { return text; }
public QuestionType getType() { return type; }
public abstract boolean validateResponse(Object answer);
public abstract void accept(QuestionVisitor visitor);
}
class ChoiceQuestion extends Question {
private final List<String> options;
public ChoiceQuestion(String id, String text, List<String> options) {
super(id, text, QuestionType.CHOICE);
this.options = new ArrayList<>(options);
}
public List<String> getOptions() { return options; }
@Override
public boolean validateResponse(Object answer) {
if (!(answer instanceof String)) return false;
return options.contains(answer);
}
@Override
public void accept(QuestionVisitor visitor) {
visitor.visit(this);
}
}
class ScaleQuestion extends Question {
private final int minVal;
private final int maxVal;
public ScaleQuestion(String id, String text, int minVal, int maxVal) {
super(id, text, QuestionType.SCALE);
this.minVal = minVal;
this.maxVal = maxVal;
}
public int getMinVal() { return minVal; }
public int getMaxVal() { return maxVal; }
@Override
public boolean validateResponse(Object answer) {
if (!(answer instanceof Integer)) return false;
int val = (Integer) answer;
return val >= minVal && val <= maxVal;
}
@Override
public void accept(QuestionVisitor visitor) {
visitor.visit(this);
}
}
class TextQuestion extends Question {
public TextQuestion(String id, String text) {
super(id, text, QuestionType.TEXT);
}
@Override
public boolean validateResponse(Object answer) {
if (!(answer instanceof String)) return false;
return !((String) answer).trim().isEmpty();
}
@Override
public void accept(QuestionVisitor visitor) {
visitor.visit(this);
}
}
class SkipRule {
private final String expectedAnswer;
private final String jumpToQuestionId;
public SkipRule(String expectedAnswer, String jumpToQuestionId) {
this.expectedAnswer = expectedAnswer;
this.jumpToQuestionId = jumpToQuestionId;
}
public String getExpectedAnswer() { return expectedAnswer; }
public String getJumpToQuestionId() { return jumpToQuestionId; }
}
class SurveyResponse {
private final String responseId;
private final String surveyId;
private final Map<String, Object> answers = new ConcurrentHashMap<>();
public SurveyResponse(String responseId, String surveyId) {
this.responseId = responseId;
this.surveyId = surveyId;
}
public String getResponseId() { return responseId; }
public String getSurveyId() { return surveyId; }
public void putAnswer(String qId, Object answer) { answers.put(qId, answer); }
public Object getAnswer(String qId) { return answers.get(qId); }
public Map<String, Object> getAnswers() { return answers; }
}
class Survey {
private final String id;
private final List<Question> questions = new CopyOnWriteArrayList<>();
private final Map<String, List<SkipRule>> skipRules = new ConcurrentHashMap<>();
public Survey(String id) {
this.id = id;
}
public String getId() { return id; }
public void addQuestion(Question q) {
questions.add(q);
}
public void addSkipRule(String questionId, String answer, String jumpToId) {
skipRules.computeIfAbsent(questionId, k -> new CopyOnWriteArrayList<>())
.add(new SkipRule(answer, jumpToId));
}
public Question getQuestion(String qId) {
for (Question q : questions) {
if (q.getId().equals(qId)) return q;
}
return null;
}
public String getNextQuestionId(String currentId, Object answer) {
// Evaluate skip-rules
List<SkipRule> rules = skipRules.get(currentId);
if (rules != null && answer != null) {
for (SkipRule r : rules) {
if (r.getExpectedAnswer().equalsIgnoreCase(answer.toString())) {
return r.getJumpToQuestionId();
}
}
}
// Sequential fallback
for (int i = 0; i < questions.size() - 1; i++) {
if (questions.get(i).getId().equals(currentId)) {
return questions.get(i + 1).getId();
}
}
return null; // Terminate survey
}
public List<Question> getQuestions() { return questions; }
}
class SurveyResponseValidator {
public boolean validate(Survey survey, SurveyResponse response) {
if (survey.getQuestions().isEmpty()) return true;
String currentId = survey.getQuestions().get(0).getId();
Set<String> expectedPath = new HashSet<>();
while (currentId != null) {
expectedPath.add(currentId);
Question q = survey.getQuestion(currentId);
Object ans = response.getAnswer(currentId);
if (ans == null) {
System.out.println("Validation fail: Missing answer on active logical path for question " + currentId);
return false;
}
if (!q.validateResponse(ans)) {
System.out.println("Validation fail: Invalid format value for question " + currentId + ": " + ans);
return false;
}
currentId = survey.getNextQuestionId(currentId, ans);
}
// Check if any extra answers exist that were supposed to be skipped
for (String qId : response.getAnswers().keySet()) {
if (!expectedPath.contains(qId)) {
System.out.println("Validation warning: Extra answer registered for skipped question " + qId);
return false;
}
}
return true;
}
}
// Analytical Visitor implementation
class AnalyticsAggregator implements QuestionVisitor {
private final List<SurveyResponse> responses;
public AnalyticsAggregator(List<SurveyResponse> responses) {
this.responses = responses;
}
@Override
public void visit(ChoiceQuestion q) {
Map<String, Integer> counts = new HashMap<>();
for (SurveyResponse res : responses) {
Object ans = res.getAnswer(q.getId());
if (ans != null) {
counts.put(ans.toString(), counts.getOrDefault(ans.toString(), 0) + 1);
}
}
System.out.println("Choice Question [" + q.getText() + "] Results: " + counts);
}
@Override
public void visit(ScaleQuestion q) {
double total = 0.0;
int count = 0;
for (SurveyResponse res : responses) {
Object ans = res.getAnswer(q.getId());
if (ans instanceof Integer) {
total += (Integer) ans;
count++;
}
}
double avg = count == 0 ? 0.0 : total / count;
System.out.println("Scale Question [" + q.getText() + "] Average Score: " + String.format("%.2f", avg));
}
@Override
public void visit(TextQuestion q) {
List<String> entries = new ArrayList<>();
for (SurveyResponse res : responses) {
Object ans = res.getAnswer(q.getId());
if (ans != null) {
entries.add(ans.toString());
}
}
System.out.println("Text Question [" + q.getText() + "] Responses: " + entries);
}
}
// Complete Simulation
public class Main {
public static void main(String[] args) {
Survey survey = new Survey("S-101");
// Add Questions
survey.addQuestion(new ChoiceQuestion("Q-1", "Do you own a car?", Arrays.asList("Yes", "No")));
survey.addQuestion(new TextQuestion("Q-2", "What brand of car is it?"));
survey.addQuestion(new ScaleQuestion("Q-3", "Rate your car satisfaction", 1, 5));
// Skip logic: If Q-1 answer is "No", jump straight to Q-3 (skip Q-2)
survey.addSkipRule("Q-1", "No", "Q-3");
// 1. Create a valid "Yes" path response
SurveyResponse res1 = new SurveyResponse("R-1", "S-101");
res1.putAnswer("Q-1", "Yes");
res1.putAnswer("Q-2", "Tesla Model 3");
res1.putAnswer("Q-3", 5);
// 2. Create a valid "No" path response (skipped Q-2)
SurveyResponse res2 = new SurveyResponse("R-2", "S-101");
res2.putAnswer("Q-1", "No");
res2.putAnswer("Q-3", 4);
// 3. Create an invalid response (submitted Q-2 answer but chose "No")
SurveyResponse res3 = new SurveyResponse("R-3", "S-101");
res3.putAnswer("Q-1", "No");
res3.putAnswer("Q-2", "Toyota");
res3.putAnswer("Q-3", 3);
SurveyResponseValidator validator = new SurveyResponseValidator();
System.out.println("Is response 1 valid? " + validator.validate(survey, res1));
System.out.println("Is response 2 valid? " + validator.validate(survey, res2));
System.out.println("Is response 3 valid? " + validator.validate(survey, res3));
// 4. Run visitor analytics
System.out.println("\n--- Aggregating Analytics ---");
List<SurveyResponse> allResponses = Arrays.asList(res1, res2);
AnalyticsAggregator aggregator = new AnalyticsAggregator(allResponses);
for (Question q : survey.getQuestions()) {
q.accept(aggregator);
}
}
}