Forget fat models: it’s time for skinny controllers and skinny models
When learning to code and build applications on the web, particularly when using MVC frameworks like Ruby on Rails, you may well have come across the mantra “Fat Models, Skinny Controllers”. On the surface this appears pretty sound advice — in our experience controllers can easily get out of hand if you’re not strict in keeping as little code in them as possible. Similarly, skinny controllers make it more likely our controllers comply with the Single Responsibility Principle, only handling incoming requests/responses and delegating any all other responsibilities to other classes.
Let’s use an example of a fat Rails controller that sends a user an SMS via Twilio to explore how this works in practice:
class SMSController < ApplicationController
def create
user = User.find(params[:id)
account_id = Rails.application.config.twilio_account_id
twilio = Twilio::REST::Client.new(account_id) args = {
to: user.phone_number,
body: params[:message]
} client.messages.create(args)
end
end
Following the “Fat Models, Skinny Controllers” dictum this code can be moved into the User model:
class User < ActiveRecord::Base
# … def send_sms(message)
account_id = Rails.application.config.twilio_account_id
twilio = Twilio::REST::Client.new(account_id) args = {
to: phone_number,
body: message
} client.messages.create(args)
end # …
end
Our controller is now significantly skinnier:
class SMSController < ApplicationController
def create
user = User.find(params[:id)
user.send_sms(params[:message])
end
end
But our User model now has an additional responsibility of sending text messages, again violating the Single Responsibility Principle. Instead of moving the code there, let’s move it into another object:
class SMS def initialize(config = nil, client = nil)
@config = config || Rails.application.config
@client = client || Twilio::REST::Client.new(config.twilio_account_id)
end def deliver(to, message)
args = {
to: to,
body: message
} client.messages.create(args)
end private attr_reader :client, :config
end
There’s nothing wrong with writing objects like this that are neither models nor controllers — the lib directory in Rails exists for precisely for this purpose. We would argue that this approach should be actively encouraged, since object-oriented code works best when building small objects without too many responsibilities. Not only does it prevent our code becoming too complex and unmaintainable, but testing becomes much easier as well: note how the dependencies to this object have been injected at the top, making it trivial to mock calls to the Twilio client in our unit tests.
All that’s left now is a simple call to our new object in our controller:
class SMSController < ApplicationController
def create
user = User.find(params[:id)
sms = SMS.new sms.deliver(user.phone_number, params[:message])
end
end
We’ve achieved our Holy Grail — a skinny controller and a skinny model.