
- [Service Objects (and Interactor Objects)](#1. Service Objects (and Interactor Objects))
- [Value Objects](#2. Value Objects)
- [Form Objects](#3. Form Objects)
- [Query Objects](#4. Query Objects)
- [View Objects (Serializer/Presenter)](#5. View Objects (Serializer/Presenter))
- [Policy Objects](#6. Policy Objects)
- [Decorators](#7. Decorators)
1. Service Objects (and Interactor Objects)
當Controller中的action有以下癥狀時適用:
- 過于復雜(如,計算員工的工資)
- 調用外部api服務
- 明顯不屬于任何model (如, 刪除過期數(shù)據(jù))
- 使用多個model(如,從一個文件中導入數(shù)據(jù)到多個model)
示例
以下示例中,主要工作由外部Stripe服務完成。該服務基于郵件地址和來源創(chuàng)建Stripe客戶,并將所有服務費用綁定到該客戶的賬號上。
問題分析
- Controller中包含調用外部服務的代碼
- Controller負責構建調用外部服務所需的數(shù)據(jù)
- Controller難于維護和擴展
class ChargesController < ApplicationController
def create
amount = params[:amount].to_i * 100
customer = .create(
email: params[:email],
source: params[:source]
)
charge = Stripe::Charge.create(
customer: customer.id,
amount: amount,
description: params[:description],
currency: params[:currency] || 'USD'
)
redirect_to charges_path
rescue Stripe::CardError => exception
flash[:error] = exception.message
redirect_to new_charge_path
end
end
為了解決這些問題,可以將其封裝為一個外部服務。
class ChargesController < ApplicationController
def create
CheckoutService.new(params).call
redirect_to charges_path
rescue Stripe::CardError => exception
flash[:error] = exception.message
redirect_to new_charge_path
end
end
class CheckoutService
DEFAULT_CURRENCY = 'USD'.freeze
def initialize(options = {})
options.each_pair do |key, value|
instance_variable_set("@#{key}", value)
end
end
def call
Stripe::Charge.create(charge_attributes)
end
private
attr_reader :email, :source, :amount, :description
def currency
@currency || DEFAULT_CURRENCY
end
def amount
@amount.to_i * 100
end
def customer
@customer ||= Stripe::Customer.create(customer_attributes)
end
def customer_attributes
{
email: email,
source: source
}
end
def charge_attributes
{
customer: customer.id,
amount: amount,
description: description,
currency: currency
}
end
end
最終由CheckoutService來負責客戶賬號的創(chuàng)建和支付,從而解決了Controller中業(yè)務代碼過多的問題。但是,還有一個問題需要解決。如果外部服務拋出異常時(如,信用卡無效)該如何處理,需要重定向的其他頁面嗎?
class ChargesController < ApplicationController
def create
CheckoutService.new(params).call
redirect_to charges_path
rescue Stripe::CardError => exception
flash[:error] = exception.error
redirect_to new_charge_path
end
end
為了解決這個問題,可以在一個Interactor對象中調用CheckoutService,并捕獲可能產(chǎn)生的異常。Interactor模式常用于封裝業(yè)務邏輯,每個Interactor一般只描述一條業(yè)務邏輯。
Interactor模式通過簡單Ruby對象(plain old Ruby objects, POROs)可以幫助我們實現(xiàn)單一原則(Single Responsibility Principle, SRP)。Interactor與Service Object類似,只是通常會返回執(zhí)行狀態(tài)及相關信息,而且一般會在Interactor內部使用Service Object。下面是該設計模式的使用示例:
class ChargesController < ApplicationController
def create
interactor = CheckoutInteractor.call(self)
if interactor.success?
redirect_to charges_path
else
flash[:error] = interactor.error
redirect_to new_charge_path
end
end
end
class CheckoutInteractor
def self.call(context)
interactor = new(context)
interactor.run
interactor
end
attr_reader :error
def initialize(context)
@context = context
end
def success?
@error.nil?
end
def run
CheckoutService.new(context.params)
rescue Stripe::CardError => exception
fail!(exception.message)
end
private
attr_reader :context
def fail!(error)
@error = error
end
end
移除所有信用卡錯誤相關的異常,Controller就達到了瘦身的目的。瘦身以后,Controller只負責成功支付和失敗支付時的頁面跳轉。
2. Value Objects
Value Object設計模式推崇簡潔的對象(僅包含一些給定的值),并支持根據(jù)給定的邏輯,或基于指定的屬性進行對象間相互比較(不基于id)。Value Object的例子如,以不同幣種表示的貨幣。我們可以用一個幣種(如,美元)來比較這些對象。同樣,Value Object也可以用于表示溫度,并可用單位開來進行比較。
示例
假設有一所帶電加熱的智能房子,加熱器可以通過網(wǎng)絡接口加以控制。Controller的一個方法將從溫度傳感器那里收到指定加熱器的參數(shù):溫度數(shù)值和溫度單位(華氏、攝氏或開)。如果是其他溫度單位,一律先轉換為開。然后,檢查溫度是否小于25°C并大于等于當前溫度。
問題分析
Controller中包含了太多與溫度轉換和比較相關的邏輯代碼。
class AutomatedThermostaticValvesController < ApplicationController
SCALES = %w(kelvin celsius fahrenheit)
DEFAULT_SCALE = 'kelvin'
MAX_TEMPERATURE = 25 + 273.15
before_action :set_scale
def heat_up
was_heat_up = false
if previous_temperature < next_temperature && next_temperature < MAX_TEMPERATURE
valve.update(degrees: params[:degrees], scale: params[:scale])
Heater.call(next_temperature)
was_heat_up = true
end
render json: { was_heat_up: was_heat_up }
end
private
def previous_temperature
kelvin_degrees_by_scale(valve.degrees, valve.scale)
end
def next_temperature
kelvin_degrees_by_scale(params[:degrees], @scale)
end
def set_scale
@scale = SCALES.include?(params[:scale]) ? params[:scale] : DEFAULT_SCALE
end
def valve
@valve ||= AutomatedThermostaticValve.find(params[:id])
end
def kelvin_degrees_by_scale(degrees, scale)
degrees = degrees.to_f
case scale.to_s
when 'kelvin'
degrees
when 'celsius'
degrees + 273.15
when 'fahrenheit'
(degrees - 32) * 5 / 9 + 273.15
end
end
end
首先,將溫度比較邏輯移到Model中,Controller只需要將參數(shù)傳給`update'方法。但這樣一來,Model就包含了太多溫度轉換的代碼。
class AutomatedThermostaticValvesController < ApplicationController
def heat_up
valve.update(next_degrees: params[:degrees], next_scale: params[:scale])
render json: { was_heat_up: valve.was_heat_up }
end
private
def valve
@valve ||= AutomatedThermostaticValve.find(params[:id])
end
end
class AutomatedThermostaticValve < ActiveRecord::Base
SCALES = %w(kelvin celsius fahrenheit)
DEFAULT_SCALE = 'kelvin'
before_validation :check_next_temperature, if: :next_temperature
after_save :launch_heater, if: :was_heat_up
attr_accessor :next_degrees, :next_scale
attr_reader :was_heat_up
def temperature
kelvin_degrees_by_scale(degrees, scale)
end
def next_temperature
kelvin_degrees_by_scale(next_degrees, next_scale) if next_degrees.present?
end
def max_temperature
kelvin_degrees_by_scale(25, 'celsius')
end
def next_scale=(scale)
@next_scale = SCALES.include?(scale) ? scale : DEFAULT_SCALE
end
private
def check_next_temperature
@was_heat_up = false
if temperature < next_temperature && next_temperature <= max_temperature
@was_heat_up = true
assign_attributes(
degrees: next_degrees,
scale: next_scale,
)
end
@was_heat_up
end
def launch_heater
Heater.call(temperature)
end
def kelvin_degrees_by_scale(degrees, scale)
degrees = degrees.to_f
case scale.to_s
when 'kelvin'
degrees
when 'celsius'
degrees + 273.15
when 'fahrenheit'
(degrees - 32) * 5 / 9 + 273.15
end
end
end
為了讓Model瘦身,我們將創(chuàng)建Value Objects。Value Objects接受溫度數(shù)值和溫度單位作為初始化參數(shù)。在進行比較時,使用<=>操作符比較轉換為開之后的溫度。
同時,Value Object也包含一個to_h方法用于批量賦值。另外,還提供了工廠方法from_kelvin、from_celsius和from_fahrenheit,便于以指定單位創(chuàng)建Temperature對象,如Temperature.from_celsius(0)將會創(chuàng)建一個0°C或273°К的溫度對象。
class AutomatedThermostaticValvesController < ApplicationController
def heat_up
valve.update(next_degrees: params[:degrees], next_scale: params[:scale])
render json: { was_heat_up: valve.was_heat_up }
end
private
def valve
@valve ||= AutomatedThermostaticValve.find(params[:id])
end
end
class AutomatedThermostaticValve < ActiveRecord::Base
before_validation :check_next_temperature, if: :next_temperature
after_save :launch_heater, if: :was_heat_up
attr_accessor :next_degrees, :next_scale
attr_reader :was_heat_up
def temperature
Temperature.new(degrees, scale)
end
def temperature=(temperature)
assign_attributes(temperature.to_h)
end
def next_temperature
Temperature.new(next_degrees, next_scale) if next_degrees.present?
end
private
def check_next_temperature
@was_heat_up = false
if temperature < next_temperature && next_temperature <= Temperature::MAX
self.temperature = next_temperature
@was_heat_up = true
end
end
def launch_heater
Heater.call(temperature.kelvin_degrees)
end
end
class Temperature
include Comparable
SCALES = %w(kelvin celsius fahrenheit)
DEFAULT_SCALE = 'kelvin'
attr_reader :degrees, :scale, :kelvin_degrees
def initialize(degrees, scale = 'kelvin')
@degrees = degrees.to_f
@scale = case scale
when *SCALES then scale
else DEFAULT_SCALE
end
@kelvin_degrees = case @scale
when 'kelvin'
@degrees
when 'celsius'
@degrees + 273.15
when 'fahrenheit'
(@degrees - 32) * 5 / 9 + 273.15
end
end
def self.from_celsius(degrees_celsius)
new(degrees_celsius, 'celsius')
end
def self.from_fahrenheit(degrees_fahrenheit)
new(degrees_celsius, 'fahrenheit')
end
def self.from_kelvin(degrees_kelvin)
new(degrees_kelvin, 'kelvin')
end
def <=>(other)
kelvin_degrees <=> other.kelvin_degrees
end
def to_h
{ degrees: degrees, scale: scale }
end
MAX = from_celsius(25)
end
最終的結果是,Controller和Model同時得到了瘦身。Controller不包含任何與溫度相關的業(yè)務邏輯,Model也不包含任何與溫度轉換相關的邏輯,僅調用了Temperature提供的方法。
3. Form Objects
Form Object模式適用于封裝數(shù)據(jù)校驗和持久化。
示例
假設我們有一個典型Rails Model和Controller用于創(chuàng)建新用戶。
問題分析
Model中包含了所有校驗邏輯,因此不能為其他實體重用,如Admin。
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
render json: @user
else
render json: @user.error, status: :unprocessable_entity
end
end
private
def user_params
params
.require(:user)
.permit(:email, :full_name, :password, :password_confirmation)
end
end
class User < ActiveRecord::Base
EMAIL_REGEX = /@/ # Some fancy email regex
validates :full_name, presence: true
validates :email, presence: true, format: EMAIL_REGEX
validates :password, presence: true, confirmation: true
end
解決方案就是將所有校驗邏輯移到一個單獨負責校驗的類中,可以稱之為UserForm。
class UserForm
EMAIL_REGEX = // # Some fancy email regex
include ActiveModel::Model
include Virtus.model
attribute :id, Integer
attribute :full_name, String
attribute :email, String
attribute :password, String
attribute :password_confirmation, String
validates :full_name, presence: true
validates :email, presence: true, format: EMAIL_REGEX
validates :password, presence: true, confirmation: true
attr_reader :record
def persist
@record = id ? User.find(id) : User.new
if valid?
@record.attributes = attributes.except(:password_confirmation, :id)
@record.save!
true
else
false
end
end
end
現(xiàn)在,就可以在Controller里面像這樣使用它了:
class UsersController < ApplicationController
def create
@form = UserForm.new(user_params)
if @form.persist
render json: @form.record
else
render json: @form.errors, status: :unpocessably_entity
end
end
private
def user_params
params.require(:user)
.permit(:email, :full_name, :password, :password_confirmation)
end
end
最終,用戶Model不在負責校驗數(shù)據(jù):
class User < ActiveRecord::Base
end
4. Query Objects
該模式適用于從Controller和Model中抽取查詢邏輯,并將它封裝到可重用的類。
示例
假設我們請求一個文章列表,查詢條件是類型為video、查看數(shù)大于100并且當前用戶可以訪問。
問題分析
所有查詢邏輯都在Controller中(即所有查詢條件都在Controller中添加)。
- 不可重用
- 難于測試
- 文章Scheme的任何改變都可能影響這段代碼
class Article < ActiveRecord::Base
# t.string :status
# t.string :type
# t.integer :view_count
end
class ArticlesController < ApplicationController
def index
@articles = Article
.accessible_by(current_ability)
.where(type: :video)
.where('view_count > ?', 100)
end
end
重構的第一步就是封裝查詢條件,提供簡潔的API接口。在Rails中,可以使用scope實現(xiàn):
class Article < ActiveRecord::Base
scope :with_video_type, -> { where(type: :video) }
scope :popular, -> { where('view_count > ?', 100) }
scope :popular_with_video_type, -> { popular.with_video_type }
end
現(xiàn)在就可以使用這些簡潔的API接口來查詢,而不用關心底層是如何實現(xiàn)的。如果article的scheme發(fā)生了改變,僅需要修改article類即可。
class ArticlesController < ApplicationController
def index
@articles = Article
.accessible_by(current_ability)
.popular_with_video_type
end
end
看起來不錯,不過又有一些新問題出現(xiàn)了。首先,需要為每個想要封裝的查詢條件創(chuàng)建scope,最終會導致Model中充斥諸多針對不同應用場景的scope組合。其次,scope不能在不同的model中重用,比如不用使用Article的scope來查詢Attachment。最后,將所有查詢相關的邏輯都塞到Article類中也違反了單一原則。解決方案是使用Query Object。
class PopularVideoQuery
def call(relation)
relation
.where(type: :video)
.where('view_count > ?', 100)
end
end
class ArticlesController < ApplicationController
def index
relation = Article.accessible_by(current_ability)
@articles = PopularVideoQuery.new.call(relation)
end
end
哈,這樣就可以做到重用了!現(xiàn)在可以將它用于查詢任何具有相似scheme的類了:
class Attachment < ActiveRecord::Base
# t.string :type
# t.integer :view_count
end
PopularVideoQuery.new.call(Attachment.all).to_sql
# "SELECT \"attachments\".* FROM \"attachments\" WHERE \"attachments\".\"type\" = 'video' AND (view_count > 100)"
PopularVideoQuery.new.call(Article.all).to_sql
# "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"type\" = 'video' AND (view_count > 100)"
如果想進一步支持鏈式調用的話,也很簡單。只需要讓call方法遵循ActiveRecord::Relation接口即可:
class BaseQuery
def |(other)
ChainedQuery.new do |relation|
other.call(call(relation))
end
end
end
class ChainedQuery < BaseQuery
def initialize(&block)
@block = block
end
def call(relation)
@block.call(relation)
end
end
class WithStatusQuery < BaseQuery
def initialize(status)
@status = status
end
def call(relation)
relation.where(status: @status)
end
end
query = WithStatusQuery.new(:published) | PopularVideoQuery.new
query.call(Article.all).to_sql
# "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"status\" = 'published' AND \"articles\".\"type\" = 'video' AND (view_count > 100)"
現(xiàn)在,我們得到了一個封裝所有查詢邏輯,可重用,提供簡潔接口并易于測試的類。
5. View Objects (Serializer/Presenter)
View Object適用于將View中的數(shù)據(jù)及相關計算從Controller和Model抽離出來,如一個網(wǎng)站的HTML頁面或API終端請求的JSON響應。
示例
View中一般通常存在以下計算:
- 根據(jù)服務器協(xié)議和圖片路徑創(chuàng)建圖片URL
- 獲取文章的標題和描述,如果沒有返回默認值
- 連接姓和名來顯示全名
- 用合適的方式顯示文章的創(chuàng)建日期
問題分析
View中包含了太多計算邏輯。
# 重構之前
#/app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
end
#/app/views/articles/show.html.erb
<% content_for :header do %>
<title>
<%= @article.title_for_head_tag || I18n.t('default_title_for_head') %>
</title>
<meta name='description' content="<%= @article.description_for_head_tag || I18n.t('default_description_for_head') %>">
<meta property="og:type" content="article">
<meta property="og:title" content="<%= @article.title %>">
<% if @article.description_for_head_tag %>
<meta property="og:description" content="<%= @article.description_for_head_tag %>">
<% end %>
<% if @article.image %>
<meta property="og:image" content="<%= "#{request.protocol}#{request.host_with_port}#{@article.main_image}" %>">
<% end %>
<% end %>
<% if @article.image %>
<%= image_tag @article.image.url %>
<% else %>
<%= image_tag 'no-image.png'%>
<% end %>
<h1>
<%= @article.title %>
</h1>
<p>
<%= @article.text %>
</p>
<% if @article.author %>
<p>
<%= "#{@article.author.first_name} #{@article.author.last_name}" %>
</p>
<%end%>
<p>
<%= t('date') %>
<%= @article.created_at.strftime("%B %e, %Y")%>
</p>
為了解決這個問題,可以先創(chuàng)建一個presenter基類,然后再創(chuàng)建一個ArticlePresenter類的實例。ArticlePresenter方法根據(jù)適當?shù)挠嬎惴祷叵胍臉撕灐?/p>
#/app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
end
end
#/app/presenters/base_presenter.rb
class BasePresenter
def initialize(object, template)
@object = object
@template = template
end
def self.presents(name)
define_method(name) do
@object
end
end
def h
@template
end
end
#/app/helpers/application_helper.rb
module ApplicationHelper
def presenter(model)
klass = "#{model.class}Presenter".constantize
presenter = klass.new(model, self)
yield(presenter) if block_given?
end
end
#/app/presenters/article_presenters.rb
class ArticlePresenter < BasePresenter
presents :article
delegate :title, :text, to: :article
def meta_title
title = article.title_for_head_tag || I18n.t('default_title_for_head')
h.content_tag :title, title
end
def meta_description
description = article.description_for_head_tag || I18n.t('default_description_for_head')
h.content_tag :meta, nil, content: description
end
def og_type
open_graph_meta "article", "og:type"
end
def og_title
open_graph_meta "og:title", article.title
end
def og_description
open_graph_meta "og:description", article.description_for_head_tag if article.description_for_head_tag
end
def og_image
if article.image
image = "#{request.protocol}#{request.host_with_port}#{article.main_image}"
open_graph_meta "og:image", image
end
end
def author_name
if article.author
h.content_tag :p, "#{article.author.first_name} #{article.author.last_name}"
end
end
def image
if article.image
h.image_tag article.image.url
else
h.image_tag 'no-image.png'
end
end
private
def open_graph_meta content, property
h.content_tag :meta, nil, content: content, property: property
end
end
現(xiàn)在View中不包含任何與計算相關的邏輯,所有組件都抽離到了presenter中,并可在其他View中重用,如下:
#/app/views/articles/show.html.erb
<% presenter @article do |article_presenter| %>
<% content_for :header do %>
<%= article_presenter.meta_title %>
<%= article_presenter.meta_description %>
<%= article_presenter.og_type %>
<%= article_presenter.og_title %>
<%= article_presenter.og_description %>
<%= article_presenter.og_image %>
<% end %>
<%= article_presenter.image%>
<h1> <%= article_presenter.title %> </h1>
<p> <%= article_presenter.text %> </p>
<%= article_presenter.author_name %>
<% end %>
6. Policy Objects
Policy Object模式與Service Object模式相似,前者負責讀操作,后者負責寫操作。Policy Object模式適用于封裝復雜的業(yè)務規(guī)則,并易于替換。比如,可以使用一個訪客Policy Object來識別一個訪客是否可以訪問某些特定資源。當用戶是管理員時,可以很方便的將訪客Policy Object替換為包含管理員規(guī)則的管理員Policy Object。
示例
在用戶創(chuàng)建一個項目之前,Controller將檢查當前用戶是否為管理者,是否有權限創(chuàng)建項目,當前用戶項目數(shù)量是否小于最大值,以及在Redis中是否存在阻塞的項目創(chuàng)建。
問題分析
- 自由Controller知道項目創(chuàng)建的規(guī)則
- Controller包含了額外的邏輯代碼
class ProjectsController < ApplicationController
def create
if can_create_project?
@project = Project.create!(project_params)
render json: @project, status: :created
else
head :unauthorized
end
end
private
def can_create_project?
current_user.manager? &&
current_user.projects.count < Project.max_count &&
redis.get('projects_creation_blocked') != '1'
end
def project_params
params.require(:project).permit(:name, :description)
end
def redis
Redis.current
end
end
class User < ActiveRecord::Base
enum role: [:manager, :employee, :guest]
end
為了讓Controller瘦身,可以將規(guī)則代碼移到Model中。所有檢查邏輯都將移出Controller。但是這樣一來,User類就知道了Redis和Project類的邏輯。并且Model也變胖了。
class User < ActiveRecord::Base
enum role: [:manager, :employee, :guest]
def can_create_project?
manager? &&
projects.count < Project.max_count &&
redis.get('projects_creation_blocked') != '1'
end
private
def redis
Redis.current
end
end
class ProjectsController < ApplicationController
def create
if current_user.can_create_project?
@project = Project.create!(project_params)
render json: @project, status: :created
else
head :unauthorized
end
end
private
def project_params
params.require(:project).permit(:name, :description)
end
end
在這種情況下,可以將這些規(guī)則抽取到一個Policy Object中,從而使Controller和Model同時瘦身。
class CreateProjectPolicy
def initialize(user, redis_client)
@user = user
@redis_client = redis_client
end
def allowed?
@user.manager? && below_project_limit && !project_creation_blocked
end
private
def below_project_limit
@user.projects.count < Project.max_count
end
def project_creation_blocked
@redis_client.get('projects_creation_blocked') == '1'
end
end
class ProjectsController < ApplicationController
def create
if policy.allowed?
@project = Project.create!(project_params)
render json: @project, status: :created
else
head :unauthorized
end
end
private
def project_params
params.require(:project).permit(:name, :description)
end
def policy
CreateProjectPolicy.new(current_user, redis)
end
def redis
Redis.current
end
end
def User < ActiveRecord::Base
enum role: [:manager, :employee, :guest]
end
最終的結果是一個干凈的Controller和Model。Policy Object封裝了所有權限檢查邏輯,并且所有外部依賴都從Controller注入到Policy Object中。所有的類都各司其職。
7. Decorators
Decorator模式允許我們給某個類的實例添加各種輔助行為,而不影響相同類的其他實例。該設計模式廣泛用于在不同類之間劃分功能,也可以用來替代繼承以遵循單一原則。
示例
假設View中存在許多計算:
- 根據(jù)
title_for_head是否有值顯示不同的標題 - 如果缺少車圖片,那么顯示一張默認的車圖片
- 如果車的品牌、類型、說明、車主、城市和聯(lián)系電話未設置時,顯示默認值
- 展示車的狀態(tài)
- 顯示格式化后的車的創(chuàng)建日期
問題分析
View中包含了過多的計算邏輯:
#/app/controllers/cars_controller.rb
class CarsController < ApplicationController
def show
@car = Car.find(params[:id])
end
end
#/app/views/cars/show.html.erb
<% content_for :header do %>
<title>
<% if @car.title_for_head %>
<%="#{ @car.title_for_head } | #{t('beautiful_cars')}" %>
<% else %>
<%= t('beautiful_cars') %>
<% end %>
</title>
<% if @car.description_for_head%>
<meta name='description' content= "#{<%= @car.description_for_head %>}">
<% end %>
<% end %>
<% if @car.image %>
<%= image_tag @car.image.url %>
<% else %>
<%= image_tag 'no-images.png'%>
<% end %>
<h1>
<%= t('brand') %>
<% if @car.brand %>
<%= @car.brand %>
<% else %>
<%= t('undefined') %>
<% end %>
</h1>
<p>
<%= t('model') %>
<% if @car.model %>
<%= @car.model %>
<% else %>
<%= t('undefined') %>
<% end %>
</p>
<p>
<%= t('notes') %>
<% if @car.notes %>
<%= @car.notes %>
<% else %>
<%= t('undefined') %>
<% end %>
</p>
<p>
<%= t('owner') %>
<% if @car.owner %>
<%= @car.owner %>
<% else %>
<%= t('undefined') %>
<% end %>
</p>
<p>
<%= t('city') %>
<% if @car.city %>
<%= @car.city %>
<% else %>
<%= t('undefined') %>
<% end %>
</p>
<p>
<%= t('owner_phone') %>
<% if @car.phone %>
<%= @car.phone %>
<% else %>
<%= t('undefined') %>
<% end %>
</p>
<p>
<%= t('state') %>
<% if @car.used %>
<%= t('used') %>
<% else %>
<%= t('new') %>
<% end %>
</p>
<p>
<%= t('date') %>
<%= @car.created_at.strftime("%B %e, %Y")%>
</p>
可以使用Draper這個裝飾gem,將所有邏輯抽取到CarDecorator中。
#/app/controllers/cars_controller.rb
class CarsController < ApplicationController
def show
@car = Car.find(params[:id]).decorate
end
end
#/app/decorators/car_decorator.rb
class CarDecorator < Draper::Decorator
delegate_all
def meta_title
result =
if object.title_for_head
"#{ object.title_for_head } | #{I18n.t('beautiful_cars')}"
else
t('beautiful_cars')
end
h.content_tag :title, result
end
def meta_description
if object.description_for_head
h.content_tag :meta, nil ,content: object.description_for_head
end
end
def image
result = object.image.url.present? ? object.image.url : 'no-images.png'
h.image_tag result
end
def brand
get_info object.brand
end
def model
get_info object.model
end
def notes
get_info object.notes
end
def owner
get_info object.owner
end
def city
get_info object.city
end
def owner_phone
get_info object.phone
end
def state
object.used ? I18n.t('used') : I18n.t('new')
end
def created_at
object.created_at.strftime("%B %e, %Y")
end
private
def get_info value
value.present? ? value : t('undefined')
end
end
改造后不包含任何計算的整潔View:
#/app/views/cars/show.html.erb
<% content_for :header do %>
<%= @car.meta_title %>
<%= @car.meta_description%>
<% end %>
?
<%= @car.image %>
<h1> <%= t('brand') %> <%= @car.brand %> </h1>
<p> <%= t('model') %> <%= @car.model %> </p>
<p> <%= t('notes') %> <%= @car.notes %> </p>
<p> <%= t('owner') %> <%= @car.owner %> </p>
<p> <%= t('city') %> <%= @car.city %> </p>
<p> <%= t('owner_phone') %> <%= @car.phone %> </p>
<p> <%= t('state') %> <%= @car.state %> </p>
<p> <%= t('date') %> <%= @car.created_at%> </p>
總結
相信如上這些概念將有助于你了解在何時以及如何重構代碼。這些工具可以幫助你有效的管理代碼的復雜度。其實,在開發(fā)的最初就應該小心地規(guī)劃代碼邏輯的組織,這樣就可以避免之后在重構上花費大量時間。