MVC flash card application

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.

.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.rb

require_relative 'model'

class View
  def initialize(question)
    @question = question
    puts "Question: " + question
  end
end

controller.rb

require_relative 'model'

class Controller
  def initialize(filename)
    a = Model.new(filename)
    a.play
  end
end

model.rb

require_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 << line
    end
  end
  def checkanswer
    @goodanswer = false
    @oneanswer == @useranswer ? @goodanswer = true && @score += 1: @goodanswer = false && score += 0
  end
  def displaypunchline
    if @goodanswer == true
      puts "well done"
    else
      puts "the good answer was #{@oneanswer}"
    end
  end
  def displayscore
    puts "Your score is now: " + @score.to_s
  end
  def display_separation
    puts "--------------------------------------------------------------------------"
  end
  def getuserinput
    @useranswer = gets.chomp
  end
  def crazygameloop
    @counter = 1
    loop do
      break if @counter >= @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
end

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 appreciated.

Answer

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.

Attribution
Source : Link , Question Author : gastngouron , Answer Author : Greg Burghardt

Leave a Comment