Refactoring Your Fat Model by Service Objects
Bài đăng này đã không được cập nhật trong 9 năm
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.
All rights reserved