Skip to content

Instantly share code, notes, and snippets.

@pcreux
Created December 5, 2012 16:30
Show Gist options
  • Save pcreux/4217154 to your computer and use it in GitHub Desktop.
Save pcreux/4217154 to your computer and use it in GitHub Desktop.
Rails++ - Talk given at Lyon.rb meetup in October 2012

Enterprise Rails

   WAT?

Lyon.rb - Octobre 2012

Philippe Creux - @pcreux - http://pcreux.com

Typical Rails App

  • Models
  • Controllers
  • Views
  • Helpers
  • Rake tasks

Typical Rails App (J+30)

  • Fat Models
  • Fat Controllers
  • Complex Views
  • ZOMG Helpers
  • WTF Rake tasks

Advice from the 2000's

Just do...

  • Thin Controllers
  • Fat Models
class UserController < ApplicationController
def create
user = User.create!(params[:user])
redirect_to home_path, notice: "Welcome!"
end
end
class User < ActiveRecord::Base
after_create :create_account
after_create :send_welcome_email
after_create :update_user_count
after_create :ring_the_bell
def create_account
# ...
end
def send_welcome_email
# ActionMailer...
end
end

WRONG!

  • Callbacks spaghetti
  • Slow tests (Store a user => send welcome email?!)
  • Single Responsibility Principle (troll)

Let's wrap the Business Logic into a method...

and put this method into... the model!

class UserController < ApplicationController
def create
user = User.signup(params)
redirect_to home_path, notice: "Welcome!"
end
end
class User < ActiveRecord::Base
def self.signup(params)
user = User.create!(params[:user])
user.accounts.create!
Mailer.send_welcome_email(user)
user
end
end

Service to the Rescue!

Move Business Logic to UserSignupService.

app/services/user_signup_service.rb

class UserController < ApplicationController
def create
user = UserSignupService.signup(params[:user])
redirect_to home_path, notice: "Welcome!"
end
end
# app/services/user_signup_service.rb
class UserSignupService
def self.signup(user_params)
user = User.create!(user_params)
user.accounts.create!
Mailer.send_welcome_email(user)
user
end
end
class UserSignupService < Struct.new(:params, :user)
end
class UserSignupService
attr_reader :params, :user
def initialize(params, user)
@params = params
@user = params
end
end
class User < ActiveRecord::Base
def full_name
[first_name, last_name].compact.join(' ')
end
def age
(Time.now - birthday) / (3600 * 24 * 365)
end
def age_in_words
age > 1 ? "#{age} years old" : "baby"
end
end
module UserHelpers
def full_name(user)
[user.first_name, user.last_name].compact.join(' ')
end
def age(user)
(Time.now - user.birthday) / (3600 * 24 * 365)
end
def age_in_words(age)
age > 1 ? "#{age} years old" : "baby"
end
end
class ICTM::UserDecorator < Draper::Base
decorates :user, class: User
def full_name
[user.first_name, user.last_name].compact.join(' ')
end
def age
(Time.now - user.birthday) / (3600 * 24 * 365)
end
def age_in_words
user.age > 1 ? "#{user.age} years old" : "baby"
end
end
user = User.first.decorate
user.full_name

When you need to aggregate / process data...

Examples:

  • Build an Activity Stream
  • Aggregate values for a charting library
  • ...

... use Presenters!

# app/presenters/user_activity_presenter.rb
class UserActivityPresenter
# @return [Array] of [Events]
def self.activity_for_user(user)
# ...
end
end
describe "Send newsletter to users"
task :send_newsletter do
User.find_each do |user|
if user.subscribed_to_mailing_list?
puts "User ##{user.id} subscribed to mailing list"
puts "Sending..."
begin
Mailer.send_newsletter!(user)
puts "Newsletter sent successfuly!"
rescue
puts "Ooops, something went wrong"
puts $!.message
end
else
puts "User ##{user.id} not subscribed to mailing list. Skipping..."
end
end
end
describe "Send newsletter to users"
task :send_newsletter do
SendNewsletterTask.send
end
# app/tasks/send_newsletter_task.rb
class SendNewsletterTask
def self.send
puts "Sending newsletter"
User.find_each do |user|
# ....
end
puts "Done sending newsletter"
end
end

Jobs...

Wrap them into a class...

Responsibilities

Service: Business Logic Decorator: Decorate a model with ~helper methods Presenter: Aggregate data Task: Do and log

Responsibilities

Service: Business Logic Decorator: Cool data Presenter: Aggregate data Task: Do and log Helpers: Return HTML snippet. (use partials instead?) Controller: Receive request, call service or presenter, redirect or render Model: Persistence, Validations & Scopes

class PagesController < ApplicationController
def show
@articles = Article.where(['
(location_id = ?
OR location_id = 0)
AND category = ?
', current_user.member.location_id, params[:id]
]).order('created_at DESC')
@locations = Location.all
end
end
class PagesController < ApplicationController
def show
@articles = ArticlePresenter.latest_articles(user, params[:id])
@locations = Location.all
end
end
class PagesController < ApplicationController
def show
@articles = ArticlePresenter.latest_articles(user, params[:id])
@locations = Location.all
end
end
class ArticlePresenter
def self.latest_articles(user, category)
Article.where(['
(location_id = ?
OR location_id = 0)
AND category = ?
', user.member.location_id, category
]).order('created_at DESC')
end
end
class ArticlePresenter
def self.latest_articles(user, category)
Article.for_location(user.member.location).
for_category(category).latest
end
end
class ArticlePresenter
def self.latest_articles(user, category)
Article.for_location(user.member.location).for_category(category).latest
end
end
class Article < ActiveRecord::Base
scope :for_location, lambda { |location| where(location_id: [location.id, 0]) }
scope :for_category, lambda { |category| where(category: category) }
scope :latest, order('created_at DESC')
end

Responsibilities

Service: Business Logic Decorator: Cool data Presenter: Aggregate data Task: Do and log Helpers: Return HTML snippet. (use partials instead?) Controller: Receive request, call service or presenter, redirect or render Model: Persistence, Validations & Scopes

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