Управление сложностью в проектах на ruby on rails. Часть 1

от автора

В этой серии статей я соберу бОльшую часть своего опыта разработки на Ruby on Rails. Эти методики позволяют контролировать сложность и облегчают сопровождение проекта. Большинство из них придумал не я, и, по возможности, буду указывать источник.

Основная проблема проектов на RoR в том, что, как правило, всю логику пытаются уместить в модели, контроллеры и представления. Т.е. код находится только в моделях(ActiveRecord::Base), контроллерах, хэлперах и шаблонах. Такой подход приводит к печальным последствиям: код становится запутанным, долго делаются фичи, появляются регрессии, у разработчиков пропадает мотивация. В качестве примера можно посмотреть на исходники redmine.

Выход из данной ситуации довольно-таки очевидный. Будем делать проекты не на ruby on rails, а с использованием ruby on rails. Как это будет выглядеть: мы никуда не уходим от MVC и Rails, просто пересмотрим Model, View, Controller. Для начала расширим понятие модели. Модель — это не просто класс-наследник ORM. Модель — это вся бизнес логика приложения. Модель включает в себя: модели, сервисы, политики, репозитории, формы и другие элементы, которые я опишу далее. Так же расширим представления. Представления — это шаблоны, презентеры, хелперы, билдеры форм. Контроллеры — это все то, что связано с обработкой запросов: контроллеры, responders.

Кроме этих методик пригодятся знания по SOLID, ruby style guide, rails conventions, ruby object model, ruby metaprogramming, основным паттернам.

Helpers

Самый простой совет — используйте хэлперы. С помощью них удобно описывать частые операции:

module ApplicationHelper   def menu_item(model, action, name, url, link_options = {})     return unless policy(model).send "#{action}?"     content_tag :li do       link_to name, url, link_options     end   end end  # _nav.haml = menu_item current_user, :show, t(:show_profile), user_path(current_user) = menu_item current_user, :edit, t(:edit_profile), edit_user_path(current_user) 

Хэлпер menu_item отображает элемент меню в зависимости от политик. Можно расширить этот хэлпер, и он будет выделять активный элемент меню.

module ApplicationHelper   def han(model, attribute)     model.to_s.classify.constantize.human_attribute_name(attribute)   end    def show_attribute(model, attribute)     value = model.send(attribute)     return if value.blank?     [         content_tag(:dt, han(model.model_name, attribute)),         content_tag(:dd, value)     ].join.html_safe   end end  # show.haml  = show_attribute user_presenter, :name  = show_attribute user_presenter, :role_text  = show_attribute user_presenter, :profile_image 

Хэлпер show_attribute печатает название атрибута и его значение, если значение есть.

Form templates

= simple_form_for @user, builder: PunditFormBuilder do |f|   = f.input :name   = f.input :contacts, as: :big_textarea   # some other inputs   = f.button :submit 

Я использую gem simple_form для рендеринга форм. Этот гем берет на себя всю работу по отображению форм. Понятно, что в случае нестандартных дизайнерских форм этот гем не сработает, но для стандартных форм он подходит отлично.

При построении формы я указываю только необходимое: список полей и их тип. Тексты для labels, placeholders, submit подставляются автоматически — достаточно прописать в файле перевода правильные ключи:

ru:   attributes:     created_at: Создано   activerecord:     attributes:       user:         name: Имя   helpers:     submit:       create: Сохранить 

Теперь подробнее про свои inputs.
Например, все текстовые формы должны содержать минимум 10 строк:

class BigTextareaInput < SimpleForm::Inputs::TextInput   def input_html_options     { rows: 10 }   end end 

Это очень простой пример, инпуты могут быть гораздо сложнее. Например, выбор, в какое состояние можно перевести модель (gem state_machines).

Так же SimpleForm позволяет подключать свои билдеры форм:

class PunditFormBuilder < SimpleForm::FormBuilder   def input(attribute_name, options = {}, &block)     return unless show_attribute? attribute_name     super(attribute_name, options, &block)   end    def show_attribute?(attr_name)     # some code   end end  = simple_form_for @user, builder: PunditFormBuilder do |f| 

PunditFormBuilder отвечает за отображение только тех полей, к которым имеет доступ текущий пользователь приложения. Более подробно я расскажу об этом в главе про ACL.

Serializers

Давайте теперь рассмотрим более специфическую задачу, а именно проектирование http json api. Вот наиболее простые способы:

  • метод Model#to_json
  • метод конроллера serialize_model

Все эти способы противоречат принципу единственной ответственности и паттерну MVC. Модель и конроллер не должны заниматься отображением — это обязанность представлений.

Я вижу 2 способа решения:

  • шаблоны jbuilder
  • serializers, причем как одноименный gem, так и просто объекты-сериализаторы (сериалайзеры?)
class CommentSerializer < ActiveModel::Serializer   attributes :name, :body    belongs_to :post end 

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

Presenters

Так мы плавно подошли к следующему подходу: использование презентеров. В rails они используются как дополнение хэлперов.

gem drapper внес путаницу: его разработчики назвали презентеры декораторами. Хотя эти паттерны похожи, они имеют значительное различие: декораторы не изменяют интерфейс. Так же с этим гемом есть много проблем (можно посмотреть на список issues).

Я нашел простой, элегантный и понятный способ, как реализовать презентеры. Ниже я опишу свою реализацию.

# app/presenters/base_presenter.rb class BasePresenter < Delegator   attr_reader :model, :h   alias_method :__getobj__, :model    def initialize(model, view_context)     @model = model     @h = view_context   end    def inspect     "#<#{self.class} model: #{model.inspect}>"   end end 

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

Кроме модели презентер содежит view_context, который для удобства назван ‘h’.
Это self, доступный в helpers и views. Соответственно, в презентерах можно использовать все хэлперы.

# app/presenters/task_presenter.rb class TaskPresenter < BasePresenter   def to_link     h.link_to model.to_s, model   end    def description     h.markdown model.description   end    # оборачиваем связь   def users     model.users.map { |user| h.present user }   end end 
# app/helpers/application_helper.rb def present(model)   return if model.blank?   klass = "#{model.class}Presenter".constantize   presenter = klass.new(model, self)   yield(presenter) if block_given?   presenter end 

Хэлпер present передает объект-презентер в блок или как результат.
Передачу через блок удобно использовать в шаблонах:

# app/views/web/tasks/index.haml - @tasks.each do |task|   %tr     - present task do |task_presenter|       %td= task_presenter.id       %td= task_presenter.to_link       %td= task_presenter.project 

Похожий подход можно использовать, если у вас очень сложная логика отображения и хэлпереры не помогают. Или отсутствует объект для отобрадения. Например, вывод сложных меню или расписаний событий.

class MenuRenderer   attr_reader :h    def initialize(view_context)     @h = view_context   end    def render     some_hard_logic   end    private   def some_hard_logic     h.link_to '', ''   end end 

В этой части я рассмотрел способы организации логики представлений. В следующей я покажу, как можно организовать логику контроллеров. В последующих — расскажу про модели. А именно: form-objects, services, ACL, query-objects, взаимодействие с различными хранилищами.

ссылка на оригинал статьи http://habrahabr.ru/post/266761/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *