Статьи

7 Шаблоны проектирования для рефакторинга компонентов MVC в Rails

dphead

В нашем предыдущем посте «Основы MVC в Rails» мы обсуждали теоретические аспекты шаблона проектирования MVC. Мы определили, что означает MVC, определили, за что отвечает каждый компонент MVC, обратились к тому, что происходит, когда компонент содержит избыточную логику, и, самое главное, мы ввели концепцию рефакторинга. В этой статье, как и было обещано, мы покажем вам каждый шаблон дизайна на работе.

Сервисные объекты (и объекты Interactor)

Сервисные объекты создаются, когда действие:

  • является сложным (например, расчет зарплаты сотрудника)
  • использует API внешних сервисов
  • явно не относится ни к одной модели (например, удаление устаревших данных)
  • использует несколько моделей (например, импорт данных из одного файла в несколько моделей)

пример

В приведенном ниже примере работа выполняется внешним сервисом Stripe. Служба Stripe создает клиента Stripe на основе адреса электронной почты и источника (токена) и связывает любые платежи за услуги с этой учетной записью.

проблема

  • Логика работы с внешним сервисом находится в контроллере.
  • Контроллер формирует данные для внешнего сервиса.
  • Сложно поддерживать и масштабировать контроллер.
class ChargesController 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 который отвечает за создание и оплату аккаунта клиента. Но, решив проблему слишком большого количества логики в контроллере, мы все еще должны решить еще одну проблему. Что произойдет, если внешняя служба выдает исключение (например, если кредитная карта недействительна), и мы должны перенаправить пользователя на другую страницу?

 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 

Чтобы справиться с этим сценарием, мы включили вызов CheckoutService и перехватили исключения с помощью объекта Interactor. Интеракторы используются для инкапсуляции бизнес-логики. Каждый интерактор обычно описывает одно бизнес-правило.

Шаблон Interactor помогает нам достичь принципа единой ответственности (SRP), используя простые старые объекты Ruby (PORO), оставляя модели ответственными только на уровне постоянства. Интеракторы похожи на Service Objects, но обычно возвращают несколько значений, которые показывают состояние выполнения и другую информацию (в дополнение к выполнению действий). Также обычной практикой является использование Объектов Сервиса внутри Объектов Interactor. Вот пример использования этого шаблона проектирования:

 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 

Перемещая все исключения, связанные с ошибками карты, мы можем добиться худого Контроллера. Теперь Контроллер отвечает только за перенаправление пользователя на страницы для успешной оплаты или неудачной оплаты.

Объекты значения

Шаблон проектирования Value Object поддерживает простые маленькие объекты (которые обычно содержат только заданные значения) и позволяет сравнивать эти объекты в соответствии с заданной логикой или просто на основе определенных атрибутов (а не их идентичности). Примерами объектов значений являются объекты, представляющие денежные значения в различных валютах. Затем мы могли бы сравнить эти объекты стоимости, используя одну валюту (например, USD) Значимые объекты могут также представлять температуры и сравниваться, например, с использованием шкалы Кельвина.

пример

Давайте предположим, что у нас есть умный дом с электрическим нагревом, а обогреватели управляются через веб-интерфейс. Действие контроллера получает параметры для данного нагревателя от датчика температуры: температуру (в виде числового значения) и температурную шкалу (по Фаренгейту, Цельсию или Кельвин). Эта температура преобразуется в Кельвин, если она указана в другой шкале, и Контроллер проверяет, равна ли температура ниже 25 ° C и равна ли она или превышает текущую температуру.

проблема

Контроллер содержит слишком много логики, связанной с преобразованием и сравнением значений температуры.

 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 

Мы переместили логику сравнения температуры в модель, поэтому контроллер просто передает параметры в метод update . Но Модель все еще не идеальна — она ​​слишком много знает о том, как справляться с температурными преобразованиями.

 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 

Чтобы сделать Модель тонкой, мы создаем объекты-значения. При инициализации объекты-значения принимают значения градусов и масштаба. При сравнении этих объектов метод космического корабля ( <=> ) сравнивает их температуру в пересчете на Кельвин.

Объекты полученных значений также содержат метод 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 

Результатом является тощий контроллер и тощая модель. Контроллер ничего не знает о логике, связанной с температурой, а Модель также ничего не знает о преобразованиях температуры, используя только методы объектов значений температуры.

Объекты формы

Form Object — это шаблон проектирования, который инкапсулирует логику, связанную с проверкой и сохранением данных.

пример

Предположим, у нас есть типичное действие Rails Model and Controller для создания новых пользователей.

проблема

Модель содержит всю логику проверки, поэтому ее нельзя использовать для других объектов, например, 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 

После того, как мы переместим логику проверки в UserForm , мы можем использовать ее внутри Контроллера следующим образом:

 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 

В результате пользовательская модель больше не отвечает за проверку данных:

 class User < ActiveRecord::Base end 

Объекты запроса

Query Object — это шаблон проектирования, который позволяет извлекать логику запросов из контроллеров и моделей в повторно используемые классы.

пример

Мы хотим запросить список статей с типом «видео», количество просмотров которых превышает 100, и которые могут быть доступны текущему пользователю.

