timehunter / branchingassessment
:description
Requires
Requires (Dev)
- mockery/mockery: ^1.1
- orchestra/testbench: ~3.0
- php-coveralls/php-coveralls: ^2.1
- phpunit/phpunit: ~7.0
- sempro/phpunit-pretty-print: ^1.0
This package is auto-updated.
Last update: 2024-10-12 11:33:54 UTC
README
Description
The solution is written on Laravel framework.
Requirement
- Laravel >=5.5
- PHP >= 7.0
Installation
- This is a Laravel composer package, you need to have a Laravel installed first.
- Via Composer
$ composer require timehunter/branchingassessment
- publish assessment config
$ php artisan vendor:publish --provider="TimeHunter\BranchingAssessment\Providers\BranchingAssessmentServiceProvider"
It will publish a branchingassessment.php config file under config directory, this will be used for you to simulate and play around with the package.
Usage
// client side $service = BranchingAssessmentFactory::createAssessmentByArray($data); $service->getNextQuestionId() // first time get fist question $service->setQuestionResponse($questionId, $isCorrect); // AssessmentProcessor factory $service = BranchingAssessmentFactory::createAssessmentByArray($data); // create service entry // Rule factory RuleFactory::process(new AssessmentProcessor($json)) // rule processor
Design Keywords
- OOP: polymorphism
- OOP: inheritance
- OOP: encapsulation
- Strategy design pattern
- Iterator design pattern
- Factory design pattern
Above design patterns might be slightly improved according to the assessment's requirement.
Commands Simulation
- Remember to publish the config file
- Go to branchingassessment.php config file, you can define your own assessment structure, by default, the structure follows the given diagram from the assessment.
- Run the following command to simulate the assessment
$ php artisan assessment:simulate
or run the following command to manually play the assessment:
$ php artisan assessment:start
Suggestions
I highly suggest you to run the two commands to play with the system first before diving into the code.
Solution
Designs
The JSON sample object from the assessment example:
{
"assessment_id":"1",
"questions":[
{
"question_id":"A",
"rule":{
"type":"simple_skip_rule",
"correct":"C",
"incorrect":"B"
}
},
{
"question_id":"C",
"rule":{
"type":"simple_skip_rule",
"correct":"E",
"incorrect":"F"
}
},
{
"question_id":"B",
"rule":{
"type":"simple_skip_rule",
"correct":"D",
"incorrect":"D"
}
},
{
"question_id":"D",
"rule":{
"type":"simple_skip_rule",
"correct":"C",
"incorrect":"C"
}
},
{
"question_id":"E",
"rule":{
"type":"simple_skip_rule",
"correct":"G",
"incorrect":"G"
}
},
{
"question_id":"F",
"rule":{
"type":"simple_skip_rule",
"correct":"H",
"incorrect":"H"
}
},
{
"question_id":"H",
"rule":{
"type":"simple_skip_rule",
"correct":"G",
"incorrect":null
}
},
{
"question_id":"G",
"rule":{
"type":"simple_skip_rule",
"correct":null,
"incorrect":null
}
}
]
}
Rule
Currently, the solution contains two different type of rules as below: (its pretty easy to add a new rule without modifying any core code.)
Simple Skip rule:
{
....
'question_id': 'C',
'rule' :{
'type' : 'simple_skip_rule',
'correct' => 'A',
'incorrect' => 'B'
}
....
}
Explanation: If the answer is correct, it goes to A, if the answer is incorrect, it goes to B.
Score Check rule:
{
....
'question_id': 'A',
'rule' :{
'type' : 'score_check_rule',
'threshold' : 2,
'next': 'E',
'default': 'F'
}
....
}
Explanation: If the current score is 2 (include the result of question A), and the score >= 2, it goes to E else goes to F.
Strategy Pattern - Rule Design
The rule design follows Strategy pattern (OOP: polymorphism), which means all the core logic has been encapsulated in AssessmentProcessor, and what you do is just to create a new strategy by extending AbstractRule class (the class implements RuleInterface).
This ensures any skip/branching logic can be freely added to the logic without touching any core code.
Factory Pattern - Rule creations and AssessmentProcessor creations
- AssessmentProcessorFactory is used to create AssessmentProcessor based on different constructor parameters.
- RuleFactory is used to create different rule strategy by passing the AssessmentProcessor class.
Back-end Logic
- The first step is to parse the json to be object-based model. There are three models:
- Assessment: This stores assessment details e.g. assessmentId
- Question: This stores question details.
- QuestionMap: This is a map collection which formats the raw data to be a basic hash map by using question_id as key. (I used Laravel Collection as a helper function.)
- The second step is to create an iterator which will iterate through the question list by calling getNextQuestionId. I implemented iterator inside of AssessmentProcessor class. The class implements the given interface and also keeps a state of current question ID.
Iterator example:
$service = BranchingAssessmentFactory::createAssessmentByArray($data); while ($currentQuestionId = $service->getNextQuestionId()) { $answer = rand(1, 0) === 1; $service->setQuestionResponse($currentQuestionId, $answer); }
- The third step is to check current question's rule by using RuleFactory to create a rule processor in which will return a next question ID.
Testing
- use PHPUnit to cover all essential classes and cases.
- use travis-ci for CI/CD:https://travis-ci.org/RyanDaDeng/branching-assessment
- tests file located at package tests file
- test coverage is using coveralls: https://coveralls.io/github/RyanDaDeng/branching-assessment?branch=master
Reflections & Conclusion
I think the solution is a bit overkill if you think there are too many classes for the question. In reality, the alternative solution could be fairly simple by manipulating array directly,however, it would encounter a lot of if-else statements which against SOLID principles. My solution is fully based on OOP idea and following SOLID principles as much as possible.
Note: The question is a bit abstract, in real practice, I believe there can be different type of questions and the branching logic would be far more complicated.