- [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 (如, 删除过期数据)
- 使用多个model(如,从一个文件中导入数据到多个model)
示例
以下示例中,主要工作由外部Stripe服务完成。该服务基于邮件地址和来源创建Stripe客户,并将所有服务费用绑定到该客户的账号上。
问题分析
- Controller中包含调用外部服务的代码
- Controller负责构建调用外部服务所需的数据
- 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
来负责客户账号的创建和支付,从而解决了Controller中业务代码过多的问题。但是,还有一个问题需要解决。如果外部服务抛出异常时(如,信用卡无效)该如何处理,需要重定向的其他页面吗?
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
,并捕获可能产生的异常。Interactor模式常用于封装业务逻辑,每个Interactor一般只描述一条业务逻辑。
Interactor模式通过简单Ruby对象(plain old Ruby objects, POROs)可以帮助我们实现单一原则(Single Responsibility Principle, SRP)。Interactor与Service Object类似,只是通常会返回执行状态及相关信息,而且一般会在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设计模式推崇简洁的对象(仅包含一些给定的值),并支持根据给定的逻辑,或基于指定的属性进行对象间相互比较(不基于id)。Value Object的例子如,以不同币种表示的货币。我们可以用一个币种(如,美元)来比较这些对象。同样,Value Object也可以用于表示温度,并可用单位开来进行比较。
示例
假设有一所带电加热的智能房子,加热器可以通过网络接口加以控制。Controller的一个方法将从温度传感器那里收到指定加热器的参数:温度数值和温度单位(华氏、摄氏或开)。如果是其他温度单位,一律先转换为开。然后,检查温度是否小于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只需要将参数传给`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瘦身,我们将创建Value Objects。Value Objects接受温度数值和温度单位作为初始化参数。在进行比较时,使用<=>
操作符比较转换为开之后的温度。
同时,Value Object也包含一个to_h
方法用于批量赋值。另外,还提供了工厂方法from_kelvin
、from_celsius
和from_fahrenheit
,便于以指定单位创建Temperature
对象,如Temperature.from_celsius(0)
将会创建一个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不包含任何与温度相关的业务逻辑,Model也不包含任何与温度转换相关的逻辑,仅调用了Temperature
提供的方法。
3. Form Objects
Form Object模式适用于封装数据校验和持久化。
示例
假设我们有一个典型Rails Model和Controller用于创建新用户。
问题分析
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
现在,就可以在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不在负责校验数据:
class User < ActiveRecord::Base
end
4. Query Objects
该模式适用于从Controller和Model中抽取查询逻辑,并将它封装到可重用的类。
示例
假设我们请求一个文章列表,查询条件是类型为video、查看数大于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实现:
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
现在就可以使用这些简洁的API接口来查询,而不用关心底层是如何实现的。如果article的scheme发生了改变,仅需要修改article类即可。
class ArticlesController < ApplicationController
def index
@articles = Article
.accessible_by(current_ability)
.popular_with_video_type
end
end
看起来不错,不过又有一些新问题出现了。首先,需要为每个想要封装的查询条件创建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
哈,这样就可以做到重用了!现在可以将它用于查询任何具有相似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)"
现在,我们得到了一个封装所有查询逻辑,可重用,提供简洁接口并易于测试的类。
5. View Objects (Serializer/Presenter)
View Object适用于将View中的数据及相关计算从Controller和Model抽离出来,如一个网站的HTML页面或API终端请求的JSON响应。
示例
View中一般通常存在以下计算:
- 根据服务器协议和图片路径创建图片URL
- 获取文章的标题和描述,如果没有返回默认值
- 连接姓和名来显示全名
- 用合适的方式显示文章的创建日期
问题分析
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>
为了解决这个问题,可以先创建一个presenter基类,然后再创建一个ArticlePresenter
类的实例。ArticlePresenter
方法根据适当的计算返回想要的标签。
#/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
现在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模式适用于封装复杂的业务规则,并易于替换。比如,可以使用一个访客Policy Object来识别一个访客是否可以访问某些特定资源。当用户是管理员时,可以很方便的将访客Policy Object替换为包含管理员规则的管理员Policy Object。
示例
在用户创建一个项目之前,Controller将检查当前用户是否为管理者,是否有权限创建项目,当前用户项目数量是否小于最大值,以及在Redis中是否存在阻塞的项目创建。
问题分析
- 自由Controller知道项目创建的规则
- 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瘦身,可以将规则代码移到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
在这种情况下,可以将这些规则抽取到一个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中存在许多计算:
- 根据
title_for_head
是否有值显示不同的标题 - 如果缺少车图片,那么显示一张默认的车图片
- 如果车的品牌、类型、说明、车主、城市和联系电话未设置时,显示默认值
- 展示车的状态
- 显示格式化后的车的创建日期
问题分析
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>
总结
相信如上这些概念将有助于你了解在何时以及如何重构代码。这些工具可以帮助你有效的管理代码的复杂度。其实,在开发的最初就应该小心地规划代码逻辑的组织,这样就可以避免之后在重构上花费大量时间。