Skip to content

Instantly share code, notes, and snippets.

@SamSamskies
Last active May 11, 2017 02:32
Show Gist options
  • Save SamSamskies/6914051 to your computer and use it in GitHub Desktop.
Save SamSamskies/6914051 to your computer and use it in GitHub Desktop.
AR Todo DBC phase 1 challenge overview

The purpose of this document is to go over a couple different solutions for the AR Todos challenge from phase 1. First, I'm going to present a solution where most of the code is done in a single file. Then I'm going to present a solution using MVC and hopefully you'll be able to see the benefits of taking the time to separate your concerns.

Example 1: Non-MVC Solution

Many beginner programmers would probably approach the AR Todos challenge in a manner somewhat like this example if they were not given specific instructions on how to implement the solution. This solution is easier to write because you can just tackle each task systematically without thinking about new classes, how to organize separate files, etc.

What if you made this program for your job and another person needed to add more functionality or fix a bug? Do you think it would be easy for them to understand what you did? They could probably figure it out, but it wouldn't be as easy as it could be. It's important to make your program as clear as possible even if it's just for yourself. You never know when you are going to have to revisit code that you have written in the past.

What if you were doing this project with a team? It's not very easy to separate the tasks to be done when everything is in one file. If you separate the views and controllers, people can easily work on separate tasks that can be later merged together for the final product.

Another thing to consider is what if one day you want to convert this into a web application? The task of converting this into a web app would be much easier if it was implemented with MVC. The main task when converting this into a web app would be swapping out the view because you would be changing the view from command line output to HTML pages. Swapping out the view in this example would be really hard because the view is intertwined with everything. It would be a lot easier if everything related to the view was separated out. I will demonstrate converting this application to a web app using Sinatra after showing you a solution that uses MVC.

Main Application

require_relative 'config/application'

def execute_todo_app
  if ARGV.any?

    case ARGV[0]
      when "list"
        tasks = Task.all
        if tasks.empty?
          puts
          puts "Woohoo no tasks to complete yet!"
          puts
        else
          tasks.each_with_index do |task, i|
            completed = task.completed ? 'x' : ' '
            puts "#{i+1}.".ljust(4) + "[#{completed}] #{task.name}"
          end
        end
      when "add"
        description = ARGV[1..-1].join(' ')
        task = Task.create(name: description)
        if task.valid?
          puts "Appended #{description} to your TODO list..."
        else
          puts "Error: #{task.errors.messages[:name].first}"
        end
      when "delete"
        task = find_task ARGV[1].to_i

        if task
          task = task.destroy
          if task.valid?
            puts "Deleted '#{task.name}' from your TODO list..."
          else
            puts "Error: Something went wrong. Please try again later."
          end
        else
          puts "Error: invalid task ID provided."
        end
      when "complete"
        task = find_task ARGV[1].to_i

        if task
          update_result = task.update_attributes completed: true
          if update_result
            puts "Completed '#{task.name}' from your TODO list..."
          else
            puts "Error: Something went wrong. Please try again later."
          end
        else
          puts "Error: invalid task ID provided."
        end
      when "help"
        display_menu
      else
        puts "Invalid command :/"
        display_menu
    end

  else
    display_menu
  end
end


def display_menu
  puts
  puts "*" * 100
  puts "Usage:"
  puts "ruby todo.rb list \t\t\t\t # List all tasks"
  puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
  puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
  puts "ruby todo.rb complete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
  puts
end

def find_task(task_id)
  tasks = Task.all

  (task_id > tasks.count or task_id < 1) ? nil : tasks[task_id - 1]
end


### Program execution starts here ###

execute_todo_app

Model

class Task < ActiveRecord::Base
  validates_presence_of :name, message: "Task can't be blank."
  validates_uniqueness_of :name, message: 'Task is already on the list.'
end

Example 2: MVC Solution

Now take a look at this example. Everything is separated and can more easily be worked on independently. You could for example delegate someone to make the output more fancy by working on the view code while you work on adding additional features.

Also, components can be swapped out easier with this example. In the next section, I will demonstrate how this could be converted to a webapp using Sinatra by swapping out the view and modifying the controller to work with Sinatra.

Main Application

require_relative 'config/application'

def execute_todo_app
  if ARGV.any?

    case ARGV[0]
      when "list"
        TasksController.list
      when "add"
        TasksController.add ARGV[1..-1].join(' ')
      when "delete"
        TasksController.delete ARGV[1].to_i
      when "complete"
        TasksController.complete ARGV[1].to_i
      when "help"
        TasksController.menu
      else
        TasksController.invalid_command
    end

  else
    TasksController.menu
  end
end


### Program execution starts here ###

execute_todo_app

Controller

