日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

重構 Rails 項目之最佳實踐

 熱血奇跡 2013-03-06
開放課堂項目是由教育大發(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 上周在他的博客里分享了他在重構方面的一些想法,也很有價值,推薦閱讀。

    本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯系方式、誘導購買等信息,謹防詐騙。如發(fā)現有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多