Upload
andre-foeken
View
492
Download
0
Embed Size (px)
Citation preview
RUBY ON RAILS
Ruby vs Java
Ruby = Slower
Java = Verbose
Ruby == JavaWhat's the same?
Garbage collected
Strongly typed objects
public, private, and protected
Ruby != JavaWhat's different?
No compilation
begin...end vs {}
require instead of import
Parentheses optional
in method calls
Everything is an object
No types, no casting, no static type checking
Foo.new vs new Foo()
nil == null
under_score vs CamelCase
No method overloading
Mixins vs interfaces
Example time
Classes
class Person attr_accessor :name
def initialize(name) @name = name end
def say(message="default message") puts "#{name}: #{message}" unless message.blank? endend
> andre = Person.new("André")> andre.say("Hello!")=> André: Hello!
Inheritance
class RudePerson < Person def shout(message="default message") say(message.upcase) endend
Mixins
module RudeBehaviour def shout(message="default message") say(message.upcase) endend
class Person include RudeBehaviour # ...end
Readability nazi'sUsually 5+ ways to do the same thing, so it looks nice.
def repeat(message="default message", count=1) # say message count times.end
def repeat(message="default message", count=1) loop do say(message) count -= 1 break if count == 0 endend
def repeat(message="default message", count=1) while(count > 0) say(message) count -= 1 endend
def repeat(message="default message", count=1) until(count == 0) say(message) count -= 1 endend
def repeat(message="default message", count=1) (0...count).each do say(message) endend
def repeat(message="default message", count=1) for index in (0...count) say(message) endend
def repeat(message="default message", count=1) count.times do say(message) endend
Meta-programmingChanging the code on the fly
> Person.new.respond_to?(:shout)=> false> Person.include(RudeBehaviour)=> Person> Person.new.respond_to?(:shout)=> true
# Add the 'shout' method to each object that has a 'say' method.> Object.subclasses.each{ |o| o.include(RudeBehaviour) if o.respond_to?(:say) }
We can manipulate code
class Person ["hello", "goodbye", "howdie"].each do |word| define_method("say_#{word}") do say("#{name}: #{word}") end endend
> Person.new("André").say_hello=> André: hello
Who even needs methods?The power of method_missing
class Person def method_missing(method_name, *arguments) if method_name.starts_with?("say_") # Say everything after 'say_' say(method_name[4..-1]) else super end endend
Your turn
Animal kingdomBuild a Fish, Chicken, and Platypus
4 Each animal makes a noise (blub, tock, gnarl)
4 All animals have health
4 Certain animals have a beak (chicken, platypus, NOT fish)
4 Animals with a beak can peck other animals (health--)
4 Certain animals can lay eggs (fish, chicken, NOT platypus)
module RudeBehaviour def shout(message="default message") say(message.upcase) endend
class Person include RudeBehaviour
attr_accessor :name
def initialize(name) self.name = name end
def say(message="default message") puts "#{name}: #{message}" unless message.blank? endend
class Animal attr_accessor :health
def initialize self.health = 100 end
def make_noise noise puts noise endend
module Beaked def peck(animal) animal.health -= 1 endend
class Egg ; end
module EggLayer def lay_egg Egg.new endend
class Chicken < Animal include Beaked, EggLayer def make_noise super("tock") endend
RailsA web framework
DRY & MVCConvention over configuration
[EXPLAIN MVC GRAPH]
GET http://localhost:3000/people
Routing# config/routes.rbMyApp::Application.routes.draw do get "/people", to: "people#index"end
The Controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController def index endend
The view# app/views/people/index.html<p>Hello world!</p>
This is a bit plain...Let's add some dynamic data
The Controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController def index @people = ["André", "Pieter", "Matthijs"] endend
The view (ERB)# app/views/people/index.html.erb<ul><% @people.each do |person| %> <li><%= person %></li><% end %></ul>
The view (SLIM)# app/views/people/index.html.slimul - @people.each do |person| li= person
What about a database?The default is SQLLite
The model# app/models/person.rbclass Person < ActiveRecord::Baseend
Migrations# db/migrations/00000000_create_people.rbclass CreatePeople < ActiveRecord::Migration def change create_table :people do |t| t.string :name end endend
rails g migration create_people name:string
Wait!!!?How does it know people belongs to the person model?
Convention over configuration
Tables are plural, models are singular
> rake db:create db:migrate
The Controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController def index @people = Person.all endend
The view (SLIM)# app/views/people/index.html.slimul - @people.each do |person| li= person.name
Convention over configuration
All columns are mapped to methods
Okay, lets add some people.
> Person.new(name: "André").save> Person.new(name: "Pieter").save> Person.new(name: "Matthijs").save
> Person.count=> 3
Validations> Person.new.save=> true
The model# app/models/person.rbclass Person < ActiveRecord::Base validates :name, presence: trueend
Validations> p = Person.new
> p.save=> false
> p.errors.messages=> {:name=>["can't be blank"]}
Let's add a formThis will introduce two new 'actions'
NEW & CREATEthe form, and the creation
Routing# config/routes.rbMyApp::Application.routes.draw do get "/people", to: "people#index" get "/people/new", to: "people#new" post "/people", to: "people#create"end
Routing# config/routes.rbMyApp::Application.routes.draw do resources :people, only: [:index, :new, :create]end
Convention over configuration
index, show, new, edit, create, update, destroy
The Controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController def index @people = Person.all end
def new @person = Person.new endend
The view (ERB)# app/views/people/new.html.erb<%= form_for @person do |f| %> <div> <%= f.text_field :name %> </div> <div> <%= f.submit "Save" %> </div><% end %>
The view (SLIM)# app/views/people/new.html.slim= form_for @person do |f| div= f.text_field :name div= f.submit "Save"
From now on, we will continue in SLIM, but ERB is just as good.
The Controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController # def index ... # def new ...
def create @person = Person.new(person_attributes) if @person.save redirect_to action: :index else render :new end end
private
def person_attributes params.require(:person).permit(:name) endend
The view (SLIM)# app/views/people/new.html.slim- @person.errors.full_messages.each do |error| div.red= error
= form_for @person do |f| div= f.text_field :name div= f.submit "Save"
Finally, destroying stuff.
Routing# config/routes.rbMyApp::Application.routes.draw do resources :people, only: [:index, :new, :create, :destroy]end
The view (SLIM)# app/views/people/index.html.slimul - @people.each do |person| li= link_to(person.name, person_path(person), method: :delete)
NOTE: the method is the HTTP method, not the controller method.
Path helpers> rake routesPrefix Verb URI Pattern Controller#Action-------------------------------------------------------------people GET /people people#indexperson GET /people/:id people#shownew_person GET /people/new people#newedit_person GET /people/:id/edit people#edit POST /people people#create PATCH /people/:id people#update DELETE /people/:id people#destroy
The Controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController # def index ... # def new ... # def create ...
def destroy Person.find(params[:id]).destroy redirect_to action: :index endend
Example time
Installing ruby (OSX)> brew install rbenv ruby-build
Then add eval "$(rbenv init -)" to your .profile
> rbenv install 2.2.3
Linux: https://github.com/sstephenson/rbenv
Installing Rails> gem install rails
Making your app> rails new [my_app]
Structure- app - models - views - controllers- config- db- Gemfile / Gemfile.lock
Dependencies# Gemfilegem 'slim-rails'
Add this line to your Gemfile to use slim, then install them:
> bundle install
Running your app> rails s
Build a small app (45 mins)Use the pdf for reference
Your app should:
4 be able to add items
4 be able to edit items
4 be able to destroy items
4 be able to show a single item
4 be able to show a list of items
Next up: relationshas_many, belongs_to, has_one, has_many_through
The model# app/models/person.rbclass Person < ActiveRecord::Base validates :name, presence: true has_many :skillsend
# app/models/skill.rbclass Skill < ActiveRecord::Base belongs_to :personend
Migrations# db/migrations/00000000_create_skills.rbclass CreateSkills < ActiveRecord::Migration def change create_table :skills do |t| t.string :name t.references :person end endend
But this binds the skill to a single person...
We need a link tableRails forces you to name it properly!
# app/models/proficiency.rbclass Proficiency < ActiveRecord::Base belongs_to :person belongs_to :skillend
Migrations# db/migrations/00000000_create_proficiencies.rbclass CreateProficiencies < ActiveRecord::Migration def change create_table :proficiencies do |t| t.references :person t.references :skill end endend
Migrations# db/migrations/00000000_remove_person_reference_from_skills.rbclass RemovePersonReferenceFromSkills < ActiveRecord::Migration def change remove_reference :skills, :person endend
The model# app/models/person.rbclass Person < ActiveRecord::Base validates :name, presence: true
has_many :proficiencies has_many :skills, through: :proficienciesend
# app/models/skill.rbclass Skill < ActiveRecord::Base has_many :proficiencies has_many :people, through: :proficienciesend
# app/models/proficiency.rbclass Proficiency < ActiveRecord::Base belongs_to :person belongs_to :skillend
> andre = Person.new(name: "André")> andre.skills << Skill.create(name: "Knitting")> andre.save
Update your app (30 mins)Use the pdf for reference
Your app should:
4 have a relationship through a link table
4 have the forms to create/update/destroy the related items (in our example: Skills)
4 should NOT be able to build the relationship using a form (yet).
Building nested formsThough usually it can be prevented by making the link table a first-class citizen.
The view# app/views/people/new.html.slim= form_for @person do |f| div= f.text_field :name = f.fields_for :proficiencies, @person.proficiencies.build do |g| div= g.collection_select :skill_id, Skill.all, :id, :name div= f.submit "Save"
The controller# app/controllers/people_controller.rbclass PeopleController < ApplicationController # def index ... # def new ... # def create ...
private
def person_attributes params.require(:person).permit(:name, proficiencies_attributes: [:skill_id]) endend
The model# app/models/person.rbclass Person < ActiveRecord::Base validates :name, presence: true
has_many :proficiencies has_many :skills, through: :proficiencies
accepts_nested_attributes_for :proficienciesend
Update your app (30 mins)Use the pdf for reference
Your app should:
4 should be able to build the relationship using a form.
4 you can pick: nested or first-class
TestingPick your poison: rspec, test-unit,
# test/models/person_test.rbrequire 'test_helper'
class PersonTest < ActiveSupport::TestCase test "Person has a name, that is required" do assert !Person.new.valid? assert Person.new(name: "André").valid? endend
Run your tests
> rake test
# test/integration/people_get_test.rbrequire 'test_helper'
class PeopleGetTest < ActionDispatch::IntegrationTest test "that the index shows a list of people" do # Build three people names = ["André", "Matthijs", "Pieter"] names.each{ |name| Person.create(name: name) }
get people_path assert_response :success
assert_select "li", "André" assert_select "li", "Matthijs" assert_select "li", "Pieter" endend
Update your app (15 mins)Use the pdf for reference
Your app should:
4 Test your validations, and relationships.
4 Test a few basic forms
Building API'sMaking a JSON API for your models
Routing# config/routes.rbMyApp::Application.routes.draw do resources :people namespace :api do resources :people endend
The controller# app/controllers/api/people_controller.rbclass Api::PeopleController < ApplicationController def index render json: Person.all endend
The response[ { id: 1, name: "André" }, { id: 2, name: "Pieter" }, { id: 3, name: "Matthijs" } ]
Adapting the JSONMultiple ways to achieve the same thing.
The model# app/models/person.rbclass Person < ActiveRecord::Base def as_json options={} { name: name } endend
But this changes it everywhere
JSON is also a view
The controller# app/controllers/api/people_controller.rbclass Api::PeopleController < ApplicationController def index @people = Person.all endend
The view# app/views/api/people/index.json.jbuilderjson.array! @people, :name
This means we can even merge both controllers
just drop the jbuilder view in the original views
the view will be selected using the request format
Update your app (45 mins)Use the pdf for reference
Your app should:
4 Have a view JSON api
4 Test the API
Q & ASpecific questions go here
EXTRA: Update your app (60 mins)
Use the pdf for reference
Your app should:
4 Do something you want it too
4 We will help.