The document summarizes various Rails antipatterns including monolithic controllers, fat controllers, voyeuristic models, duplicate code, and messy migrations. It provides examples of each antipattern and recommendations for refactoring code to avoid them, such as extracting logic into modules, using scopes over class methods, and avoiding external dependencies in migrations.
5. Index
• Monolithic Controllers
• Fat Controller
• PHPitis
• Voyeuristic Models
• Spaghetti SQL
• Fat Model
• Duplicate Code Duplication
• Fixture Blues
• Messy Migration
6. Monolithic Controllers
• User Authentication
class UsersController ApplicationController
def action
operation = params[:operation]
# ...
end
end
7. Monolithic Controllers
• Our projects
resources :users, only: [] do
collection do
get 'show'
get 'sign_in', to: 'users#sign_in'
get 'sign_up', to: 'users#new'
post 'sign_up', to: 'users#create'
get 'email_sent', to: 'users#email_sent'
get 'verify/:code', to: 'users#verify'
end
end
powerful user
8. Monolithic Controllers
• Our projects
class UsersController ApplicationController
def new
end
def create
end
def show
end
??
def sign_in
end
def sign_out
end
def email_sent
end
def verify
end
end
10. Fat Controller
class RailsController ApplicationController
def create
# ...
# transaction, association
# service logic, etc
# Suppose that this method contains 100+ lines of code.
end
end
11. Fat Controller
class RailsController ApplicationController
def create
active record callback,
build object
# ...
# transaction, association
# service logic, etc
# Suppose that this method contains 100+ lines of code.
end
end
service objects, lib
12. Controller + lib
class ReservationsController ApplicationController
def create
reservation = Reservation.new
ticket = Ticket.new
# ticket code generation
# ...
ticket.code = # ...
reservation.transaction do
ticket.save!
reservation.ticket = ticket
reservation.save!
end
end
end
1) to lib?
13. Controller + lib
class TicketsController ApplicationController
def create
ticket = Ticket.new
code_generator = CodeGenerator.new
ticket.code = code_generator.generate
# ...
end
end
ticket need to be coupling with code
may miss it?
14. Model + lib
class Ticket ActiveRecord::Base
# has a code column
before_save :generate_code
private
def generate_code
active record callback
code_generator = CodeGenerator.new
self.code ||= code_generator.generate
end
end
# TicketsController#create
ticket = Ticket.create!
profit!
15. internal transaction
class ReservationsController ApplicationController
def create
reservation = Reservation.new
reservation.transaction do
reservation.ticket = Ticket.create!
reservation.save!
end
end
end
2) Remove transaction
16. internal transaction
class ReservationsController ApplicationController
def create
reservation = Reservation.new
reservation.ticket.build
reservation.save!
end
end association
17. Service Object
class ReservationsController ApplicationController
def create
result = CreateReservationService.new.execute
end
end
ServiceObject
19. PHPitis
• Do you know PHP?
% if current_user
(current_user == @post.user ||
@post.editors.include?(current_user))
@post.editable?
@post.user.active? %
%= link_to 'Edit this post', edit_post_url(@post) %
% end %
20. Useful accessors to model
• Post#editable_by? (not a helper method)
% if @post.editable_by?(current_user) %
%= link_to 'Edit this post', edit_post_url(@post) %
% end %
21. Useful accessors to model
• Our project
module Admin::UsersHelper
def pretty_phone_number(phone_number)
return unless phone_number
# prettify logic
prettified
end
def pretty_rails # ...
end
%= pretty_phone_number(user.phone_number) %
22. Useful accessors to model
• Decorate a user
class User ActiveRecord::Base
# recommend to use draper
def display_phone_number
return unless phone_number
# prettify logic
prettified
end
end
%= user.display_phone_number %
23. content_for?
• named yield block
html
head
%= yield :head %
/head
body
%= yield %
/body
/html
% content_for :head do %
titleA simple page/title
% end %
pHello, Rails!/p
24. Extract into Custom Helpers
• Markup Helpers
def rss_link(project = nil)
link_to Subscribe to these #{project.name if project} alerts.,
alerts_rss_url(project), :class = feed_link
end
div class=feed
%= rss_link(@project) %
/div
25. Extract into Custom Helpers
• Our project
def nav_link_to (text, link)
active = active if current_page?(link)
content_tag :li, class: active do
link_to text, link
end
end
ul class=nav nav-pills nav-stacked col-md-3 pull-left
%= nav_link_to Unread, notifications_path %
%= nav_link_to All Notifications, notifications_all_path %
/ul
26. Voyeuristic Models
• Situation
class Invoice ActiveRecord::Base
belongs_to :customer
end
class Customer ActiveRecord::Base
has_one :address
has_many :invoice
end
class Address ActiveRecord::Base
belongs_to :customer
end
%= @invoice.customer.address.city %
27. Voyeuristic Models
• Law of Demeter
• No method chaining (Down coupling)
• Basic refactoring of OOP (why getter, setter?)
• Not only for rails
28. Voyeuristic Models
• General way
class Invoice ActiveRecord::Base
# ...
def customer_city
customer.city
end
end
class Customer ActiveRecord::Base
# ...
def city
address.city
end
end
%= @invoice.customer_city %
29. Voyeuristic Models
class Customer ActiveRecord::Base
def city
address.city
end
def street
address.street
end
def state
address.state
end
# many fields below
end
??
30. Voyeuristic Models
• Refactoring using delegate (Rails way)
class Customer ActiveRecord::Base
# ...
delegate :street, :city, :state, to: :address
end
class Invoice ActiveRecord::Base
# ...
delegate :city, to: :customer, prefix: true
end
%= @invoice.customer_city %
35. Scope vs Class method
• Almost same, but scopes are always chainable
class Post ActiveRecord::Base
def self.status(status)
where(status: status) if status.present?
end
def self.recent
limit(10)
end
end
Post.status('active').recent
Post.status('').recent
Post.status(nil).recent
nil
36. Scope vs Class method
• Almost same, but scopes are always chainable
class Post ActiveRecord::Base
scope :status, - status { where(status: status) if
status.present? }
scope :recent, limit(10)
end
Post.status('active').recent
Post.status('').recent
Post.status(nil).recent
just ignored
37. Spaghetti SQL
• Further reading
• http://blog.plataformatec.com.br/2013/02/
active-record-scopes-vs-class-methods/
38. Fat Model
• Use extend, include module
• example: too many scope, finder, etc.
39. Fat Model
• ledermann/unread
module Unread
module Readable
module Scopes
def join_read_marks(user)
# ...
end
def unread_by(user)
# ...
end
# ...
end
end
end
class SomeReadable ActiveRecord::Base
# ...
extend Unread::Readable::Scopes
end
40. Fat Model
• Do you prefer composition to inheritance?
41. Fat Model
• Further Reading
• http://blog.codeclimate.com/blog/
2012/10/17/7-ways-to-decompose-fat-activerecord-
models/
42. Duplicate Code Duplication
• Basic of refactoring
• Extract into modules
• included, extended
• using metaprogramming
43. Extract into modules
class Car ActiveRecord::Base
validates :direction, :presence = true
validates :speed, :presence = true
def turn(new_direction)
self.direction = new_direction
end
def brake
self.speed = 0
end
def accelerate
self.speed = [speed + 10, 100].min
end
# Other, car-related activities...
end
class Bicycle ActiveRecord::Base
validates :direction, :presence = true
validates :speed, :presence = true
def turn(new_direction)
self.direction = new_direction
end
def brake
self.speed = 0
end
def accelerate
self.speed = [speed + 1, 20].min
end
end
44. Extract into modules
module Drivable
extend ActiveSupport::Concern
included do
validates :direction, :presence = true
validates :speed, :presence = true
end
def turn(new_direction)
self.direction = new_direction
end
def brake
self.speed = 0
end
def accelerate
self.speed = [speed + acceleration, top_speed].min
end
end
45. Write a your gem! (plugin)
ex) https://github.com/FeGs/read_activity
module Drivable
extend ActiveSupport::Concern
included do
validates :direction, :presence = true
validates :speed, :presence = true
end
def turn(new_direction)
self.direction = new_direction
end
def brake
self.speed = 0
end
def accelerate
self.speed = [speed + acceleration, top_speed].min
end
end
‘drivable’ gem
46. Write a your gem! (plugin)
module DrivableGem
def self.included(base)
base.extend(Module)
end
module Module
def act_as_drivable
include Drivable
end
end
end
ActiveRecord::Base.send(:include, DrivableGem)
47. Write a your gem! (plugin)
class Car ActiveRecord::Base
act_as_drivable
end
48. How about Metaprogramming?
class Purchase ActiveRecord::Base
validates :status, presence: true,
inclusion: { in: %w(in_progress submitted ...) }
# Status Finders
scope :all_in_progress, where(status: in_progress)
# ...
# Status
def in_progress?
status == in_progress
end
# ...
end
49. How about Metaprogramming?
class Purchase ActiveRecord::Base
STATUSES = %w(in_progress submitted ...)
validates :status, presence: true,
inclusion: { in: STATUSES }
STATUSES.each do |status_name|
scope all_#{status_name}, where(status: status_name)
define_method #{status_name}? do
status == status_name
end
end
end
How to improve reusability?
50. How about Metaprogramming?
class ActiveRecord::Base
def self.has_statuses(*status_names)
validates :status, presence: true,
inclusion: { in: status_names }
status_names.each do |status_name|
scope all_#{status_name}, where(status: status_name)
define_method #{status_name}? do
status == status_name
end
end
end
end
Use extension!
class Purchase ActiveRecord::Base
has_statuses :in_progress, :submitted, # ...
end
51. Fixture Blues
• Rails fixture has many problems:
• No validation
• Not following model lifecycle
• No context
• …
52. Make Use of Factories
• Rails fixture has many problems:
• No validation
• Not following model lifecycle
• No context
• …
53. Make Use of Factories
module Factory
class self
def create_published_post
post = Post.create!({
body: lorem ipsum,
title: published post title,
published: true
})
end
def create_unpublished_post
# ...
end
end
end
54. Make Use of Factories:
FactoryGirl
Factory.sequence :title do |n|
Title #{n}
end
Factory.define :post do |post|
post.body lorem ipsum
post.title { Factory.next(:title) }
post.association :author, :factory = :user
post.published true
end
Factory(:post)
Factory(:post, :published = false)
55. Make Use of Factories
• Rails fixture has many problems:
• No validation
• Not following model lifecycle
• No context
• …
56. Refactor into Contexts
context A dog do
setup do
@dog = Dog.new
end
should bark when sent #talk do
assert_equal bark, @dog.talk
end
context with fleas do
setup do
@dog.fleas Flea.new
@dog.fleas Flea.new
end
should scratch when idle do
@dog.idle!
assert @dog.scratching?
end
57. Refactor into Contexts:
rspec
• context is alias of describe
describe #bark do
before(:each) do
@dog = Dog.new
end
context sick dog do
before(:each) do
@dog.status = :sick
end
# ...
end
end
58. Messy Migrations
• You should ensure that your migrations never
irreconcilably messy.
• Never Modify the up Method on a Committed
Migration : obviously
• Always Provide a down Method in Migrations
59. Never Use External Code in a Migration
class AddJobsCountToUser ActiveRecord::Migration
def self.up
add_column :users, :jobs_count, :integer, :default = 0
Users.all.each do |user|
user.jobs_count = user.jobs.size
user.save
end
end
If No User, No Job?
def self.down
remove_column :users, :jobs_count
end
end
60. Never Use External Code in a Migration
class AddJobsCountToUser ActiveRecord::Migration
def self.up
add_column :users, :jobs_count, :integer, :default = 0
update(-SQL)
UPDATE users SET jobs_count = (
SELECT count(*) FROM jobs
WHERE jobs.user_id = users.id
)
SQL
end
def self.down
remove_column :users, :jobs_count
end
end
No dependancy
61. Never Use External Code in a Migration
class AddJobsCountToUser ActiveRecord::Migration
class Job ActiveRecord::Base
end
class User ActiveRecord::Base
has_many :jobs
end
def self.up
add_column :users, :jobs_count, :integer, :default = 0
User.reset_column_information
Users.all.each do |user|
Provide definition internally
Alternative to raw SQL
user.jobs_count = user.jobs.size
user.save
end
end
# ...
end
62. Never Use External Code in a Migration
• Further Reading
• http://railsguides.net/change-data-in-migrations-
like-a-boss/
• https://github.com/ajvargo/data-migrate
• https://github.com/ka8725/migration_data