Upgrade to Pro — share decks privately, control downloads, hide ads and more …

In Relentless Pursuit of Rest

In Relentless Pursuit of Rest

"That's not very RESTful." As a Rails developer you've probably heard or even spoken that proclamation before, but what does it really mean? What's so great about being RESTful anyway?

RESTful architecture can narrow the responsibilities of your Rails controllers and make follow-on refactorings more natural. In this talk, you'll learn to refactor code to follow RESTful principles and to identify the positive impact those changes have throughout your application stack.

Derek Prior

April 26, 2017
Tweet

More Decks by Derek Prior

Other Decks in Technology

Transcript

  1. In Relentless Pursuit of
    REST

    View Slide

  2. Derek Prior
    @derekprior

    View Slide

  3. What is REST?

    View Slide

  4. That's not very RESTful

    View Slide

  5. HTTP

    View Slide

  6. Hypermedia

    View Slide

  7. HATEOAS

    View Slide

  8. It Doesn't Matter

    View Slide

  9. What Does
    Matter?

    View Slide

  10. Nouns

    View Slide

  11. Verbs

    View Slide

  12. Resources
    & Actions

    View Slide

  13. More
    Small
    Things

    View Slide

  14. View Slide

  15. Custom Actions

    View Slide

  16. View Slide

  17. resource :user, only: [:edit, :update] do
    get :edit_password, on: :member
    put :update_password, on: :member
    end

    View Slide

  18. Longhand Definitions
    get '/user/edit_password',
    to: 'users#edit_password'
    put '/user/update_password',
    to: 'users#update_password'

    View Slide

  19. Controller Mappings

    View Slide

  20. Controller Mappings
    • users#edit_password

    View Slide

  21. Controller Mappings
    • users#edit_password
    • users#update_password

    View Slide

  22. Controller Mappings
    • users#edit_password
    • users#update_password
    • noun#verb_noun

    View Slide

  23. Controller Mappings

    View Slide

  24. Controller Mappings
    • passwords#edit

    View Slide

  25. Controller Mappings
    • passwords#edit
    • passwords#update

    View Slide

  26. Controller Mappings
    • passwords#edit
    • passwords#update
    • noun#verb

    View Slide

  27. Controller Mappings
    Before After
    users#edit_password passwords#edit
    users#update_password passwords#update

    View Slide

  28. resource :user, only: [:edit, :update] do
    resource :password, only: [:edit, :update]
    end

    View Slide

  29. class PasswordsController < ApplicationController
    def edit
    # what used to be `users#edit_password`
    end
    def update
    # what used to be `users#update_password`
    end
    end

    View Slide

  30. View Slide

  31. That's not worth
    another controller!

    View Slide

  32. Password
    isn't a model!

    View Slide

  33. Controllers do not need
    to map to ActiveRecord objects

    View Slide

  34. Complex Actions

    View Slide

  35. No one sets out
    to write complex
    controllers

    View Slide

  36. def update
    @photo = current_user.photos.find(params[:id])
    if @photo.update(update_photo_params)
    redirect_to @photo
    else
    render :edit
    end
    end
    private
    def update_photo_params
    params.require(:photo).permit(:caption)
    end

    View Slide

  37. As a user, I want to mark a photo as featured so it is
    displayed prominently on my profile.

    View Slide

  38. As a user, I want to mark a photo as featured so it is
    displayed prominently on my profile.
    • Oh, and I should only be allowed to have one featured
    photo

    View Slide

  39. As a user, I want to mark a photo as featured so it is
    displayed prominently on my profile.
    • Oh, and I should only be allowed to have one featured
    photo
    • Oh, and if I un-feature a photo, my first photo should
    automatically be featured

    View Slide

  40. def update
    @photo = current_user.photos.find(params[:id])
    @photo.assign_attributes(update_photo_params)
    if @photo.valid? && update_photo(photo)
    redirect_to @photo
    else
    render :edit
    end
    end
    private
    def update_photo(photo)
    ApplicationRecord.transaction do
    if photo.featured_changed? && photo.featured?
    current_user.photos.featured.update!(featured: false)
    elsif photo.featured_changed? && !photo.featured?
    current_user.photos.first.update!(featured: true)
    end
    photo.save!
    end
    end

    View Slide

  41. def update_photo(photo)
    ApplicationRecord.transaction do
    if photo.featured_changed? && photo.featured?
    current_user.photos.featured.update!(featured: false)
    elsif photo.featured_changed? && !photo.featured?
    current_user.photos.first.update!(featured: true)
    end
    photo.save!
    end
    end

    View Slide

  42. def update
    @photo = current_user.photos.find(params[:id])
    @photo.assign_attributes(update_photo_params)
    if @photo.valid? && update_photo(photo)
    redirect_to @photo
    else
    render :edit
    end
    end
    private
    def update_photo(photo)
    ApplicationRecord.transaction do
    if photo.featured_changed? && photo.featured?
    current_user.photos.featured.update!(featured: false)
    elsif photo.featured_changed? && !photo.featured?
    current_user.photos.first.update!(featured: true)
    end
    photo.save!
    end
    end

    View Slide

  43. Differing
    Perspectives

    View Slide

  44. resources :photos, only: [:new, :edit, :create, :update] do
    resource :featured_flag, only: [:create, :destroy]
    end

    View Slide

  45. class FeaturedFlagsController < ApplicationController
    def create
    @photo = current_user.photos.find(params[:photo_id])
    if AddFeaturedFlag.to(@photo)
    redirect_to photos_path
    else
    redirect_to photos_path, error: t('.error')
    end
    end
    def destroy
    @photo = current_user.photos.find(params[:photo_id])
    if RemoveFeaturedFlag.from(@photo)
    redirect_to photos_path
    else
    redirect_to photos_path, error: t('.error')
    end
    end
    end

    View Slide

  46. def create
    @photo = current_user.photos.find(params[:photo_id])
    if AddFeaturedFlag.to(@photo)
    redirect_to photos_path
    else
    redirect_to photos_path, error: t('.error')
    end
    end

    View Slide

  47. def destroy
    @photo = current_user.photos.find(params[:photo_id])
    if RemoveFeaturedFlag.from(@photo)
    redirect_to photos_path
    else
    redirect_to photos_path, error: t('.error')
    end
    end

    View Slide

  48. class AddFeaturedFlag
    def self.to(photo)
    new(photo).call
    end
    def initialize(photo)
    @photo = photo
    end
    def call
    ApplicationRecord.transaction do
    @photo.user.photos.featured.update!(featured: false)
    @photo.update!(featured: true)
    end
    end
    end

    View Slide

  49. Be Boring

    View Slide

  50. Ambiguous Language

    View Slide

  51. resources :orders do
    put :process, on: :member
    end

    View Slide

  52. View Slide

  53. View Slide

  54. Language Matters

    View Slide

  55. Noun: Order
    Verb: process

    View Slide

  56. class OrdersController < ApplicationController
    before_action :find_order, except: [:index]
    before_action #...
    before_action #...
    before_action :ensure_processable, only: [:process]
    before_action #...
    # a bunch of actions
    def process
    # procedure for processing an order
    # potentially very complicated
    order.update!(process_params)
    end
    # more actions
    private
    # dozens of private methods useful to various actions
    end

    View Slide

  57. Noun: Order
    Verb: ship

    View Slide

  58. resources :orders do
    put :ship, on: :member
    end

    View Slide

  59. resources :orders do
    resource :shipment, only: [:create]
    end

    View Slide

  60. class ShipmentsController < ApplicationController
    before_action :ensure_processable
    def create
    # procedure for processing an order
    # potentially very complicated
    order.update!(shipment_params)
    end
    private
    # a subset of those dozens of private methods
    end

    View Slide

  61. View Slide

  62. I see duplication!

    View Slide

  63. You didn't improve
    the code!

    View Slide

  64. Breathing Room

    View Slide

  65. View Slide

  66. View Slide

  67. State Machines

    View Slide

  68. State Machines
    on Rails

    View Slide

  69. resources :orders do
    member do
    put :authorize
    put :capture
    put :refund
    put :reject
    end
    end

    View Slide

  70. state_machine initial: :pending do
    before_transition captured: :refunded, do: :ensure_refundable
    after_transition captured: :refunded, do: :process_refund
    after_transition authorized: :captured, do: :capture_payment
    after_transition pending: :rejected, do: :void_order
    after_transition pending: :authorized, do: :authorize_payment
    #...
    end

    View Slide

  71. state_machine initial: :pending do
    # callbacks...
    event :authorize do
    transition pending: :authorized
    end
    event :capture do
    transition authorized: :captured
    end
    event :refund do
    transition captured: :refunded
    end
    event :reject do
    transition all => :rejected
    end
    end

    View Slide

  72. state_machine initial: :pending do
    # callbacks...
    # events...
    state :captured do
    validates :reward_points, presence: true
    end
    state :rejected do
    validate :validates_tax_exemption
    end
    end

    View Slide

  73. state_machine initial: :pending do
    before_transition captured: :refunded, do: :ensure_refundable
    after_transition captured: :refunded, do: :process_refund
    after_transition authorized: :captured, do: :capture_payment
    after_transition pending: :rejected, do: :void_order
    after_transition pending: :authorized, do: :authorize_payment
    event :authorize do
    transition pending: :authorized
    end
    event :capture do
    transition authorized: :captured
    end
    event :refund do
    transition captured: :refunded
    end
    event :reject do
    transition all => :rejected
    end
    state :captured do
    validates :reward_points, presence: true
    end
    state :pending do
    validate :validates_tax_emption
    end
    end

    View Slide

  74. View Slide

  75. What if update
    didn't exist?

    View Slide

  76. resources :orders, only: [:new, :create] do
    resource :authorization, only: [:create]
    resource :capture, only: [:create]
    resource :refund, only: [:create]
    resource :rejection, only: [:create]
    end

    View Slide

  77. class Authorization
    def initialize(order)
    @order = order
    end
    def process
    raise unless authorizable?
    ApplicationRecord.transaction do
    @order.update!(state: "authorized")
    PaymentGateway.authorize(@order.payment_method, @order.total)
    end
    end
    private
    def authorizable?
    # Is the order in a state where it can be authorized?
    end
    end

    View Slide

  78. Outside In

    View Slide

  79. View Slide

  80. This is fine for
    CRUD apps, but...

    View Slide

  81. Imagination

    View Slide

  82. Be CRUDdy

    View Slide

  83. Be Boring

    View Slide

  84. Be RESTful

    View Slide

  85. Questions?

    View Slide

  86. Derek Prior
    • twitter: @derekprior
    • email: [email protected]
    • podcast: http://bikeshed.fm

    View Slide