開放課堂項目是由教育大發(fā)現社區(qū)發(fā)起,成都 ThoughtWorks,成都彩程設計公司,成都超有愛教育科技有限公司等一起合作開發(fā)和運營的教育公益網站,是一個提供給小學3-6年級師生設計和開展綜合實踐課的教育開放平臺。項目代碼放在 GitHub,采用 Ruby on Rails 作為開發(fā)框架。 很高興我們 Pragmatic.ly 團隊能參與到這個公益項目的開發(fā)中,我相信這是個對社會很有價值的事情。征得發(fā)起方的同意,我把這次重構工作做成了一次在線秀,也正是因為這次這樣的形式,和很多朋友直接在http:///zhuxian/ 上交流了很多 Rails 項目重構方面的想法。通俗點說,重構就是對內要通過修改代碼結構等方法讓代碼變得更美,提高可閱讀性和可維護性,而對外不改變原來的行為,不做任何功能的修改。所以我們做重構要做好兩點: 1) 一次只做一件事情,不能修改了多個地方后再做驗證 2) 小步增量前進,路是一步一步走出來的。同時,為了保證重構的正確性,必須要測試保護,每一次小步修改都必須要保證集成測試仍然通過。之所以要保護集成測試而非單元測試,正是因為重構只改變內部結構,而不改變外部行為,所以,單元測試是可能失敗的(其實概率也不高),而集成測試是不允許失敗的?;?Re-education 的代碼,這次重構主要涉及了 Controllers 和 Models 兩個方面。有興趣的朋友可以去 RailsCasts China 觀看視頻。 Rails 做為一個 Web 開發(fā)框架,幾個哲學一直影響著它的發(fā)展,比如 CoC, DRY。而代碼組織方式,則是按照 MVC 模式,推崇 “Skinny Controller, Fat Model”,把應用邏輯盡可能的放在 Models 中。 Skinny Controller, Fat Model 讓我們來看最實際的例子,來自 Re-education 的代碼。 class PublishersController < ApplicationController def create @publisher = Publisher.new params[:publisher] # trigger validation @publisher.valid? unless simple_captcha_valid? then @publisher.errors.add :validation_code, "驗證碼有誤" end if !(params[:password_copy].eql? @publisher.password) then @publisher.errors.add :password, "兩次密碼輸入不一致" end if @publisher.errors.empty? then @publisher.password = Digest::MD5.hexdigest @publisher.password @publisher.save! session[:user_id] = @publisher.id redirect_to publisher_path(@publisher) else p @publisher.errors render "new", :layout => true end end end 按照 “Skinny Controller, Fat Model” 的標準,這段代碼有這么幾個問題: action 代碼量過長 有很多 @publisher 相關的邏輯判斷。 從權責而言,Controller 負責的是接收 HTTP Request,并返回 HTTP Response。而具體如何處理和返回什么數據,則應該交由其他模塊比如 Model/View 去完成,Controller 只需要當好控制器即可。所以,從這點上講,如果一個 action 行數超過 10 行,那絕對已經構成了重構點。如果一個 action 對一個 model 變量引用了超過 3 次,也應該構成了重構點。下面是我重構后的代碼。 class PublishersController < ApplicationController def create @publisher = Publisher.new params[:publisher] if @publisher.save_with_captcha self.current_user = @publisher redirect_to publisher_path(@publisher) else render "new" end end end class Publisher < ActiveRecord::Base apply_simple_captcha :message => "驗證碼有誤" validates :password, :presence => { :message => "密碼為必填寫項" }, :confirmation => { :message => "兩次密碼輸入不一致" } attr_reader :password attr_accessor :password_confirmation def password=(pass) @password = pass self.password_digest = encrypt_password(pass) unless pass.blank? end private def encrypt_password(pass) Digest::MD5.hexdigest(pass) end end 在上面的重構中,我主要遵循了兩個方法。 把應該屬于 Model 的邏輯從 Controller 移除,放入了 Model。 利用虛擬屬性 password, password_confirmation 處理了本不屬于 Publisher Schema 的邏輯。 關于簡化 Controller,多利用 Model 方面的重構方法,Rails Best Practices 有不少不錯的例子,也可以參考。 Move code into model Add model virtual attribute Move finder to scope Beyond Fat Model 對于項目初期而言,做好這兩個基本就夠了。但是,隨著邏輯的增多,代碼量不斷增加,我們會發(fā)現 Models 開始變得臃腫,整體維護性開始降低。如果一個 Model 對象有效代碼行超過了 100 行,我個人認為因為引起警覺了,要思考一下有沒有重構點。一般而言,我們有下面幾種方法。 Concern Concern 其實也就是我們通常說的 Shared Mixin Module,也就是把 Controllers/Models 里面一些通用的應用邏輯抽象到一個 Module 里面做封裝,我們約定叫它 Concern。而 Rails 4 已經內建支持 Concern, 也就是在創(chuàng)建新 Rails 項目的同時,會創(chuàng)建 app/models/concerns 和 app/controllers/concerns。大家可以看看 DHH 寫的這篇博客 Put chubby models on a diet with concerns 和 Rails 4 的相關 commit。具體使用可以參照上面的博客和下面我們在 Pragmatic.ly 里的實際例子。 module Membershipable extend ActiveSupport::Concern included do has_many :memberships, as: :membershipable, dependent: :destroy has_many :users, through: :memberships after_create :create_owner_membership end def add_user(user, admin = false) Membership.create(membershipable: self, user: user, admin: admin) end def remove_user(user) memberships.find_by_user_id(user.id).try(:destroy) end private def create_owner_membership self.add_user(owner, true) after_create_owner_membership end def after_create_owner_membership end end class Project < ActiveRecord::Base include Membershipable end class Account < ActiveRecord::Base include Membershipable end 通過上面的例子,可以看到 Project 和 Account 都可以擁有很多個用戶,所以 Membershipable 是公共邏輯,可以抽象成 Concern 并在需要的類里面 include,達到了 DRY 的目的。 Delegation Pattern Delegation Pattern 是另外一種重構 Models 的利器。所謂委托模式,也就是我們把一些本跟 Model 數據結構淺耦合的東西抽象成一個對象,然后把相關方法委托給這個對象,同樣看看具體例子。 未重構前: class User < ActiveRecord::Base has_one :user_profile def birthday user_profile.try(:birthday) end def timezone user_profile.try(:timezone) || 0 end def hometown user_profile.try(:hometown) end end 當我們需要調用的 user_profile 屬性越來越多的時候,會發(fā)現方法會不斷增加。這個時候,通過 delegate, 我們可以把代碼變得更加的簡單。 class User < ActiveRecord::Base has_one :user_profile delegate :birthday, :tomezone, :hometown, to: :profile def profile self.user_profile || UserProfile.new(birthday: nil, timezone: 0, hometown: nil) end end 關于更多的如何在 Rails 里使用 delegate 的方法,參考官方文檔 delegate module Acts As XXX 相信大家對 acts-as-list,acts-as-tree 這些插件都不陌生,acts-as-xxx 系列其實跟 Concern 差不多,只是它有時不單單是一個 Module,而是一個擁有更多豐富功能的插件。這個方式在重構 Models 時也是非常的有用。還是舉個例子。 module ActiveRecord module Acts #:nodoc: module Cache #:nodoc: def self.included(base) base.extend(ClassMethods) end module ClassMethods def acts_as_cache(options = { }) klass = options[:class_name] || "#{self.name}Cache".constantize options[:delegate] ||= [] class_eval <<-EOV def acts_as_cache_class ::#{klass} end after_commit :create_cache, :if => :persisted? after_commit :destroy_cache, on: :destroy if #{options[:delegate]}.any? delegate *#{options[:delegate]}, to: :cache end include ::ActiveRecord::Acts::Cache::InstanceMethods EOV end end module InstanceMethods def create_cache acts_as_cache_class.create(self) end def destroy_cache acts_as_cache_class.destroy(self) end def cache acts_as_cache_class.find_or_create_cache(self.id) end end end end end class User < ActiveRecord::Base acts_as_cache end class Project < ActiveRecord::Base acts_as_cache end Beyond MVC 如果你在使用了這些方式重構后還是不喜歡代碼結構,那么我覺得可能僅僅 MVC 三層就不能滿足你需求了,我們需要更多的抽象,比如 Java 世界廣而告之的 Service 層或者 Presenter 層。這個更多是個人習慣的問題,比如有些人認為應用邏輯(業(yè)務邏輯)不應該放在數據層(Model),或者一個 Model 只應該管好他自己的事情,多個 Model 的融合需要另外的類來做代理。關于這些的爭論已經屬于意識形態(tài)的范疇,個人的觀點是視需要而定,沒必要一上來就進入 Service 或者 Presenter,保持代碼的簡單性,畢竟減少項目 Bugs 的永恒不變法就是沒有代碼。但是,一旦達到可適用范圍,該引入時就引入。這里也給大家介紹一些我們在用的方法。 Service 之前已經提到 Controller 層應該只接受 HTTP Request,返回 HTTP Response,中間的處理部分應該交由其他部分。我們可以優(yōu)先把這部分邏輯放在 Model 層處理。但是,Model 層本身從定義而言應該是只和數據打交道,而不應該過多涉及業(yè)務邏輯。這個時候我們就需要用到 Service 層。繼續(xù)例子! class ProjectHookService attr_reader :project, :data def initialize(hook_params = {}) @project = Project.from_param(hook_params) @data = JSON.parse(hook_params['payload']) end def parse Prly.hook_services.each do |service| parser = service.new(@project, @data) if parser.parseable? parser.parse end end end def parseable? @project.present? && @data.present? end end class HooksController < ApplicationController def create service = ProjectHookService.new(params) if service.parseable? service.parse render nothing: true, status: 200 else render text: 'Faled to parse the payload', status: 403 end end end 如果大家仔細分析這段代碼的話,會發(fā)現用 Service 是最好的方案,既不應該放在 Controller,又不適合放在 Model。如果你需要大量使用這種模式,可以考慮一下看看 Imperator 這個 Gem,算是 Rails 世界里對 Service Layer 實現比較好的庫了。 Presenter 關于 Presenter,不得不提的是一個 Gem ActivePresenter,基本跟 ActiveRecord 的使用方法一樣,如果項目到了一定規(guī)模比如有了非常多的 Models,那么可以關注一下 Presenter 模式,會是一個很不錯的補充。 class SignupPresenter < ActivePresenter::Base presents :user, :account end SignupPresenter.new(:user_login => 'dingding', :user_password => '123456', :user_password_confirmation => '123456', :account_subdomain => 'pragmaticly') We’re good now 基本上上面是我在一個 Rails 項目里重構 Controller 和 Model 時會使用的幾種方法,希望對你有用。Terry Tai 上周在他的博客里分享了他在重構方面的一些想法,也很有價值,推薦閱讀。
|
|