Beyond MVC

70
Beyond MVC Kyle Rames @krames

Transcript of Beyond MVC

Beyond MVC

Kyle Rames@krames

Conventions

app/model/store.rb

stores

db/migrate/20140903103121_create

_store.rb

http://host/stores/edit

app/controller/stores_controller.rb

app/views/store app/helpers

Reduce Cognitive Burden

What happens if the action interacts with an

external service?

What happens when if the action interacts with

multiple models?

How do we deal with complex processes?

?

/lib model

controller gem

Let’s talk about some new patterns

Service Objects

Model Processes

1 class UserAuthenticator 2 def initialize(user) 3 @user = user 4 end 5 6 def authenticate(unencrypted_password) 7 return false unless @user 8 9 if BCrypt::Password.new(@user.password_digest) == 10 unencrypted_password 11 @user 12 else 13 false 14 end 15 end 16 end

1 class UserAuthenticator 2 def initialize(user) 3 @user = user 4 end 5 6 def authenticate(unencrypted_password) 7 return false unless @user 8 9 if BCrypt::Password.new(@user.password_digest) == 10 unencrypted_password 11 @user 12 else 13 false 14 end 15 end 16 end

1 class UserAuthenticator 2 def initialize(user) 3 @user = user 4 end 5 6 def authenticate(unencrypted_password) 7 return false unless @user 8 9 if BCrypt::Password.new(@user.password_digest) == 10 unencrypted_password 11 @user 12 else 13 false 14 end 15 end 16 end

1 class SessionsController < ApplicationController 2 def create 3 user = User.where(email: params[:email]).first 4 5 if UserAuthenticator.new(user).

authenticate(params[:password]) 6 self.current_user = user 7 redirect_to dashboard_path 8 else 9 flash[:alert] = "Login failed." 10 render "new" 11 end 12 end 13 end

1 class SessionsController < ApplicationController 2 def create 3 user = User.where(email: params[:email]).first 4 5 if UserAuthenticator.new(user).

authenticate(params[:password]) 6 self.current_user = user 7 redirect_to dashboard_path 8 else 9 flash[:alert] = "Login failed." 10 render "new" 11 end 12 end 13 end

Martin Fowler Says

The conceptual problem I run into in a lot of codebases is that rather than

representing a process, the "service objects" represent "a thing that does the

process".

Adding User

1. Create User

2. Collect Additional Information

3. Approve

4. Complete

1 class UserAddition 2 def begin 3 # ... 4 end 5 6 def collect_extra_info 7 # ... 8 end 9 10 def approve 11 # ... 12 end 13 14 def complete 15 # ... 16 end 17 end

app/services

dhh says

Concerns

Form Objects

Model Stand-In

1 class SignupForm 2 include ActiveModel::Model 3 4 validates_presence_of :email 5 6 delegate :name, :name= to: :company, prefix: true 7 delegate :name, :name=, :email, :email= to: :user 8 9 def company 10 @company ||= Company.new 11 end 12 13 def user 14 @user ||= company.build_user 15 end 16 17 def persisted? 18 false # Forms are never themselves persisted 19 end 20 21 def save 22 valid? ? persist!; true : false 23 end 24 25 private 26 27 def persist! 28 company.save && user.save 29 end 30 end

1 class SignupForm 2 include ActiveModel::Model 3 4 validates_presence_of :email 5 6 delegate :name, :name= to: :company, prefix: true 7 delegate :name, :name=, :email, :email= to: :user 8 9 def company 10 @company ||= Company.new 11 end 12 13 def user 14 @user ||= company.build_user 15 end 16 17 def persisted? 18 false # Forms are never themselves persisted 19 end 20 21 def save 22 valid? ? persist!; true : false 23 end 24 25 private 26 27 def persist! 28 company.save && user.save 29 end 30 end

1 class SignupForm 2 include ActiveModel::Model 3 4 validates_presence_of :email 5 6 delegate :name, :name= to: :company, prefix: true 7 delegate :name, :name=, :email, :email= to: :user 8 9 def company 10 @company ||= Company.new 11 end 12 13 def user 14 @user ||= company.build_user 15 end 16 17 def persisted? 18 false # Forms are never themselves persisted 19 end 20 21 def save 22 valid? ? persist!; true : false 23 end 24 25 private 26 27 def persist! 28 company.save && user.save 29 end 30 end

1 class SignupForm 2 include ActiveModel::Model 3 4 validates_presence_of :email 5 6 delegate :name, :name= to: :company, prefix: true 7 delegate :name, :name=, :email, :email= to: :user 8 9 def company 10 @company ||= Company.new 11 end 12 13 def user 14 @user ||= company.build_user 15 end 16 17 def persisted? 18 false # Forms are never themselves persisted 19 end 20 21 def save 22 valid? ? persist!; true : false 23 end 24 25 private 26 27 def persist! 28 company.save && user.save 29 end 30 end

1 class SignupsController < ApplicationController 2 def create 3 @signup = SignupForm.new(params[:signup_form]) 4 5 if @signup.save 6 redirect_to dashboard_path 7 else 8 render "new" 9 end 10 end 11 end

1 class SignupsController < ApplicationController 2 def create 3 @signup = SignupForm.new(params[:signup_form]) 4 5 if @signup.save 6 redirect_to dashboard_path 7 else 8 render "new" 9 end 10 end 11 end

dhh says

accepts_nested_attributes_for

1 class User < ActiveRecord::Base 2 3 validates_length_of :new_password, 4 minimum: 6, 5 if: :changing_password 6 7 ... 8 end

1 class User < ActiveRecord::Base 2 3 validates_length_of :new_password, 4 minimum: 6, 5 if: :changing_password 6 7 ... 8 end

1 class PasswordForm 2 include ActiveModel::Model 3 4 attr_accessor :original_password, :new_password 5 6 validate :verify_original_password 7 validates_presence_of :original_password, :new_password 8 validates_confirmation_of :new_password 9 validates_length_of :new_password, minimum: 6 10 11 def submit(params) 12 self.original_password = params[:original_password] 13 self.new_password = params[:new_password] 14 self.new_password_confirmation = params[ 15 :new_password_confirmation] 16 if valid? 17 @user.password = new_password; @user.save; true 18 else 19 false 20 end 21 end 22 23 def initialize(user); @user = user; end 24 def persisted?; false; end 25 def verify_original_password; ...; end 26 end

1 class PasswordForm 2 include ActiveModel::Model 3 4 attr_accessor :original_password, :new_password 5 6 validate :verify_original_password 7 validates_presence_of :original_password, :new_password 8 validates_confirmation_of :new_password 9 validates_length_of :new_password, minimum: 6 10 11 def submit(params) 12 self.original_password = params[:original_password] 13 self.new_password = params[:new_password] 14 self.new_password_confirmation = params[ 15 :new_password_confirmation] 16 if valid? 17 @user.password = new_password; @user.save; true 18 else 19 false 20 end 21 end 22 23 def initialize(user); @user = user; end 24 def persisted?; false; end 25 def verify_original_password; ...; end 26 end

1 class PasswordsController < ApplicationController 2 def new 3 @password_form = PasswordForm.new(current_user) 4 end 5 6 def create 7 @password_form = PasswordForm.new(current_user) 8 if @password_form.submit(params[:password_form]) 9 redirect_to current_user, notice: “Success!" 10 else 11 render "new" 12 end 13 end 14 end

app/forms

Presenters

1 <div id="profile"> 2 <dl> 3 <dt>Username:</dt> 4 <dd><%= @user.username %></dd> 5 <dt>Member Since:</dt> 6 <dd><%= @user.member_since %></dd> 7 <dt>Website:</dt> 8 <dd> 9 <% if @user.url.present? %> 10 <%= link_to @user.url, @user.url %> 11 <% else %> 12 <span class="none">None given</span> 13 <% end %> 14 </dd> 15 <dt>Twitter:</dt> 16 <dd> 17 <% if @user.twitter_name.present? %> 18 <%= link_to @user.twitter_name, "http://twitter.com/#{@user. 19 twitter_name}" %> 20 <% else %> 21 <span class="none">None given</span> 22 <% end %> 23 </dd> 24 </dl> 25 </div>

1 <div id="profile"> 2 <dl> 3 <dt>Username:</dt> 4 <dd><%= @user.username %></dd> 5 <dt>Member Since:</dt> 6 <dd><%= @user.member_since %></dd> 7 <dt>Website:</dt> 8 <dd> 9 <% if @user.url.present? %> 10 <%= link_to @user.url, @user.url %> 11 <% else %> 12 <span class="none">None given</span> 13 <% end %> 14 </dd> 15 <dt>Twitter:</dt> 16 <dd> 17 <% if @user.twitter_name.present? %> 18 <%= link_to @user.twitter_name, "http://twitter.com/#{@user. 19 twitter_name}" %> 20 <% else %> 21 <span class="none">None given</span> 22 <% end %> 23 </dd> 24 </dl> 25 </div>

