Service objects

# Service Objects

This is a variation of the Presenter technique, with an emphasis on complex
model interactions. A `Service Object` called by a controller should return an
object that encapsulates what the controller should do. This includes flash
messages, redirects, etc. The example class `ControllerResponse` provides a
sample of how to do this. A service object could create a presenter for the
controller to pass to the view.

# Service Objects Applicable Situation

1.  Some action involving the interaction between several models, such as:
    -   A customer creating an order and charging the purchase.
    -   Post login process for a user.
2.  Long complicated methods, and groups of methods on a single model.

# Service Objects Example for Micro Blog

1.  Minors are not allowed to post profanity.
2.  User model counts the number of profanity words used by a minor.
3.  First commit presents a tangled up controller method:
    ```ruby
        def create
          @micropost = current_user.microposts.build(micropost_params)
          ok = true
          if current_user.minor?
            profanity_word =  profanity_word(@micropost.content)
            if profanity_word.present?
              flash.now[:error] = "You cannot create a micropost with profanity: '#{profanity_word}'!"
              current_user.increment(:profanity_count)
              @feed_items = []
              render 'static_pages/home'
              ok = false
            end
          end
          if ok
            if @micropost.save
              flash[:success] = "Micropost created!"
              redirect_to root_url
            else
              @feed_items = []
              render 'static_pages/home'
            end
          end
        end
    ```
    This code is not easily testable, being on the controller. The addition of the
    `profanity_words` method on the controller also does not fit. I don't go into
    building tests, as the refactor will allow easy testing.
    ```ruby
         PROFANITY_WORDS = %w(poop fart fartface poopface poopbuttface)
        
        # Yes, this could go into a validator for the micropost, but let's suppose there's reasons
        # that we don't want to do that, such as we only want to filter profanity for posts
        # created by minors, etc.
        # returns profanity word if existing in content, or else nil
        def profanity_word(content)
          words = content.split(/\W/)
          PROFANITY_WORDS.each do |test_word|
            puts "test_word is #{test_word}, words is #{words}"
            return test_word if words.include?(test_word)
          end
          nil
        end
  ```
4.  After adding MicropostCreationService:
    1.  The code is much easier to test.
    2.  The controller method is much simpler:
        ```ruby
            def create
              response = MicropostCreationService.new(current_user, micropost_params[:content]).create_micropost
              response.apply(self)
            
              unless response.ok # if not ok, then we did a redirect when calling response.apply
                @micropost = response.data[:micropost]
                @feed_items = []
                render 'static_pages/home'
              end
            end 
        ```
        The `response.apply` method does things like setting the flash message and
        the response code.
    3.  The main method of `MicropostCreationService` is simple:
        
     ```ruby
            def create_micropost
              micropost = @user.microposts.build(content: @content)
              response  = check_profanity(micropost)
              unless response
                response = save_micropost(micropost)
              end
              response
            end
       ```
    4.  Note how the Service utilizes the `ControllerResponse` class so as not be
        throwing exceptions for error scenarios.
        ```ruby
            def save_micropost(micropost)
              if micropost.save
                ControllerResponse.new(flash_message: "Micropost created!",
                                       flash_type:    :success,
                                       redirect_path: h.root_url)
              else
                ControllerResponse.new(http_status: :bad_request, data: { micropost: micropost })
              end
            end
            
            # return response or
            def check_profanity(micropost)
              if profanity_word
                msg = "Whoa, better watch your language! Profanity: '#{profanity_word}' not allowed!"
                @user.increment(:profanity_count)
                ControllerResponse.new(flash_message: msg,
                                       flash_type:    :error,
                                       flash_now:     true, # True b/c not redirecting
                                       http_status:   :bad_request,
                                       data:          { micropost: micropost })
              end
            end
      ```
5.  A good test of code is how it changes when requirements change. Suppose that
    the code should print all the profanity in the message, and add the number of
    profanity words to the user's profanity counter.
6.  Putting the profanity calculation into it's own class further shrinks the
    size of classes and clarifies the tests. The main advantage of having the
    `ProfanityChecker` into its own class is that the Micropost can also use the
    logic for validation.
7.  A slight error was left in the commits along with the fix. This is to show
    the usefulness of having concise and simple unit tests on the
    `MicropostCreationService`.

# Service Object Solution

1.  Create a PORO in a `services` directory (or optionally in models).
2.  If using the services directory, be sure to add it to the load path 
   ```ruby
        config.autoload_paths += %W(
          #{config.root}/app/services
        )
    ```
3.  Move all applicable methods into this class from the model.
4.  Use an instance of ControllerResponse to return the responses back to the
    controller.
5.  Create test in `spec/services` directory.

# Service Object Advantages

1.  Clearly bundles code around a specific, complex action.
2.  Promotes the creation of smaller, tighter methods when all methods in a small
    class are around a single purpose.
3.  Allows better unit testing of methods on this service object.

# Service Object Disadvantages

-   It can be overkill for a very simple action.
-   Something simply and generally useful on the model is better put in a concern
    or decorator, or maybe a method class called by the model.