patternrubyMinor
MVC flash card application
Viewed 0 times
flashcardmvcapplication
Problem
I am trying to learn how to build a very basic MVC structure before jumping on rails and other frameworks. I am working from a .txt file, and I organized a project into four parts: a model, a view, a controller, and a runner.
and so on.
So far it's working but it's un-DRY, not very readable and probably a bad/incomplete MVC implementation. I'm looking for advice on how I should re-organize my model in order to increase my view responsibilities and optimize my controller in case I would add other features. Any input apprec
.txt file pattern:line1 -> "question"
line2 -> "answer"
line3 -> "" #blank line
line4 -> "question"
line5 -> "answer"
line6 -> ""and so on.
runner.rb:require_relative 'view'
require_relative 'model'
require_relative 'controller'
file = 'flashcard_samples.txt'
go = Controller.new(file)view.rbrequire_relative 'model'
class View
def initialize(question)
@question = question
puts "Question: " + question
end
endcontroller.rbrequire_relative 'model'
class Controller
def initialize(filename)
a = Model.new(filename)
a.play
end
endmodel.rbrequire_relative 'view'
class Model
attr_reader :data, :questions, :answers, :punchline
def initialize(filename)
@data = []
@questions = []
@answers = []
@score = 0
@file = File.open(filename)
end
def play
welcome_message
parse
startquestions
end
def parse
@file.each_line do |line|
@data = @data.length - 1
@questions = @data.unshift[@counter-1].tr("\n","")
@oneanswer = @data.unshift[@counter].tr("\n","")
@onequestion = View.new(@questions)
@data.unshift[@counter+1]
@counter += 3
getuserinput
checkanswer
displaypunchline
displayscore
display_separation
end
end
def startquestions
crazygameloop
end
def welcome_message
puts "welcome in my shitty game"
puts "|||||||||||||||||||||||||"
end
endSo far it's working but it's un-DRY, not very readable and probably a bad/incomplete MVC implementation. I'm looking for advice on how I should re-organize my model in order to increase my view responsibilities and optimize my controller in case I would add other features. Any input apprec
Solution
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
After that, a
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:
A quiz has one or more questions. An answer requires a question. We need to model this in Ruby.
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.
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
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
Since you are going with flat file storage, we want to extract this behavior into its own layer.
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
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
endA 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
endThe 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
endLastly 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 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
endThe 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
Code Snippets
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
endclass 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
endclass 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
endclass 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
endclass 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
endContext
StackExchange Code Review Q#111508, answer score: 3
Revisions (0)
No revisions yet.