1 class UserPresenter 2 attr_reader :user 3 delegate :username, :url, :twitter_name, 4 :full_name,:created_at to: :user 5 6 def initialize(user, template) 7 @user, @template = user, template 8 end 9 10 def member_since 11 user.created_at.strftime("%B %e, %Y") 12 end 13 14 def website 15 handle_none user.url { h.link_to(user.url, user.url) } 16 end 17 18 def twitter 19 handle_none user.twitter_name { h.link_to user.twitter_name, "http: 20 //twitter.com/#{user.twitter_name}" } 21 end 22 23 private 24 def h 25 @template 26 end 27 28 def handle_none(value) 29 ... 30 end 31 end

1 class UserPresenter 2 attr_reader :user 3 delegate :username, :url, :twitter_name, 4 :full_name,:created_at to: :user 5 6 def initialize(user, template) 7 @user, @template = user, template 8 end 9 10 def member_since 11 user.created_at.strftime("%B %e, %Y") 12 end 13 14 def website 15 handle_none user.url { h.link_to(user.url, user.url) } 16 end 17 18 def twitter 19 handle_none user.twitter_name { h.link_to user.twitter_name, "http: 20 //twitter.com/#{user.twitter_name}" } 21 end 22 23 private 24 def h 25 @template 26 end 27 28 def handle_none(value) 29 ... 30 end 31 end

1 class UserPresenter 2 attr_reader :user 3 delegate :username, :url, :twitter_name, 4 :full_name,:created_at to: :user 5 6 def initialize(user, template) 7 @user, @template = user, template 8 end 9 10 def member_since 11 user.created_at.strftime("%B %e, %Y") 12 end 13 14 def website 15 handle_none user.url { h.link_to(user.url, user.url) } 16 end 17 18 def twitter 19 handle_none user.twitter_name { h.link_to user.twitter_name, "http: 20 //twitter.com/#{user.twitter_name}" } 21 end 22 23 private 24 def h 25 @template 26 end 27 28 def handle_none(value) 29 ... 30 end 31 end

1 class UserPresenter 2 attr_reader :user 3 delegate :username, :url, :twitter_name, 4 :full_name,:created_at to: :user 5 6 def initialize(user, template) 7 @user, @template = user, template 8 end 9 10 def member_since 11 user.created_at.strftime("%B %e, %Y") 12 end 13 14 def website 15 handle_none user.url { h.link_to(user.url, user.url) } 16 end 17 18 def twitter 19 handle_none user.twitter_name { h.link_to user.twitter_name, "http: 20 //twitter.com/#{user.twitter_name}" } 21 end 22 23 private 24 def h 25 @template 26 end 27 28 def handle_none(value) 29 ... 30 end 31 end

1 <div id="profile"> 2 <dl> 3 <dt>Username:</dt> 4 <dd><%= @user_presenter.username %></dd> 5 <dt>Member Since:</dt> 6 <dd><%= @user_presenter.member_since %></dd> 7 <dt>Website:</dt> 8 <dd><%= @user_presenter.website %></dd> 9 <dt>Twitter:</dt> 10 <dd><%= @user_presenter.twitter %></dd> 11 </dl> 12 </div>

1 <div id="profile"> 2 <dl> 3 <dt>Username:</dt> 4 <dd><%= @user_presenter.username %></dd> 5 <dt>Member Since:</dt> 6 <dd><%= @user_presenter.member_since %></dd> 7 <dt>Website:</dt> 8 <dd><%= @user_presenter.website %></dd> 9 <dt>Twitter:</dt> 10 <dd><%= @user_presenter.twitter %></dd> 11 </dl> 12 </div>

1 def show 2 user = User.find(params[:id]) 3 @user_presenter = UserPresenter.new(user, view_context) 4 end

1 def show 2 user = User.find(params[:id]) 3 @user_presenter = UserPresenter.new(user, view_context) 4 end

1 class UserPresenter 2 attr_reader :user 3 delegate :username, :member_since, :url, 4 :twitter_name, :full_name,:created_at to: :user 5 6 def initialize(user, template) 7 @user, @template = user, template 8 end 9 10 def member_since 11 user.created_at.strftime("%B %e, %Y") 12 end 13 14 def website 15 handle_none user.url { h.link_to(user.url, user.url) } 16 end 17 18 def twitter 19 handle_none user.twitter_name { h.link_to user.twitter_name, "http: 20 //twitter.com/#{user.twitter_name}" } 21 end 22 23 private 24 def h 25 @template 26 end 27 28 def handle_none(value) 29 ... 30 end 31 end

gem 'draper'

1 class UserPresenter < Draper::Decorator 2 delegate_all 3 4 def member_since 5 object.created_at.strftime("%B %e, %Y") 6 end 7 8 def website 9 handle_none object.url do 10 h.link_to(object.url, object.url) 11 end 12 end 13 14 def twitter 15 handle_none object.twitter_name do 16 h.link_to object.twitter_name, 17 "http://twitter.com/#{object.twitter_name}" 18 end 19 end 20 21 private 22 def handle_none(value) 23 ... 24 end 25 end

1 class UserPresenter < Draper::Decorator 2 delegate_all 3 4 def member_since 5 object.created_at.strftime("%B %e, %Y") 6 end 7 8 def website 9 handle_none object.url do 10 h.link_to(object.url, object.url) 11 end 12 end 13 14 def twitter 15 handle_none object.twitter_name do 16 h.link_to object.twitter_name, 17 "http://twitter.com/#{object.twitter_name}" 18 end 19 end 20 21 private 22 def handle_none(value) 23 ... 24 end 25 end

1 class UserPresenter < Draper::Decorator 2 delegate_all 3 4 def member_since 5 object.created_at.strftime("%B %e, %Y") 6 end 7 8 def website 9 handle_none object.url do 10 h.link_to(object.url, object.url) 11 end 12 end 13 14 def twitter 15 handle_none object.twitter_name do 16 h.link_to object.twitter_name, 17 "http://twitter.com/#{object.twitter_name}" 18 end 19 end 20 21 private 22 def handle_none(value) 23 ... 24 end 25 end

1 class UserPresenter < Draper::Decorator 2 delegate_all 3 4 def member_since 5 object.created_at.strftime("%B %e, %Y") 6 end 7 8 def website 9 handle_none object.url do 10 h.link_to(object.url, object.url) 11 end 12 end 13 14 def twitter 15 handle_none object.twitter_name do 16 h.link_to object.twitter_name, 17 "http://twitter.com/#{object.twitter_name}" 18 end 19 end 20 21 private 22 def handle_none(value) 23 ... 24 end 25 end

1 class UserPresenter < Draper::Decorator 2 delegate_all 3 4 def member_since 5 object.created_at.strftime("%B %e, %Y") 6 end 7 8 def website 9 handle_none object.url do 10 h.link_to(object.url, object.url) 11 end 12 end 13 14 def twitter 15 handle_none object.twitter_name do 16 h.link_to object.twitter_name, 17 "http://twitter.com/#{object.twitter_name}" 18 end 19 end 20 21 private 22 def handle_none(value) 23 ... 24 end 25 end

1 class UserPresenter < Draper::Decorator 2 delegate_all 3 4 def member_since 5 object.created_at.strftime("%B %e, %Y") 6 end 7 8 def website 9 handle_none object.url do 10 h.link_to(object.url, object.url) 11 end 12 end 13 14 def twitter 15 handle_none object.twitter_name do 16 h.link_to object.twitter_name, 17 "http://twitter.com/#{object.twitter_name}" 18 end 19 end 20 21 private 22 def handle_none(value) 23 ... 24 end 25 end

1 def show 2 @user = User.find(params[:id]).decorate 3 end

app/presenters

dhh says

helpers

Summary

References• http://blog.codeclimate.com/blog/2012/10/17/7-

ways-to-decompose-fat-activerecord-models/

• https://gist.github.com/blaix/5764401

• https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services

• https://medium.com/@ryakh/rails-form-objects-84b6849c886e

Railscasts

• http://railscasts.com/episodes/416-form-objects

• http://railscasts.com/episodes/398-service-objects

• http://railscasts.com/episodes/287-presenters-from-scratch

• http://railscasts.com/episodes/286-draper

Questions?

@krames