12/08/2018, 00:06

Refactoring Your Fat Model by Service Objects

Thin Controller, Fat Model Thin Controller, Fat Model. It means that we should make our controller compact and put all business logic into the models to keep our application maintainable. This sentence reminds me of a good old days with our Rails app when it was still small and cute like a ...

Thin Controller, Fat Model

Thin Controller, Fat Model. It means that we should make our controller compact and put all business logic into the models to keep our application maintainable. This sentence reminds me of a good old days with our Rails app when it was still small and cute like a kitten. But as service grows, our app grows. It did not take too long until I realize my kitten was going to be a behemoth.

These five years was the time that people are realizing that Thin Controller Fat Model principle contains a serious problem with Rails -- as your application grows, you will feed most of your code to your models, especially to your business's core domain models. And they will get fatter and fatter infinitely. So your most important model is going to be the fattest model, which is the most difficult to maintain. This is obviously a sad situation because your mother will not allow you to buy a new kitten.

Fortunately enough, now we have many ways to lose weight of our fat models. This problem happens because there are only three layers in RoR. So the basic idea is to add more layers. One solution is to make Service Objects.

Service Object

So what was really wrong with Fat Model principle? It basically told us to put all business logic into models. Sounds reasonable. But what if we have a business logic which handles multiple models and multiple instances. In which model class should we put our code? And why?

For example, when a user buys something in our shopping website, we might want to do many things like

  • Save Order records.
  • Decrement the number of stocks of Item record.
  • Give users promotional point on User record.
  • Send email to the customer by OrderMailer.

So, which class should have these logic? It seems that there is no absolute answer. Instead, we can make an independent class which handle all those logics but does not belong to any of the classes above. This is Service Object. In the example above we will make a Service::MakeOrder class. Note that the class name starts with verb because this class represents a "doing", not "being".

Service::MakeOrder will look like this.

class Service::MakeOrder
  def make(order)
    ::Order.transaction do
      save_order_record(order)
      decrement_stock(order.items)
      give_point(order.user)
      send_thanks_mail(order)
    end
  end

  ....
end

And in controller we can just call like

class OrderController
  def create
    order = Order.new(order_params)
    Serivce::MakeOrder.new.make(order)
    ...
  end

So a service class is:

  • It encapsulates the application's core business logic and handles user's action.
  • It has a single responsiblilty and has only one public method.
  • It represents "doing", not "being".
  • It does not have a return value.
  • It does not have a state.
  • It has a side effect.

Basically we can put anything into this class. So many complicated and dirty logic might come here. But still we can keep this class clean and testable by following the rules like single responsibility, no state and no return value. The most important point is that it should represent a single business process described in the user's point of view. So the name is important too.

Here are some other example of the class name of Service Objects.

  • PublishBlogPost
  • CancelOrder
  • ShipItem
  • ConfirmRegistration
  • BookRoom

They all represent single business process in the application domain. Notice that no technical words like save or delete is used. All verbs should be stated in the user's point of view.

Actually the restrictions of "no state, no return value" sounds too strict. But we can handle this problem by something like pub/sub mechanism. I will post about it later.

0