rails antipatterns

62
Rails Antipattern Hong ChulJu

Upload: chul-ju-hong

Post on 17-Jul-2015

128 views

Category:

Software


1 download

TRANSCRIPT

Rails AntipatternHong����������� ������������������  ChulJu

Speaker Hong ChulJu

• http://blog.fegs.kr

• https://github.com/FeGs

• Rails Newbie

• SW Maestro 5th

RAILS ANTIPATTERNmainly about code refactoring

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• Use extend, include module

• example: too many scope, finder, etc.

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• Do you prefer composition to inheritance?

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)

Write a your gem! (plugin)

class Car < ActiveRecord::Base act_as_drivable end

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

EOF