rails antipatterns
TRANSCRIPT
Speaker Hong ChulJu
• http://blog.fegs.kr
• https://github.com/FeGs
• Rails Newbie
• SW Maestro 5th
Index• Monolithic Controllers
• Fat Controller
• PHPitis
• Voyeuristic Models
• Spaghetti SQL
• Fat Model
• Duplicate Code Duplication
• Fixture Blues
• Messy Migration
Monolithic Controllers• User Authentication
class UsersController < ApplicationController def action operation = params[:operation] # ... end end
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
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
??
• UsersController#new
• UsersController#create
• UsersController#verify
• UsersController#show
• UsersController#sign_in
• UsersController#sign_out
• UsersController#email_sent
• -
break apart controllers
ActivationsController [:new, :create, :show]
SessionsController [:new, :destroy]
Fat Controller
class RailsController < ApplicationController def create # ... # transaction, association # service logic, etc # Suppose that this method contains 100+ lines of code. end end
Fat Controller
class RailsController < ApplicationController def create # ... # transaction, association # service logic, etc # Suppose that this method contains 100+ lines of code. end end
active record callback, build object
service objects, 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
Controller + lib
1) to lib?
class TicketsController < ApplicationController def create ticket = Ticket.new code_generator = CodeGenerator.new ticket.code = code_generator.generate # ... end end
Controller + lib
ticket need to be coupling with code may miss it?
Model + lib
class Ticket < ActiveRecord::Base # has a code column before_save :generate_code
private def generate_code code_generator = CodeGenerator.new self.code ||= code_generator.generate end end
# TicketsController#create ticket = Ticket.create!
active record callback
profit!
class ReservationsController < ApplicationController def create reservation = Reservation.new
reservation.transaction do reservation.ticket = Ticket.create! reservation.save! end end end
internal transaction
2) Remove transaction
class ReservationsController < ApplicationController def create reservation = Reservation.new reservation.ticket.build reservation.save! end end association
internal transaction
class ReservationsController < ApplicationController def create result = CreateReservationService.new.execute end end
Service Object
ServiceObject
Service Object
https://github.com/gitlabhq/gitlabhq/tree/master/app/services
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 %>
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 %>
module Admin::UsersHelper def pretty_phone_number(phone_number) return "" unless phone_number # prettify logic prettified end
def pretty_rails # ... end
• Our project
<%= pretty_phone_number(user.phone_number) %>
Useful accessors to model
• Decorate a user
<%= user.display_phone_number %>
class User < ActiveRecord::Base # recommend to use draper def display_phone_number return "" unless phone_number # prettify logic prettified end end
Useful accessors to model
• named yield block
content_for?
<html> <head> <%= yield :head %> </head> <body> <%= yield %> </body> </html>
<% content_for :head do %> <title>A simple page</title> <% end %>
<p>Hello, Rails!</p>
• Markup Helpers
Extract into Custom 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>
• Our project
Extract into Custom Helpers
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>
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 %>
Voyeuristic Models• Law of Demeter
• No method chaining (Down coupling)
• Basic refactoring of OOP (why getter, setter?)
• Not only for rails
Voyeuristic Models
<%= @invoice.customer_city %>
class Invoice < ActiveRecord::Base # ... def customer_city customer.city end end
class Customer < ActiveRecord::Base # ... def city address.city end end
• General way
Voyeuristic Modelsclass Customer < ActiveRecord::Base def city address.city end
def street address.street end
def state address.state end
# many fields below end
??
Voyeuristic Models
class Customer < ActiveRecord::Base # ... delegate :street, :city, :state, to: :address end
• Refactoring using delegate (Rails way)
class Invoice < ActiveRecord::Base # ... delegate :city, to: :customer, prefix: true end
<%= @invoice.customer_city %>
Voyeuristic Models• Furthermore
• http://blog.revathskumar.com/2013/08/rails-use-delegates-to-avoid-long-method-chains.html
• http://simonecarletti.com/blog/2009/12/inside-ruby-on-rails-delegate/
• http://blog.aliencube.org/ko/2013/12/06/law-of-demeter-explained/
Spaghetti SQL
class RemoteProcess < ActiveRecord::Base def self.find_top_running_processes(limit = 5) find(:all, :conditions => "state = 'Running'", :order => "percent_cpu desc", :limit => limit) end end
Reusability?
Spaghetti SQL
class RemoteProcess < ActiveRecord::Base scope :running, where(:state => 'Running') scope :system, where(:owner => ['root', 'mysql']) scope :sorted, order("percent_cpu desc") scope :top, lambda {|l| limit(l) } end
RemoteProcess.running.sorted.top(5) RemoteProcess.running.system.sorted.top(5)
Reusability!
Spaghetti SQL
class RemoteProcess < ActiveRecord::Base scope :running, where(:state => 'Running') scope :system, where(:owner => ['root', 'mysql']) scope :sorted, order("percent_cpu desc") scope :top, lambda {|l| limit(l) }
# Shortcut def self.find_top_running_processes(limit = 5) running.sorted.top(limit) end end
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
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
Spaghetti SQL• Further reading
• http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/
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
Fat Model• Further Reading
• http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
Duplicate Code Duplication
• Basic of refactoring
• Extract into modules
• included, extended
• using metaprogramming
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
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
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
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)
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
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?
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
How about Metaprogramming?
Use extension!
class Purchase < ActiveRecord::Base has_statuses :in_progress, :submitted, # ... end
Fixture Blues• Rails fixture has many problems:
• No validation
• Not following model lifecycle
• No context
• …
Make Use of Factories
• Rails fixture has many problems:
• No validation
• Not following model lifecycle
• No context
• …
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
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)
Make Use of Factories
• Rails fixture has many problems:
• No validation
• Not following model lifecycle
• No context
• …
Refactor into Contextscontext "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
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
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
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
def self.down remove_column :users, :jobs_count end end
If No User, No Job?
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
Never Use External Code in a Migrationclass 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| user.jobs_count = user.jobs.size user.save end end # ... end
Provide definition internally Alternative to raw SQL
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