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.
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.
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
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
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.
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
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
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
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
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.
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.
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>
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.
- Final solution for command line AR Todos using MVC: https://github.com/SamSamskies/ar-todo.
- Write up on the approach I took to creating the command line AR Todos using MVC solution: https://gist.github.com/SamSamskies/6832950
- Final solution for AR Todos Sinatra app: https://github.com/SamSamskies/ar-todo-sinatra
- Sinatra tutorial: http://net.tutsplus.com/sessions/singing-with-sinatra/
- Sinatra docs: http://www.sinatrarb.com/documentation.html