DEV Community

K Putra
K Putra

Posted on

Rails: skinny controller, skinny model

If you continue to read this article, I assume that you know Ruby, OOP in Ruby, and RoR. Perhaps this article is for beginner in rails.

Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.

This is important. Well-structured code has many benefits, one of it is easy to maintained. In rails, the first sign of well-structured code is skinny controller, skinny model.

Let's start our journey! (I use Rails API-only as example, but this article can be implemented in normal Rails as well)

Table of Contents:
Phase 1: Fat Controller, Skinny Model
Phase 2: Skinny Controller, Fat Model
Phase 3: Skinny Controller, Skinny Model
Supplement

Phase 1: Fat Controller, Skinny Model

So, if you are new in rails, probably you just put your business logics in controllers, causing your controllers have hundreds lines of code. On the other hand your models are only have a few lines of code. This is called fat controller, skinny model.

It is not ilegal. But always remember what I quoted in the beginning of this article!

I'll use example. Let say we have these 2 controllers and 2 models:

# app/controllers/users_controller.rb
# total: 37 lines
class UsersController < ApplicationController
  def create
    user = User.create!(user_params)
    # 10 lines of BusinessLogic-A
    # 5 lines of BusinessLogic-B
    render json: { status: "OK", message: "User created!" }, status: 201
  end

  def update
    user = User.update!(user_params)
    # 5 lines of BusinessLogic-B
    render json: { status: "OK", message: "User updated!" }, status: 200
  end

  private

  def user_params
    params.permit(:role, :public_id, :email, :password)
  end
end

# app/controllers/companies_controller.rb
# total: 80 lines
class CompaniesController < ApplicationController
  def create
    company = Company.create!(company_params)
    # 20 lines of BusinessLogic-C
    # 14 lines of BusinessLogic-D
    render json: { status: "OK", message: "Company created!" }, status: 201
  end

  def update
    company = Company.create!(company_params)
    # 20 lines of BusinessLogic-C
    # 9 lines of BusinessLogic-E
    render json: { status: "OK", message: "Company updated!" }, status: 200
  end

  private

  def company_params
      params.permit(:name, :tax_id)
  end
end

# app/models/user.rb
# total: 3 lines
class User < ApplicationRecord
  has_secure_password
end

# app/models/company.rb
# total: 2 lines
class Company < ApplicationRecord
end
Enter fullscreen mode Exit fullscreen mode

What is the first thing you notice? I hope it is that the controllers both have redundant code: UsersController has two BusinessLogic-B written, CompaniesController has two BusinessLogic-C written.

Well, you might get killed!

You can put, let say, BusinessLogic-C as method in CompaniesController, so it does look like this:

# app/controllers/companies_controller.rb
# total: 46 lines (from 80 lines)
class CompaniesController < ApplicationController
  def create
    @company = Company.create!(company_params)
    business_logic_c
    # 14 lines of BusinessLogic-D
    render json: { status: "OK", message: "Company created!" }, status: 201
  end

  def update
    @company = Company.create!(company_params)
    business_logic_c
    # 9 lines of BusinessLogic-E
    render json: { status: "OK", message: "Company updated!" }, status: 200
  end

  private

  def company_params
      params.permit(:name, :tax_id)
  end

  def business_logic_c
    # 20 lines of BusinessLogic-C
  end
end
Enter fullscreen mode Exit fullscreen mode

So you reduce CompaniesController from 80 lines to 46 lines. Not so bad, you have implemented DRY. But it is not good enough.

Why? Let's move to next phase.

Phase 2: Skinny Controller, Fat Model

To answer your last question, I'll quote from other:

“Fat Model, Skinny Controller” refers to how the M and C parts of MVC ideally work together. Namely, any non-response-related logic should go in the model, ideally in a nice, testable method. Meanwhile, the “skinny” controller is simply a nice interface between the view and model.

In practice, this can require a range of different types of refactoring, but it all comes down to one idea: by moving any logic that isn’t about the response to the model (instead of the controller), not only have you promoted reuse where possible but you’ve also made it possible to test your code outside of the context of a request.

source

So, let's move our BusinessLogic from controllers to models!

# app/controllers/users_controller.rb
# total: 20 lines (from 37 lines)
class UsersController < ApplicationController
  def create
    user = User.create!(user_params)
    user.business_logic_a
    user.business_logic_b
    render json: { status: "OK", message: "User created!" }, status: 201
  end

  def update
    user = User.update!(user_params)
    user.business_logic_b
    render json: { status: "OK", message: "User updated!" }, status: 200
  end
  ...
end