проблема

Вся логика запросов находится в контроллере (все условия запроса налагаются в контроллере);

  • Эта логика не может быть повторно использована
  • Это сложно проверить
  • Любые изменения в схеме статьи могут нарушить этот код
 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 мы можем сделать это, создав области:

 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 для запроса всего, что нам нужно, не беспокоясь о базовой реализации. Если наша схема статьи изменится, нам нужно будет только внести изменения в класс статьи:

 class ArticlesController < ApplicationController def index @articles = Article .accessible_by(current_ability) .popular_with_video_type end end 

Это намного лучше, но теперь возникают новые проблемы. Нам нужно создать области для каждого условия запроса, которое мы хотим инкапсулировать, увеличивая Модель с различными комбинациями областей действия для разных вариантов использования. Другая проблема заключается в том, что области не могут повторно использоваться в разных моделях, что означает, что вы не можете просто использовать область действия из класса Article для запроса класса Attachment. Мы также нарушаем принцип единой ответственности, отбрасывая все связанные с запросами обязанности в класс Article. Решением этих проблем является использование объекта Query.

 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 

Вау, это многоразово! Теперь мы можем использовать этот класс для запроса любых других репозиториев, имеющих похожую схему:

 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 позволяет нам получать данные и расчеты, которые необходимы только для отображения представления модели в представлении, такого как HTML-страница для веб-сайта или ответ JSON от конечной точки API, из контроллера и модели.

пример

В представлении происходят различные действия (расчеты). Вид:

  • Создает URL изображения из хоста протокола и пути к изображению
  • Принимает название статьи и описание; если нет никаких пользовательских значений, он принимает значения по умолчанию
  • Объединяет имя и фамилию для отображения полного имени
  • Применяет правильное форматирование для даты создания статьи

проблема

Представление содержит слишком много логики вычислений.

 #before refactoring #/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> 

Чтобы решить эту проблему, мы создаем базовый класс презентатора, а затем создаем ArticlePresenter класса ArticlePresenter . Методы ArticlePresenter возвращают нужные теги с соответствующими вычислениями:

 #/app/controllers/articles_controller.rb class ArticlesController 

Большой! У нас есть представление, которое не содержит никакой логики, связанной с вычислениями, и все компоненты были перемещены в презентатор и могут использоваться в других представлениях следующим образом:

 #/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 %> 

Объекты политики

Шаблон проектирования объектов политики аналогичен объектам службы, но отвечает за операции чтения, а объекты службы отвечают за операции записи. Объекты политики инкапсулируют сложные бизнес-правила и могут быть легко заменены другими объектами политики с другими правилами. Например, мы можем проверить, может ли гостевой пользователь получить определенные ресурсы, используя гостевой объект политики. Если пользователь является администратором, мы можем легко изменить этот гостевой объект политики на объект политики администратора, который содержит правила администратора.

пример

Перед тем, как пользователь создает проект, Контроллер проверяет, является ли текущий пользователь менеджером, есть ли у него разрешение на создание проекта, не превышает ли число текущих пользовательских проектов максимальное количество, и проверяет наличие блоков при создании проектов. в хранилище ключей / значений 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 def User < ActiveRecord::Base enum role: [:manager, :employee, :guest] end 

Чтобы сделать контроллер более тонким, мы переместили логику политики в модель. В результате проверка полностью перемещается из контроллера. Но теперь класс User знает о логике класса Redis и Project, и поэтому модель становится толстой.

 def 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 

В этом случае мы можем сделать Модель и Контроллер тонкими, переместив эту логику в объект политики:

 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 

Результатом является чистый контроллер и модель. Объект политики инкапсулирует логику проверки разрешений, и все внешние зависимости внедряются из контроллера в объект политики. Все классы делают свою работу, а не другую.

Декораторы

Шаблон Decorator позволяет нам добавлять любые виды вспомогательного поведения к отдельным объектам, не затрагивая другие объекты того же класса. Этот шаблон проектирования широко используется для разделения функциональности между различными классами и является хорошей альтернативой подклассам для соблюдения принципа единой ответственности.

пример

Давайте предположим, что в представлении происходит много действий (вычислений):

  • Заголовок отображается по-разному в зависимости от наличия значения title_for_head
  • В представлении отображается изображение автомобиля по умолчанию, если не указан URL-адрес настраиваемого изображения автомобиля
  • В представлении отображается текст по умолчанию, когда значения для бренда, модели, заметок, владельца, города и телефона владельца не определены.
  • Вид показывает текстовое представление состояния автомобиля
  • Вид отображает отформатированную дату создания объекта автомобиля

проблема

Представление содержит слишком много логики вычислений.

 #/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, который перемещает всю логику в методы 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 

Результатом является аккуратный вид без каких-либо расчетов:

 #/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>  #/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> 

Завершение

Эти концепции должны дать вам базовое понимание того, когда и как вы можете реорганизовать свой код. Есть много инструментов для управления сложностью кода. Тщательно разместив свою логику с самого начала разработки, вы можете сократить количество времени, которое вам нужно потратить на рефакторинг.