当 Controller 中的 action 有以下症状时适用:
以下示例中,主要工作由外部 Stripe 服务完成。该服务基于邮件地址和来源创建 Stripe 客户,并将所有服务费用绑定到该客户的账号上。
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 只负责成功支付和失败支付时的页面跳转。
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
提供的方法。
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
该模式适用于从 Controller 和 Model 中抽取查询逻辑,并将它封装到可重用的类。
假设我们请求一个文章列表,查询条件是类型为 video、查看数大于 100 并且当前用户可以访问。
所有查询逻辑都在 Controller 中(即所有查询条件都在 Controller 中添加)。
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)"
现在,我们得到了一个封装所有查询逻辑,可重用,提供简洁接口并易于测试的类。
View Object 适用于将 View 中的数据及相关计算从 Controller 和 Model 抽离出来,如一个网站的 HTML 页面或 API 终端请求的 JSON 响应。
View 中一般通常存在以下计算:
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 %>
Policy Object 模式与 Service Object 模式相似,前者负责读操作,后者负责写操作。Policy Object 模式适用于封装复杂的业务规则,并易于替换。比如,可以使用一个访客 Policy Object 来识别一个访客是否可以访问某些特定资源。当用户是管理员时,可以很方便的将访客 Policy Object 替换为包含管理员规则的管理员 Policy Object。
在用户创建一个项目之前,Controller 将检查当前用户是否为管理者,是否有权限创建项目,当前用户项目数量是否小于最大值,以及在 Redis 中是否存在阻塞的项目创建。
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 中。所有的类都各司其职。
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>
相信如上这些概念将有助于你了解在何时以及如何重构代码。这些工具可以帮助你有效的管理代码的复杂度。其实,在开发的最初就应该小心地规划代码逻辑的组织,这样就可以避免之后在重构上花费大量时间。