# app/controllers/companies_controller.rb
# total: 21 lines (from 80 lines)
class CompaniesController < ApplicationController
  def create
    company = Company.create!(company_params)
    company.business_logic_c
    company.business_logic_d
    render json: { status: "OK", message: "Company created!" }, status: 201
  end

  def update
    company = Company.create!(company_params)
    company.business_logic_c
    company.business_logic_e
    render json: { status: "OK", message: "Company updated!" }, status: 200
  end
  ...
end

# app/models/user.rb
# total: 24 lines (from 3 lines)
class User < ApplicationRecord
  has_secure_password

  def business_logic_a
    # 10 lines of BusinessLogic-A
  end

  def business_logic_b
    # 5 lines of BusinessLogic-A
  end
end

# app/models/company.rb
# total: 53 lines (from 2 lines)
class Company < ApplicationRecord
  def business_logic_c
    # 20 lines of BusinessLogic-C
  end

  def business_logic_d
    # 14 lines of BusinessLogic-D
  end

  def business_logic_e
    # 9 lines of BusinessLogic-E
  end
end
Enter fullscreen mode Exit fullscreen mode

As you can see, the more complex your code is, the skinnier your controller, and the fatter your model.

Note: You can use callbacks if it is suitable for your need. Callbacks are powerful tools in rails. I won't cover callbacks in this article.

And finally, we will be going to the last phase!

Phase 3: Skinny Controller, Skinny Model

"Fat Model, Skinny Controller" is a very good first step, but it doesn't scale well once your codebase starts to grow.

Let's think on the Single Responsibility of models. What is the single responsibility of models? Is it to hold business
logic? Is it to hold non-response-related logic?

No. Its responsibility is to handle the persistence layer and its abstraction.

Business logic, as well as any non-response-related logic and non-persistence-related logic, should go in domain
objects.

Domain objects are classes designed to have only one responsibility in the domain of the problem. Let your classes
"Scream Their Architecture" for the problems they solve.

In practice, you should strive towards skinny models, skinny views and skinny controllers. The architecture of your
solution shouldn't be influenced by the framework you're choosing

source

In practice, you can put Domain Objects in lib or you make app/lib. If you put in app/lib, so you have to know about autoloading in rails. If you put in lib, this is worth reading

In this article, I'll just put our Domain Objects in app/lib. You have to make this directory first. In your root folder, run:

$ mkdir app/lib
Enter fullscreen mode Exit fullscreen mode

Now the directory is ready. Let's start!

# app/controllers/users_controller.rb
# total: 20 lines (from 20 lines)
class UsersController < ApplicationController
  def create
    user = User.create!(user_params)
    BusinessLogicA.new
    BusinessLogicB.new
    render json: { status: "OK", message: "User created!" }, status: 201
  end

  def update
    user = User.update!(user_params)
    BusinessLogicB.new
    render json: { status: "OK", message: "User updated!" }, status: 200
  end
  ...
end

# app/controllers/companies_controller.rb
# total: 21 lines (from 21 lines)
class CompaniesController < ApplicationController
  def create
    company = Company.create!(company_params)
    BusinessLogicC.new
    BusinessLogicD.new
    render json: { status: "OK", message: "Company created!" }, status: 201
  end

  def update
    company = Company.create!(company_params)
    BusinessLogicC.new
    BusinessLogicE.new
    render json: { status: "OK", message: "Company updated!" }, status: 200
  end
  ...
end

# app/models/user.rb
# total: 3 lines (from 24 lines)
class User < ApplicationRecord
  has_secure_password
end

# app/models/company.rb
# total: 2 lines (from 53 lines)
class Company < ApplicationRecord
end

# app/lib/business_logic_a.rb
class BusinessLogicA
  # 10 lines of BusinessLogic-A
end

# app/lib/business_logic_b.rb
class BusinessLogicB
  # 5 lines of BusinessLogic-B
end

# app/lib/business_logic_c.rb
class BusinessLogicC
  # 20 lines of BusinessLogic-C
end

# app/lib/business_logic_d.rb
class BusinessLogicD
  # 14 lines of BusinessLogic-D
end

# app/lib/business_logic_e.rb
class BusinessLogicE
  # 9 lines of BusinessLogic-E
end
Enter fullscreen mode Exit fullscreen mode

So you have skinny controller, skinny model, but you add 5 more files with total of 78 lines. Adding these files is necessary. Why? Read again the quoted text in this phase (read quoted text).

Supplement

This is not the end. You still has many things to learn so your code is more beautiful and easier to maintained. Some of them are:

  1. Ruby Design pattern: you can start from here
  2. Rails Design pattern: you can start from here
  3. Sandi Matz Rule: you can start from here
  4. Clean code: you can start from here
  5. Rails callbacks: you can start from here
  6. Rails concerns: you can start from here
  7. Ruby Metaprogramming: you can start from here
  8. And many more!

Thanks for reading.

Top comments (0)