All the parts are here, but your model is taking on too much work. First, let's tackle one of the three hardest things in computer programming: Naming things.
Naming Classes in an MVC Application
You have three classes, called aptly Controller, View and Model. I know you are trying to learn the MVC pattern, but an important aspect or learning this pattern is that names should make sense beyond telling you which layer in MVC they go. A convention for naming things is a must.
The "Rails" way of naming things all starts with the "model". In your case, your Model class contains data about a "quiz", so Quiz
is a perfect name for that class. The controller that handles user interaction for Quiz
objects should be called QuizesController
-- note that "Quiz" is pluralized, because the controller object handles all quizes, not just one.
After that, a QuizView
seems like a good name for the view object.
Really if you think about it, a "quiz" is composed of two other things: quiz questions and quiz answers. We need to properly model a "quiz" before anything else. The Model layer in MVC is the foundation -- the bedrock of your application.
Know Your "Domain:" Properly Modeling Your Quiz
We actually have a need for 3 model classes:
- Quiz
- QuizQuestion
- QuizAnswer
A quiz has one or more questions. An answer requires a question. We need to model this in Ruby.
class Quiz
attr_reader :questions
def initialize(questions)
@questions = questions
@grade = 0
end
def answer_question(question, answer_text)
question = questions[question_number - 1]
question.answer answer_text
end
def grade
return @grade if @grade > 0
number_correct = 0
questions.each do |question|
number_correct += 1 if question.answered_correctly?
end
@grade = calculate_grade number_correct
end
def retake
@grade = 0
questions.each do |question|
question.remove_answer
end
end
def quesion_count
questions.count
end
private
def calculate_grade(number_correct)
number_correct / questions.count
end
end
A Quiz is a tad more complex, but it just needs a list of QuizQuestions in its constructor. All other operations on the quiz are public methods, some of which are delegated to the QuizQuestion class.
class QuizQuestion
attr_reader :text, :answer, :expected_answer
def initialize(text, expected_answer)
@text = text
@expected_answer = expected_answer
end
def answer(answer_text)
@answer = QuizAnswer.new self, answer_text
end
def answered_correctly?
return @answer.nil? ? false : @answer.correct?
end
def remove_answer
@answer = nil
end
end
The QuizQuestion class glues the quiz together with the answer, which we will see in a moment. Again, the constructor doesn't do much, except take in the data it needs in order to exist: The question text and the expected answer. The answer
method creates the QuizAnswer object and returns it. The logic of "retaking" a quiz is also delegated to the remove_answer
method.
class QuizAnswer
attr_reader :question, :text
def initialize(question, text)
@question = question
@text = text
end
def correct?
return question.expected_answer == text
end
def question_text
question.text
end
end
Lastly the QuizAnswer class glues together the quiz question and the user's answer, along with the logic for testing the answer.
Now that we've properly modeled our "domain", the Quiz, QuizQuestion and QuizAnswer classes become our Domain Models combining data with business logic (taking a quiz). Next, we will take a step up from the "foundation" of our application to explore the data access layer, or "Repository".
The Data Access Layer (Repository)
The Repository Pattern decouples the data access from your model and controller. What your controller needs is a QuizRepository
object that does all the data access.
Since you are going with flat file storage, we want to extract this behavior into its own layer.
class QuizRepository
def initialize(filename)
@filename = filename
end
def all
return @quizes unless @quizes.nil?
load
@quizes
end
def find(index)
return nil if index < 0 || index > all.count
return all[index]
end
def reload
@quizes = nil
end
private
def filename
@filename
end
def load
questions = []
index = 0
question_text = ""
expected_answer = ""
file = File.open filename
file.each_line do |line|
if index % 2 == 0
question_text = line
else
expected_answer = line
questions << QuizQuestion.new question_text, expected_answer
end
index += 1
end
@quizes << Quiz.new questions
end
end
The QuizRepository class needs a filename, and it lazy loads the quiz objects only when you need to get them.
Notice that there is no console interaction. This should be encapsulated by the "Controller".
Handling User Interaction and Displaying Information
Knowledge of the console, and what to display is the realm of the controller and views.
class QuizesController
def initialize(quiz_repository)
@quiz_repository = quiz_repository
end
def run
counter = 1
quiz = @quiz_repository.all.first
puts "Welcome to the Quiz!"
puts "--------------------"
quiz.questions.each do |question|
puts QuizQuestionView.new question, counter
answer_text = gets.chomp
answer = quiz.answer_question question, answer_text
puts QuizAnswerView.new quiz, answer
counter += 1
end
end
end
The main loop is inside the controller, since the controller in MVC responds to user input. The controller needs a "quiz repository" which is the only argument in its constructor. You'll also notice there are 2 views: QuizQuestionView and QuizAnswerView.
class QuizQuestionView
def initialize(question, question_number)
@question = question
@question_number = question_number
end
def to_s
"Question \##{@question_number}: #{@question.text}"
end
end
class QuizAnswerView
def initialize(quiz, answer)
@quiz = quiz
@answer = answer
end
def to_s
text = if answer.correct?
"well done"
else
"the correct answer is #{answer.question_text}"
end
text + "\nYour score is now #{@quiz.grade}"
+ "\n--------------------------------------------------------------------------"
end
end
Also notice that there are no calls to gets
or puts
in the "view" classes. These classes are responsible for output only. The fact that you are outputting to a console is the responsibility of the Controller, since the Controller knows you are interacting with the user via the console. The to_s
method is defined, which returns a string that the Controller then prints to the standard output of the console.
Putting All The Layers Together
Now we actually need to run the program:
#!/bin/env ruby
quiz_repository = QuizRepository.new "/data/quiz.txt"
controller = QuizController.new quiz_repository
controller.run
The whole application is started, run and exits in three lines of code.