class TasksController

  def self.menu
    TasksView.display_menu
  end

  def self.invalid_command
    TasksView.display_invalid_command
    TasksView.display_menu
  end

  def self.list
    TasksView.display_list Task.all
  end

  def self.add(sentence)
    task = Task.create(name: sentence)
    if task.valid?
      TasksView.display_notice "Appended #{sentence} to your TODO list..."
    else
      TasksView.display_notice "Error: #{task.errors.messages[:name].first}"
    end
  end

  # Note this is not the id in the database. This id identifies where on the list the task is.
  def self.delete(task_id)
    task = find_task task_id

    if task
      task = task.destroy
      if task.valid?
        TasksView.display_notice "Deleted '#{task.name}' from your TODO list..."
      else
        TasksView.display_notice "Error: Something went wrong. Please try again later."
      end
    else
      TasksView.display_notice "Error: invalid task ID provided."
    end
  end

  def self.complete(task_id)
    task = find_task task_id

    if task
      update_result = task.update_attributes completed: true
      if update_result
        TasksView.display_notice "Completed '#{task.name}' from your TODO list..."
      else
        TasksView.display_notice "Error: Something went wrong. Please try again later."
      end
    else
      TasksView.display_notice "Error: invalid task ID provided."
    end

  end

  def self.find_task(task_id)
    tasks = Task.all

    (task_id > tasks.count or task_id < 1) ? nil : tasks[task_id - 1]
  end
end

View

class TasksView
  def self.display_menu
    puts
    puts "*" * 100
    puts "Usage:"
    puts "ruby todo.rb list \t\t\t\t # List all tasks"
    puts "ruby todo.rb add TASK \t\t\t\t # Add task to do e.g. ruby todo.rb Buy groceries"
    puts "ruby todo.rb delete NUM_OF_TASK_TO_DELETE \t # Delete a task e.g. ruby todo.rb delete 1"
    puts "ruby todo.rb complete NUM_OF_TASK_TO_DELETE \t # Complete a task e.g. ruby todo.rb complete 1"
    puts
  end

  def self.display_invalid_command
    puts "Invalid command :/"
  end

  def self.display_list(tasks)
    if tasks.empty?
      puts
      puts "Woohoo no tasks to complete yet!"
      puts
    else
      tasks.each_with_index do |task, i|
        completed = task.completed ? 'x' : ' '
        puts "#{i+1}.".ljust(4) + "[#{completed}] #{task.name}"
      end
    end
  end

  def self.display_notice(notice)
    puts notice
  end
end

Model

class Task < ActiveRecord::Base
  validates_presence_of :name, message: "Task can't be blank."
  validates_uniqueness_of :name, message: 'Task is already on the list.'
end

AR Todos Sinatra Web App Example

Using the MVC example I was able to fairly easily convert AR Todos into a web app using Sinatra. I only implemented the list and add functionality because the delete and complete actions are a bit more difficult when using Sinatra and I don't want your brain to explode. You will notice that the controller looks fairly the same despite the the configuration lines that were added at the top. Also, the controller is now the main application file when using Sinatra. As for the view, it previously was a ruby file filled with puts statements in it and now it has been replaced with ERB files (HTML files that allow you to embed Ruby code) . All I did was delete the old ruby file and plop in these new ERB files to change the view from command line to a webpage. The model did not change at all.

Controller (Main Application)

require_relative 'config/application'
require 'sinatra'
require 'rack-flash'

set :root, File.dirname(__FILE__)
set :views, Proc.new { File.join(root, "app/views") }

enable :sessions
use Rack::Flash

after do
  ActiveRecord::Base.connection.close
end


get '/' do
  @tasks = Task.all
  erb :index
end

get '/add' do
  erb :add
end

post '/add' do
  task = Task.create(name: params[:name])
    if task.valid?
      flash[:notice] = "Appended '#{params[:name]}' to your TODO list..."
    else
      flash[:notice] = "Error: #{task.errors.messages[:name].first}"
    end
  redirect '/'
end

# delete and complete are a bit more difficult, so I'll save those for another day. Feel free to attempt those as a challenge. http://net.tutsplus.com/tutorials/ruby/singing-with-sinatra-the-recall-app-2/ <= Check out that tutorial for info on how to implement the delete and complete actions.

Views

index.erb

<% if flash[:notice] %>
<p>*** <%= flash[:notice] %> ***</p>
<% end %>

<h1>AR Todos Sinatra Example</h1>
<a href='/add'>Add A task</a>
<ol>
<% @tasks.each do |task| %>
  <li><%= task.name %></li>
<% end %>
</ol>

add.erb

<h1>Add a Task</h1>

<form action='/add' method='post'>
  <input type='text' name='name'>
  <input type='submit'>
</form>

Wrap Up

I hope this document helped you understand MVC and the whole idea of separating your concerns better. Please feel free to email me at [email protected] if you have any questions, would like a code review, want to pair with me on refactoring your code, etc.

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment