추천중입니다.
닫기 블로그로 보내기


설정된 블로그가 없습니다.

블로그 설정하기

슬라이드를 블로그에 보내는 중입니다.
Advanced Active Record techniques
0
0310
jwseo 2008.07.25 15:49:47
rails 리펙토링에 관한 내용입니다.

마가린 바르기bookmarkr.netmetagsWzd.com네이버에 북마크하기다음에 북마크하기HanRSS에 북마크하기이올린에 북마크하기Pumfit에 글 올리기News2.0에 투고하기del.icio.us에 북마크하기
TAG
URL Copy_btn
EMBED Copy_btn
작성자가 등록한 다른 큐
댓글을 작성하기 위해서는 먼저 로그인 하셔야 합니다.
현재 댓글의 수는 0 개 입니다.
Page 0: Page 1: Advanced Active Record Techniques Best Practice Refactoring Chad Pytel cpytel@thoughtbot.com Page 2: Advanced Active Record Techniques Best Practice Refactoring Chad Pytel cpytel@thoughtbot.com Page 3: Simple Active Record Techniques Best Practice Refactoring Chad Pytel cpytel@thoughtbot.com Page 4: Smart Active Record Techniques Best Practice Refactoring Chad Pytel cpytel@thoughtbot.com Page 5: L A E LD Record Techniques R RActive O Practice Refactoring Best W When Database Records Stop Being Polite... And Start Getting Real. Page 6: This presentation is not going to teach you how to do test driven development. Page 7: In order to refactor your application must be backed by a good test suite. Page 8: This presentation is going to show you code that you might recognize. Page 9: This presentation is going to show you how to make that code better. Page 10: Moving Code from the Controller to the Models Page 11: An Offending Controller class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 12: class ArticlesController < ApplicationController #... def update old_state = @article.state year = params[:article].delete("to_be_published_at(1i)") month = params[:article].delete("to_be_published_at(2i)") day = params[:article].delete("to_be_published_at(3i)") hour = params[:article].delete("to_be_published_at(4i)") minute = params[:article].delete("to_be_published_at(5i)") to_be_published_at = (year and month and day and hour and minute ? Time.mktime(year, month, day, hour, minute) : nil) saved = Article.transaction do if params[:send] or params[:new_version] or params[:subedit] or params[:back] or params[:send_later] or @article.state == "Published" or current_user != @article.current_version.writer @version = @article.create_version!(params[:article], current_user) else @version = @article.current_version @version.attributes = params[:article] @version.writer_id = current_user.id @version.written_at = Time.now end # update existing corrections @version.corrections = params[:corrections] # add correction for published article if old_state == "Published" if params[:correction] and !params[:correction][:correction].blank? @correction = @version.corrections.build(params[:correction]) @correction.person = current_user.name end end @version.authors = params[:authors] @version.categories = params[:categories] @version.tags = params[:tags] @version.references = params[:references] @version.relateds = params[:relateds] @version.links = params[:links] Page 13: state_map = { "Raw" => {:send => "Edit"}, "Edit Check" => {:send => "Edit"}, "Edit" => {:send => "Published", :back => "Edit Check", :subedit => "Sub Edit", :send_later => "Publish Ready"}, "Sub Check" => {:send => "Published", :back => "Edit Check", :subedit => "Sub Edit", :send_later => "Publish Ready"}, "Sub Edit" => {:subedit => "Sub Check"}, "Publish Ready" => {:send_later => "Publish Ready"} } # if everything else is valid, then save the state if params[:send] and @version.valid? @version.state = state_map[old_state][:send] elsif params[:send_later] and @version.valid? @version.state = state_map[old_state][:send_later] elsif params[:subedit] and @version.valid? @version.state = state_map[old_state][:subedit] elsif params[:back] and @version.valid? @version.state = state_map[old_state][:back] end # default and fallback @version.state ||= @article.state # set publish dates if needed if @version.state == "Published" published_at = Time.now @article.first_published_at ||= published_at @article.last_published_at = published_at elsif @version.state == "Publish Ready" @article.to_be_published_at = to_be_published_at if to_be_published_at end if !params[:pdf].blank? @article.pdf = params[:pdf] @article.print = true end @article.save! @version.save! end rescue false Page 14: if saved post_to_api(:staging) email_notifications(@article, old_state, @version.state) if @version.state == "Published" # this may become a special republish api call that doesn't have to do as much work if a correction has been specified (meaning it's a republish) post_to_api(:live) end if params[:send] and @version.state == "Edit" flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.state == "Sub Check" flash[:success] = "Article sent back for editing." elsif @version.state == "Sub Edit" flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.state == "Edit Check" flash[:success] = "Article sent back to reporter for further work." end elsif @version.state == "Publish Ready" flash[:success] = "Article has been marked for publishing at a later date." elsif @version.state == "Published" and old_state != "Published" flash[:success] = "Article saved and published to live server." elsif @version.state == "Published" flash[:success] = "Article has been republished." end # default and fallback flash[:success] ||= "Article updated." # this is temporary, as dispatcher is set up only on staging environment for now if RAILS_ENV == "staging" flash[:success] += " Changes may not be immediately available for preview on foxtrot." end redirect_to article_path(@publication, @article) and return true else load_article_data render :action => 'edit' and return false end end Page 15: An Offending Controller class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 16: An Offending Controller class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 17: An Offending Controller class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 18: An Offending Controller class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 19: A Better Controller class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id if @article.save redirect_to article_path(@publication, @article) else index render :action => :index end end Page 20: Here is How We Get There The Current create_version! Method def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 21: Here is How We Get There The Current create_version! Method def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 22: Here is How We Get There The Current create_version! Method def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 23: Here is How We Get There The Current create_version! Method def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 24: Here is How We Get There The Current create_version! Method def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 25: Here is How We Get There The Current create_first_version! Method def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 26: Here is How We Get There Identifying Things to Refactor: Similar Code def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 27: Here is How We Get There Identifying Things to Refactor: Similar Code def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 28: Here is How We Get There Unnecessary Code def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 29: Here is How We Get There Use Active Record’s Built-in Functionality def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 30: Here is How We Get There Use Active Record’s Built-in Functionality def create_first_version!(attributes, user) version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.written_at = Time.now version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 31: Here is How We Get There Manually Setting Default Values def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 32: Here is How We Get There Manually Setting Default Values def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.state ||= "Raw" version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.article_id = self.id version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 33: Here is How We Get There Set Default Values in the Database class AddRawDefaultToState < ActiveRecord::Migration def self.up change_column_default :article_versions, :state, "Raw" end def self.down execute "ALTER TABLE article_versions ALTER state DROP DEFAULT" end end Page 34: Here is How We Get There Remove Default Fixed in 2.1 class AddRawDefaultToState < ActiveRecord::Migration def self.up change_column_default :article_versions, :state, "Raw" end def self.down change_column_default :article_versions, :state, nil end end Page 35: Here is How We Get There Fodder for Callbacks def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 36: Here is How We Get There Fodder for Callbacks def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 37: Here is How We Get There Fodder for Callbacks def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id version.version = 1 self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current def version if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } self.current_version.version end end version = self.versions.build(attributes) version.writer_id = user.id version.version = self.version + 1 self.save! self.update_attribute(:current_version_id, version.id) version end Page 38: Here is How We Get There A Callback on ArticleVersion class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end Page 39: Here is How We Get There A Callback on ArticleVersion class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end Page 40: Here is How We Get There Now Code is Identical def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end Page 41: Here is How We Get There Now Code is Identical def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end Page 42: Here is How We Get There Now Code is Identical def create_first_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end def create_version!(attributes, user) return create_first_version!(attributes, user) if self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each { |rel| rel.update_attribute(:current, false) } end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end Page 43: Here is How We Get There Identify Another Callback def create_version!(attributes, user) unless self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end Page 44: Here is How We Get There Identify Another Callback def create_version!(attributes, user) unless self.versions.empty? # mark old related links as not current if self.current_version.relateds.any? self.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end Page 45: Here is How We Get There Expressive Callback Names class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? # mark old related links as not current if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end Page 46: Here is How We Get There Expressive Callback Names class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? # mark old related links as not current if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end Page 47: Here is How We Get There Do What You Mean class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end Page 48: Here is How We Get There Do What You Mean class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current unless article.versions.empty? if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end Page 49: Here is How We Get There More Unnecessary Code class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end Page 50: Here is How We Get There More Unnecessary Code class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version if article.current_version.relateds.any? article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end end Page 51: Here is How We Get There Minor Law of Demeter Violation class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end Page 52: Here is How We Get There Minor Law of Demeter Violation class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def set_version_number self.version = (article.current_version ? article.current_version.version : 0) + 1 end def mark_related_links_not_current if article.current_version article.current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end Page 53: Here is How We Get There Conditional Callbacks class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def current_version article.current_version end def set_version_number self.version = (current_version ? current_version.version : 0) + 1 end def mark_related_links_not_current if current_version current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end Page 54: Here is How We Get There Conditional Callbacks class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current private def current_version article.current_version end def set_version_number self.version = (current_version ? current_version.version : 0) + 1 end def mark_related_links_not_current if current_version current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end end Page 55: Here is How We Get There Conditional Callback class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current, :if => :current_version private def current_version article.current_version end def set_version_number self.version = (current_version ? current_version.version : 0) + 1 end def mark_related_links_not_current current_version.relateds.each do |rel| rel.update_attribute(:current, false) end end Page 56: Here is How We Get There The New create_version! Method def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end Page 57: Here is How We Get There The New create_version! Method def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! self.update_attribute(:current_version_id, version.id) version end Page 58: Here is How We Get There Callback to Update the Current Version class ArticleVersion < ActiveRecord::Base before_validation_on_create :set_version_number before_create :mark_related_links_not_current, :if => :current_version after_create :set_current_version_on_article private def set_current_version_on_article article.update_attribute :current_version_id, self.id end Page 59: Here is How We Get There The New create_version! Method def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! version end Page 60: Here is How We Get There The New create_version! Method def create_version!(attributes, user) version = self.versions.build(attributes) version.writer_id = user.id self.save! version end Page 61: Here is How We Get There The Current Create Action class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 62: Here is How We Get There class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = self.versions.build(attributes) @version.writer_id = current_user.id begin Article.transaction do @version = @article.create_version!(params[:version], current_user) end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 63: Here is How We Get There The New EMPTY create_version! Method def create_version!(attributes, user) self.save! version end Page 64: Here is How We Get There Remove the Transaction class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin Article.transaction do @article.save! end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 65: Here is How We Get There Remove the Transaction class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin Article.transaction do @article.save! end rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 66: Here is How We Get There Change to the Non-Bang Save class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin @article.save! rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 67: Here is How We Get There Change to the Non-Bang Save class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id begin @article.save! rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid index render :action => :index and return false end redirect_to article_path(@publication, @article) end Page 68: Here is How We Get There class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication_id = @publication.id @article.reporter_id = current_user.id @version = @article.versions.build(params[:version]) @version.writer_id = current_user.id if @article.save redirect_to article_path(@publication, @article) else index render :action => :index end end Page 69: Here is How We Get There The Final Create Action class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) @article.publication = @publication @article.reporter = current_user @version = @article.versions.build(params[:version]) @version.writer = current_user if @article.save redirect_to article_path(@publication, @article) else index render :action => :index end end Page 70: Phew! What Did We Learn? • • • Even a 15 Line Controller Action is too long. Exceptions should be exceptional. Your models should use callbacks to add complex behavior. Page 71: Too Much Domain Knowledge if params[:send] and @version.state == "Edit" flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.state == "Sub Check" flash[:success] = "Article sent back for editing." elsif @version.state == "Sub Edit" flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.state == "Edit Check" flash[:success] = "Article sent back to reporter for further work." end elsif @version.state == "Publish Ready" flash[:success] = "Article has been marked for publishing at a later date." elsif @version.state == "Published" and old_state != "Published" flash[:success] = "Article saved and published to live server." elsif @version.state == "Published" flash[:success] = "Article has been republished." end Page 72: Too Much Domain Knowledge if params[:send] and @version.edit? flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.sub_check? flash[:success] = "Article sent back for editing." elsif @version.sub_edit? flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.edit_check? flash[:success] = "Article sent back to reporter for further work." end elsif @version.publish_ready? flash[:success] = "Article has been marked for publishing at a later date." elsif @version.published? and old_state != ArticleVersion::STATES[:published] flash[:success] = "Article saved and published to live server." elsif @version.published? flash[:success] = "Article has been republished." end Page 73: Too Much Domain Knowledge if params[:send] and @version.edit? flash[:success] = "Article saved and ready for editing." elsif params[:subedit] if @version.sub_check? flash[:success] = "Article sent back for editing." elsif @version.sub_edit? flash[:success] = "Article sent to sub editors." end elsif params[:back] if @version.edit_check? flash[:success] = "Article sent back to reporter for further work." end elsif @version.publish_ready? flash[:success] = "Article has been marked for publishing at a later date." elsif @version.published? and old_state != ArticleVersion::STATES[:published] flash[:success] = "Article saved and published to live server." elsif @version.published? flash[:success] = "Article has been republished." end Page 74: Too Much Domain Knowledge class ArticleVersion < ActiveRecord::Base STATES = { :edit => 'Edit', :edit_check => 'Edit Check', :sub_edit => 'Sub Edit', :publish_ready => 'Publish Ready', :published => 'Published' } STATES.each do |key, value| define_method "#{key}?", { self.state == "#{value}" } end Page 75: What Did We Learn? • • • Tackle large refactorings iteratively Push as much business logic into the model as possible In lots of code, simple DRY principles will lead a lot of refactoring Page 76: Finders in the Controller class ArticlesController < ApplicationController def index @articles = Article.find_all_by_state(Article::STATES[:published], :order => "created_at DESC") end end Page 77: Move it into the Model class ArticlesController < ApplicationController def index @articles = Article.published end end class Article < ActiveRecord::Base def self.published find_all_by_state(STATES[:published], :order => "created_at DESC") end end Page 78: Move it into the Model has_finder class ArticlesController < ApplicationController def index @articles = Article.published.ordered end end class Article < ActiveRecord::Base has_finder :published, :conditions => {:state => STATES[:published]} has_finder :ordered, :order => "created_at DESC" end Page 79: Move it into the Model named_scope class ArticlesController < ApplicationController def index @articles = Article.published.ordered end end class Article < ActiveRecord::Base named_scope :published, :conditions => {:state => STATES[:published]} named_scope :ordered, :order => "created_at DESC" end Page 80: More named_scope named_scope: passing in arguments class ArticlesController < ApplicationController def index @articles = Article.by_state(params[:state]).ordered end end class Article < ActiveRecord::Base named_scope :ordered, :order => "created_at DESC" named_scope :by_state, lambda { |state| :conditions => {:state => state} } end Page 81: Common Domain Patterns Page 82: User Roles Page 83: User Roles The User Model class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end Page 84: User Roles The Role Model class Role < ActiveRecord::Base has_and_belongs_to_many :users validates_presence_of :name validates_uniqueness_of :name def name=(value) write_attribute("name", value.downcase) end def self.[](name) # Get a role quickly by using: Role[:admin] self.find(:first, :conditions => ["name = ?", name.id2name]) end def add_user(user) self.users << user end def delete_user(user) self.users.delete(user) end end Page 85: User Roles Thoughtless Code class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end Page 86: User Roles Thoughtless Code class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end Page 87: User Roles Thoughtless Code class User < ActiveRecord::Base has_and_belongs_to_many :roles, :uniq => true def has_role?(role_in_question) self.roles.find(:first, :conditions => { :name => role_in_question }) ? true : false end def has_roles?(roles_in_question) roles_in_question = self.roles.find(:all, :conditions => ["name in (?)", roles_in_question]) roles_in_question.length > 0 end def can_post? self.has_roles?(['admin', 'editor', 'associate editor', 'research writer']) end def can_review_posts? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_content? self.has_roles?(['admin', 'editor', 'associate editor']) end def can_edit_post?(post) self == post.user || self.has_roles?(['admin', 'editor', 'associate editor']) end end Page 88: User Roles What We’ve Done class User < ActiveRecord::Base end Page 89: User Roles What We’ve Done class User < ActiveRecord::Base end • Get rid of the role model • Make an admin, editor, and writer boolean on the user model • Active Record gives us nice admin?, editor?, writer? methods on User, and the UI for giving users roles is straightforward Page 90: User Roles Dealing with More Roles in the Future • • If down the road, you need one more role, add one more boolean Two more roles: add the Role model back, but don’t use has and belongs to many Page 91: User Roles Adding the Role Model Back In class User < ActiveRecord::Base has_many :roles end class Role < ActiveRecord::Base TYPES = %w(admin editor writer guest) validates_inclusion_of :name, :in => TYPES class << self TYPES.each do |role_type| define_method "#{role_type}?" do roles.exists?(:name => role_type) end end end end Page 92: What Did We Learn? • • • Don’t Build Beyond Requirements Don’t Jump To a New Model Prematurely No UI to Add == No Model Page 93: Addresses Page 94: Addresses A Normal Association class User < ActiveRecord::Base validates_presence_of :billing_address_id, :shipping_address_id belongs_to :billing_address, class_name => "Address", :foreign_key => "billing_address_id" belongs_to :shipping_address, class_name => "Address", :foreign_key => "shipping_address_id" has_many :orders end class Order < ActiveRecord::Base validates_presence_of :billing_address_id, :shipping_address_id belongs_to :billing_address, class_name => "Address", :foreign_key => "billing_address_id" belongs_to :shipping_address, class_name => "Address", :foreign_key => "shipping_address_id" belongs_to :user end class Address < ActiveRecord::Base validates_presence_of :address_one, :city, :state_province, :postal_code, :country validates_format_of :postal_code, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |address| address.country == "US" } validates_format_of :postal_code, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |address| address.country == "CA" } end Page 95: Addresses class User < ActiveRecord::Base validates_presence_of :billing_address_one, :billing_city, :billing_state, :billing_zip, :billing_country validates_presence_of :shipping_address_one, :shipping_city, :shipping_state, :shipping_zip, :shipping_country validates_format_of :billing_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user.billing_country == "US" } validates_format_of :shipping_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user.shipping_country == "US" } validates_format_of :billing_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user.billing_country == "CA" } validates_format_of :shipping_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user.shipping_country == "CA" } end class Order < ActiveRecord::Base validates_presence_of :billing_address_one, :billing_city, :billing_state, :billing_zip, :billing_country validates_presence_of :shipping_address_one, :shipping_city, :shipping_state, :shipping_zip, :shipping_country validates_format_of :billing_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |order| order.billing_country == "US" } validates_format_of :shipping_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |order| order.shipping_country == "US" } validates_format_of :billing_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |order| order.billing_country == "CA" } validates_format_of :shipping_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |order| order.shipping_country == "CA" } end No Address Model Page 96: Addresses An Address Module class User < ActiveRecord::Base include Addressable end class Order < ActiveRecord::Base include Addressable end module Addressable def self.included(base) base.validates_presence_of :billing_address_one, :billing_city, :billing_state, :billing_zip, :billing_country base.validates_presence_of :shipping_address_one, :shipping_city, :shipping_state, :shipping_zip, :shipping_country base.validates_format_of :billing_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |klass| klass.billing_country == "US" } base.validates_format_of :shipping_zip, :with => /\d{5}(-\d{4})?/, :if => Proc.new { |klass| klass.shipping_country == "US" } base.validates_format_of :billing_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |klass| klass.billing_country == "CA" } base.validates_format_of :shipping_zip, :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |klass| klass.shipping_country == "CA" } end end Page 97: Addresses An Address Mixin class User < ActiveRecord::Base has_address :shipping has_address :billing has_many :orders end module Addressable class << self def included(base) base.extend ClassMethods end end module ClassMethods def has_address(name = "") name = "#{name}_" unless name.blank? validates_presence_of "#{name}address_one", "#{name}city", "#{name}state_province", "#{name}postal_code", "#{name}country" validates_format_of "#{name}postal_code", :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user["#{name}country"] == "US" } validates_format_of "#{name}postal_code", :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user["#{name}country"] == "CA" } end end end ActiveRecord::Base.send :include, Addressable Page 98: class User < ActiveRecord::Base has_address :shipping has_address :billing has_many :orders end module Addressable class << self def included(base) base.extend ClassMethods end end Addresses composed_of module ClassMethods def has_address(name) name << “_” unless name.blank? validates_presence_of "#{name}address_one", "#{name}city", "#{name}state_province", "#{name}postal_code", "#{name}country" validates_format_of "#{name}postal_code", :with => /\d{5}(-\d{4})?/, :if => Proc.new { |user| user["#{name}country"] == "US" } validates_format_of "#{name}postal_code", :with => /[a-zA-Z]\d[a-zA-Z] \d[a-zA-Z]\d/, :if => Proc.new { |user| user["#{name}country"] == "CA" } composed_of "#{name}address", :class_name => “Address”, :mapping => [ ["#{name}address_one", "address_one"], ["#{name}address_two", "address_two"], ["#{name}city", "city"], ["#{name}state_province", "state_province"], ["#{name}postal_code", "postal_code"] ["#{name}country", "country"] ] end end end ActiveRecord::Base.send :include, Addressable Page 99: Addresses composed_of user.billing_address = Address.new("41 Winter St.", "Floor 3", "Boston", "MA", "02108", "US") user.shipping_address = Address.new("407 Broome St.", "5th Floor", "New York", "NY", "10013", "US") user.billing_address == user.shipping_address # => false Page 100: Bad Code Happens to Good People Page 101: This Code Worked Page 102: Refactoring != Bug Fixing Page 103: Always Be Refactoring Page 104: Questions? Chad Pytel cpytel@thoughtbot.com Recommend Me On Working With Rails http://www.workingwithrails.com/person/5509 Page 105: