Join the social network of Tech Nerds, increase skill rank, get work, manage projects...
 
  • Service Objects in Rails (Refactoring)

    • 0
    • 0
    • 0
    • 0
    • 0
    • 0
    • 0
    • 0
    • 385
    Comment on it

    "Fat models, skinny controllers" has been the design style for Rails app development. However, with time this style has been outdated as with code growth the models become too fat to handle.


    Why not use concerns?

    ActiveSupport concern can be used to extract the fat code out of models and share in multiple models. However ActiveSupport concerns are not useful in much scenarios as we end up including these in our models thus increasing burden.


    In such cases we use a special purpose classes called 'service objects'. So in this tutorial we will look how these classes are used.


    Service objects:

    As SRP (single responsibility principle) focuses on a single responsibility for methods and classes. If our model is handling too many responsibilities, we can extract these out in special classes which focuses on particular bit of business logic each class. Suppose we have some e-commerce website. Where we receive order details from customer, we do some validation and construct some formatted data before sending it to payment gateway. We put this logic in our controller or model which is not directly related to the model. In such scenario we can extract this functionality out and abstract in some special class that only focuses on the processing details. These special classes are called service objects.


    Service objects help in organizing the code in a structured way. It makes it easy to understand and debug the code. Also it is easy to test multiple small functionalities than one large functionality.


    We keep service objects under app/service folder. The name should explain the functionality of service object. So our service object files look like this in our app:

     

    app/
      services/
        create_order.rb
        register_user.rb
        register_user_with_facebook.rb
        change_password.rb


    Lets take an example of registration process where a user registers to the application. After creating user a welcome email is sent to user. This functionality can be extracted out in a service as below:

     

    class RegistrationMailer
      attr_reader :user
     
      def initialize(user)
        @user = user
      end
    
      class << self
        def deliver user
          user = new(user)
          mail = UserMailer.registration user
          mail.deliver
        end
      end
    
    end


     

    class RegistrationController < ApplicationController
      def new
        @user = User.new(params[:user])
      end
    
      def create
        @user = User.new(params[:user])
        begin
          if @user.save
            RegistrationMailer.deliver(@user)
            redirect_to @user
          else
            render 'new'
          end
        rescue
          # Handle exceptions
        end
      end
    end


    In above code we extracted the mailer functionality from controller in a service object class. The class is concerned about the mailing functionality only. This is just a simple example, we can abstract the large code in multiple classes.

     

    Lets see little complex example. In one of my projects i implemented layer chatting functionality. Layer is a third party api provider. It gives chatting facility. It store all chat data on their server and exposed apis to retrieve chats. I created a super class Conversation:

     

    class Conversation
      attr_reader :game, :user
    
      def initialize(game, user)
        @game = game
        @user = user
      end
    
      def host
        game.host
      end
    
      def winner
        game.winner
      end
    
      private
    
      def find_or_create_conversation
        has_conversation? ? game_conversation : new_conversation
      end
    
      def new_conversation
        client = authenticate_layer_user
        Layer::Conversation.create(conversation_attrbs, client)
      end
    
      def conversation_attrbs
        participant = (game.host == user) ? [game.winner.id.to_s] : [game.host.id.to_s]
        {
          participants: participant,
          distinct: true, #create separate conversation
          metadata: {game_id: game.id.to_s, host: game.host.id.to_s, winner: game.winner.id.to_s}
        }
      end
    
      def authorized_for_game?
        return false unless (game && game.can_message?(user))
        true
      end
    
      def has_conversation?
        game.conversation_id.present? && game.conversation_url.present?
      end
    
      def no_conversation?
        !has_conversation?
      end
    
      def game_conversation
        return nil unless has_conversation?
        Layer::Conversation.find(game.conversation_id, authenticate_layer_user)
      end
    
      def authenticate_layer_user
        Layer::Client.authenticate { |nonce| Layer::IdentityToken.new(user.id, nonce)}
      end
    
    end


    For sending new message i created a service objects class inheriting Conversation:
     

    class PostMessage < Conversation
      attr_reader :message, :errors
    
      def initialize(game, user, message)
        super(game, user)
        @message = message
        @errors = []
      end
    
      def process!
        if authorized_for_game?
          begin
            create_chat
            true
          rescue RestClient::ResourceNotFound, Layer::Exceptions::ObjectDeleted
            @errors << "conversation not found"
            false
          end
        else
          @errors << "not authorized for chat"
          false
        end
      end
    
      private
    
      def create_chat
        conversation = find_or_create_conversation
        raise RestClient::ResourceNotFound unless conversation.present?
        save_conversation_params(conversation) if no_conversation?
        conversation.messages.create({parts: [{ body: message, mime_type: 'text/plain' }]})
      end
    
      def save_conversation_params conversation
        game.conversation_url = conversation.url
        game.conversation_id = conversation.id
        game.save
      end
    end


    Now in my controller i can use:


     

    message = PostMessage.new(game, user, message)
    if message.process!
      render json: {message: "Post created"}, status: :ok
    else
      render json: {message: message.errors}, status: :unprocessable_entity
    end


    Conclusion:

    Service objects is not core style of Rails, however rails developers are widely using this approach to maintain the code. It simplifies the testing process and debugging.

     

 0 Comment(s)

Sign In
                           OR                           
                           OR                           
Register

Sign up using

                           OR                           
Forgot Password
Fill out the form below and instructions to reset your password will be emailed to you:
Reset Password
Fill out the form below and reset your password: