|
開放課堂項(xiàng)目是由教育大發(fā)現(xiàn)社區(qū)發(fā)起,成都 ThoughtWorks,成都彩程設(shè)計(jì)公司,成都超有愛教育科技有限公司等一起合作開發(fā)和運(yùn)營的教育公益網(wǎng)站,是一個(gè)提供給小學(xué)3-6年級(jí)師生設(shè)計(jì)和開展綜合實(shí)踐課的教育開放平臺(tái)。項(xiàng)目代碼放在 GitHub,采用 Ruby on Rails 作為開發(fā)框架。 很高興我們 Pragmatic.ly 團(tuán)隊(duì)能參與到這個(gè)公益項(xiàng)目的開發(fā)中,我相信這是個(gè)對(duì)社會(huì)很有價(jià)值的事情。征得發(fā)起方的同意,我把這次重構(gòu)工作做成了一次在線秀,也正是因?yàn)檫@次這樣的形式,和很多朋友直接在http:///zhuxian/ 上交流了很多 Rails 項(xiàng)目重構(gòu)方面的想法。通俗點(diǎn)說,重構(gòu)就是對(duì)內(nèi)要通過修改代碼結(jié)構(gòu)等方法讓代碼變得更美,提高可閱讀性和可維護(hù)性,而對(duì)外不改變?cè)瓉淼男袨?,不做任何功能的修改。所以我們做重?gòu)要做好兩點(diǎn): 1) 一次只做一件事情,不能修改了多個(gè)地方后再做驗(yàn)證 2) 小步增量前進(jìn),路是一步一步走出來的。同時(shí),為了保證重構(gòu)的正確性,必須要測試保護(hù),每一次小步修改都必須要保證集成測試仍然通過。之所以要保護(hù)集成測試而非單元測試,正是因?yàn)橹貥?gòu)只改變內(nèi)部結(jié)構(gòu),而不改變外部行為,所以,單元測試是可能失敗的(其實(shí)概率也不高),而集成測試是不允許失敗的。基于 Re-education 的代碼,這次重構(gòu)主要涉及了 Controllers 和 Models 兩個(gè)方面。有興趣的朋友可以去 RailsCasts China 觀看視頻。 Rails 做為一個(gè) Web 開發(fā)框架,幾個(gè)哲學(xué)一直影響著它的發(fā)展,比如 CoC, DRY。而代碼組織方式,則是按照 MVC 模式,推崇 “Skinny Controller, Fat Model”,把應(yīng)用邏輯盡可能的放在 Models 中。 Skinny Controller, Fat Model 讓我們來看最實(shí)際的例子,來自 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, "驗(yàn)證碼有誤" 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” 的標(biāo)準(zhǔn),這段代碼有這么幾個(gè)問題: action 代碼量過長 有很多 @publisher 相關(guān)的邏輯判斷。 從權(quán)責(zé)而言,Controller 負(fù)責(zé)的是接收 HTTP Request,并返回 HTTP Response。而具體如何處理和返回什么數(shù)據(jù),則應(yīng)該交由其他模塊比如 Model/View 去完成,Controller 只需要當(dāng)好控制器即可。所以,從這點(diǎn)上講,如果一個(gè) action 行數(shù)超過 10 行,那絕對(duì)已經(jīng)構(gòu)成了重構(gòu)點(diǎn)。如果一個(gè) action 對(duì)一個(gè) model 變量引用了超過 3 次,也應(yīng)該構(gòu)成了重構(gòu)點(diǎn)。下面是我重構(gòu)后的代碼。 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 => "驗(yàn)證碼有誤" validates :password, :presence => { :message => "密碼為必填寫項(xiàng)" }, :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 在上面的重構(gòu)中,我主要遵循了兩個(gè)方法。 把應(yīng)該屬于 Model 的邏輯從 Controller 移除,放入了 Model。 利用虛擬屬性 password, password_confirmation 處理了本不屬于 Publisher Schema 的邏輯。 關(guān)于簡化 Controller,多利用 Model 方面的重構(gòu)方法,Rails Best Practices 有不少不錯(cuò)的例子,也可以參考。 Move code into model Add model virtual attribute Move finder to scope Beyond Fat Model 對(duì)于項(xiàng)目初期而言,做好這兩個(gè)基本就夠了。但是,隨著邏輯的增多,代碼量不斷增加,我們會(huì)發(fā)現(xiàn) Models 開始變得臃腫,整體維護(hù)性開始降低。如果一個(gè) Model 對(duì)象有效代碼行超過了 100 行,我個(gè)人認(rèn)為因?yàn)橐鹁X了,要思考一下有沒有重構(gòu)點(diǎn)。一般而言,我們有下面幾種方法。 Concern Concern 其實(shí)也就是我們通常說的 Shared Mixin Module,也就是把 Controllers/Models 里面一些通用的應(yīng)用邏輯抽象到一個(gè) Module 里面做封裝,我們約定叫它 Concern。而 Rails 4 已經(jīng)內(nèi)建支持 Concern, 也就是在創(chuàng)建新 Rails 項(xiàng)目的同時(shí),會(huì)創(chuàng)建 app/models/concerns 和 app/controllers/concerns。大家可以看看 DHH 寫的這篇博客 Put chubby models on a diet with concerns 和 Rails 4 的相關(guān) commit。具體使用可以參照上面的博客和下面我們?cè)?Pragmatic.ly 里的實(shí)際例子。 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 都可以擁有很多個(gè)用戶,所以 Membershipable 是公共邏輯,可以抽象成 Concern 并在需要的類里面 include,達(dá)到了 DRY 的目的。 Delegation Pattern Delegation Pattern 是另外一種重構(gòu) Models 的利器。所謂委托模式,也就是我們把一些本跟 Model 數(shù)據(jù)結(jié)構(gòu)淺耦合的東西抽象成一個(gè)對(duì)象,然后把相關(guān)方法委托給這個(gè)對(duì)象,同樣看看具體例子。 未重構(gòu)前: 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 當(dāng)我們需要調(diào)用的 user_profile 屬性越來越多的時(shí)候,會(huì)發(fā)現(xiàn)方法會(huì)不斷增加。這個(gè)時(shí)候,通過 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 關(guān)于更多的如何在 Rails 里使用 delegate 的方法,參考官方文檔 delegate module Acts As XXX 相信大家對(duì) acts-as-list,acts-as-tree 這些插件都不陌生,acts-as-xxx 系列其實(shí)跟 Concern 差不多,只是它有時(shí)不單單是一個(gè) Module,而是一個(gè)擁有更多豐富功能的插件。這個(gè)方式在重構(gòu) Models 時(shí)也是非常的有用。還是舉個(gè)例子。 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 如果你在使用了這些方式重構(gòu)后還是不喜歡代碼結(jié)構(gòu),那么我覺得可能僅僅 MVC 三層就不能滿足你需求了,我們需要更多的抽象,比如 Java 世界廣而告之的 Service 層或者 Presenter 層。這個(gè)更多是個(gè)人習(xí)慣的問題,比如有些人認(rèn)為應(yīng)用邏輯(業(yè)務(wù)邏輯)不應(yīng)該放在數(shù)據(jù)層(Model),或者一個(gè) Model 只應(yīng)該管好他自己的事情,多個(gè) Model 的融合需要另外的類來做代理。關(guān)于這些的爭論已經(jīng)屬于意識(shí)形態(tài)的范疇,個(gè)人的觀點(diǎn)是視需要而定,沒必要一上來就進(jìn)入 Service 或者 Presenter,保持代碼的簡單性,畢竟減少項(xiàng)目 Bugs 的永恒不變法就是沒有代碼。但是,一旦達(dá)到可適用范圍,該引入時(shí)就引入。這里也給大家介紹一些我們?cè)谟玫姆椒ā?/div> Service 之前已經(jīng)提到 Controller 層應(yīng)該只接受 HTTP Request,返回 HTTP Response,中間的處理部分應(yīng)該交由其他部分。我們可以優(yōu)先把這部分邏輯放在 Model 層處理。但是,Model 層本身從定義而言應(yīng)該是只和數(shù)據(jù)打交道,而不應(yīng)該過多涉及業(yè)務(wù)邏輯。這個(gè)時(shí)候我們就需要用到 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 如果大家仔細(xì)分析這段代碼的話,會(huì)發(fā)現(xiàn)用 Service 是最好的方案,既不應(yīng)該放在 Controller,又不適合放在 Model。如果你需要大量使用這種模式,可以考慮一下看看 Imperator 這個(gè) Gem,算是 Rails 世界里對(duì) Service Layer 實(shí)現(xiàn)比較好的庫了。 Presenter 關(guān)于 Presenter,不得不提的是一個(gè) Gem ActivePresenter,基本跟 ActiveRecord 的使用方法一樣,如果項(xiàng)目到了一定規(guī)模比如有了非常多的 Models,那么可以關(guān)注一下 Presenter 模式,會(huì)是一個(gè)很不錯(cuò)的補(bǔ)充。 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 基本上上面是我在一個(gè) Rails 項(xiàng)目里重構(gòu) Controller 和 Model 時(shí)會(huì)使用的幾種方法,希望對(duì)你有用。Terry Tai 上周在他的博客里分享了他在重構(gòu)方面的一些想法,也很有價(jià)值,推薦閱讀。
|
|
|