Skip to content
cavalle edited this page Dec 23, 2012 · 28 revisions

Taxonomies

Who calls the service object?

  • The controller (service-layer?)
  • The AR model

What represents the service object?

  • A role
  • An external interface
  • A use case

Version #1

http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

class UserAuthenticator
  def initialize(user)
    @user = user
  end

  def authenticate(unencrypted_password)
    return false unless @user

    if BCrypt::Password.new(@user.password_digest) == unencrypted_password
      @user
    else
      false
    end
  end
end
class SessionsController < ApplicationController
  def create
    user = User.where(email: params[:email]).first

    if UserAuthenticator.new(user).authenticate(params[:password])
      self.current_user = user
      redirect_to dashboard_path
    else
      flash[:alert] = "Login failed."
      render "new"
    end
  end
end

Version #2

http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html

class UserCreationService
  def initialize(user_klass = User, log_klass = Log)
    @user_klass = user_klass
    @log_klass  = log_klass
  end

  def create(params)
    @user_klass.create(params).tap do |u|
      @log_klass.new_user(u)
    end
  end
end
describe UserCreationService do
  before do
    @user       = stub("User")
    @user_klass = stub("Class:User", :create   => @user)
    @log_klass  = stub("Class:Log",  :new_user => nil)
    @service    = UserCreationService.new(@user_klass, @log_klass)
    @params     = {:name => "Matz", :hobby => "Being Nice"}
    @service.create(@params)
  end

  it "creates the user with the supplied parameters" do
    @user_klass.should have_received(:create).with(@params)
  end

  it "logs the creation of the user" do
    @log_klass.should have_received(:new_user).with(@user)
  end
end

Before

class User < ActiveRecord::Base
  after_create :log_creation

  protected
    def log_creation
      Log.new_user(self)
    end
end

Version #3

http://www.naildrivin5.com/blog/2012/06/10/single-responsibility-principle-and-rails.html

class ApplicationUserCreator
  def initialize(welcome_mailer=nil)
    @welcome_mailer = welcome_mailer
  end

  def create_new_user(params)
    User.create(params).tap { |new_user|
      if new_user.valid?
        self.welcome_mailer.deliver_welcome_email(new_user)
      end
    }
  end

private

  def welcome_mailer
    @welcome_mailer ||= UserMailer
  end
end
class AdminUsersController < ApplicationController
  def create
    @user = ApplicationUserCreator.new(AdminUserMailer).create_new_user(params[:user])
  end
end

Before

class UsersController < ApplicationController
  def create
    @user = User.new(params[:user])
    respond_to do |format|
      if @user.save
        UserMailer.deliver_welcome_email(@user)
      end
    end
  end
end

Version #4

http://slides.jcoglan.com/di-eurucamp

class Github::Client
  def initialize(http_client)
    @http = http_client
  end

  def get_user(name)
    data = @http.get("/users/#{name}").json_data
    Github::User.new(data)
  end
end

Version #5

(Service layer)

http://slides.jcoglan.com/di-eurucamp

class Services::ConcertsClient
  def initialize(http_client)
    @http = http_client
  end

  def find(id)
    data = @http.get("/concerts/#{id}").json_data
    Models::Concert.new(data)
  end
end
class Models::Concert
  def initialize(data)
    @json_data = data
  end

  def name
    @json_data['name']
  end

  def headliners
    @json_data['performances'].
        select { |p| p['billing'] == 'headline' }.
        map { |p| Models::Artist.new(p['artist']) }
  end
end
module Services
  def self.concerts
    @concerts ||= begin
                    uri  = 'http://concerts-service'
                    http = HTTPClient.new(uri)
                    ConcertsClient.new(http)
                  end
  end
end
concert = Services.concerts.find(params[:id])

Version #6

class DiscountCalculator
  def total_discount(items)
    items.collect(&:discount).inject(:+)
  end
end
class Basket < ActiveRecord::Base
  has_many :basket_items

  def total_discount
    DiscountCalculator.new.total_discount(basket_items)
  end
end

Version #7

(Use case objects)

https://gist.github.com/2838490

class PostComment
  def initialize(user, entry, attributes)
    @user = user
    @entry = entry
    @attributes = attributes
  end
 
  def post
    @comment = @user.comments.new
    @comment.assign_attributes(@attributes)
    @comment.entry = @entry
    @comment.save!
 
    LanguageDetector.new(@comment).set_language
    SpamChecker.new(@comment).check_spam
    CommentMailer.new(@comment).send_mail
 
    post_to_twitter  if @comment.share_on_twitter?
    post_to_facebook if @comment.share_on_facebook?
 
    @comment
  end
 
  private
 
  def post_to_twitter
    PostToTwitter.new(@user, @comment).post
  end
 
  def post_to_facebook
    PostToFacebook.new(@user, @comment).action(:comment)
  end
end

Version #8

class OrdersController < ActionController::Base
  def close
    #retrieval
    payment_handler = Payment.for(params[:method], params[:card])
    process = OrderProcess.new(order, payment_handler)
    process.close
    #rendering
  end
end
class Payment
  def self.for(method, card_details=nil)
    if method == "card"
      CardPayment.new(card_details)
    else
      new
    end
  end

  #default payment handling code
end
class OrderProcess
  def close
    @payment_handler.handle_payment
    Delivery.create! :status => :pending, :order => @order
    @order.close!
  end
end

Version #9

# app/services/authentication.rb
class Authentication
  def initialize(params, omniauth = nil)
    @params = params
    @omniauth = omniauth
  end

  def user
    @user ||= @omniauth ? user_from_omniauth : user_with_password
  end

  def authenticated?
    user.present?
  end

private

  def user_from_omniauth
    User.where(@omniauth.slice(:provider, :uid)).first_or_initialize.tap do |user|
      user.provider = @omniauth[:provider]
      user.uid = @omniauth[:uid]
      user.username = @omniauth[:info][:nickname]
      user.save!
    end
  end

  def user_with_password
    user = User.find_by_username(@params[:username])
    user && user.authenticate(@params[:password])
  end
end
class User < ActiveRecord::Base
  attr_accessible :email, :username, :password, :password_confirmation

  validates_presence_of :username, :email
  validates_uniqueness_of :username
  validates_confirmation_of :password

  has_secure_password
end
class SessionsController < ApplicationController
  #...

  def create
    auth = Authentication.new(params, env["omniauth.auth"])
    if auth.authenticated?
      session[:user_id] = auth.user.id
      redirect_to root_url, notice: "Logged in!"
    else
      flash.now.alert = "Username or password is invalid"
      render "new"
    end
  end

Version #10

class Commenter
  def initialize(person)
    @person = person
  end

  def recent_comments
    @person.comments.where("created_at > ?", 3.days.ago)
  end

  def post_comment(text)
    @person.comments.create(:body => text)
  end

  def update_comment(new_text)
    raise "Comment not owned by this person" unless comment.author == @person

    comment.update_attribute(:body, new_text)
  end
end

Clone this wiki locally