In this series of articles, I will gather most of my experience with Ruby on Rails. These techniques allow you to control the complexity and facilitate project maintenance. Most of them were not invented by me, and, if possible, I will indicate the source.
The main problem of RoR projects is that, as a rule, they try to fit all the logic into models, controllers and views. Those. the code is only in models (ActiveRecord :: Base), controllers, helper and templates. Such an approach leads to sad consequences: the code becomes confusing, features are made for a long time, regressions appear, developers lose motivation. As an example, you can look at the source code
redmine .
The way out of this situation is pretty obvious. We will do projects not on ruby on rails, but on using ruby on rails. What it will look like: we are not going anywhere from MVC and Rails, just reconsider the Model, View, Controller. To begin, expand the concept of the model. A model is not just an ORM descendant class. A model is all the business logic of an application. The model includes: models, services, policies, repositories, forms, and other elements, which I will describe next. Also expand the view. Views are templates, presenters, helpers, form builders. Controllers are all about query processing: controllers, responders.
')
In addition to these techniques, knowledge on SOLID, ruby style guide, rails conventions, ruby object model, ruby metaprogramming, basic patterns will be useful.
Helpers
The easiest advice is to use helpers. With the help of them it is convenient to describe the frequent operations:
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)
The menu_item helper displays a menu item depending on policies. You can extend this helper, and it will highlight the active 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
The show_attribute helper prints the attribute name and its value, if any.
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
I use gem simple_form to render forms. This gem takes all the work on displaying forms. It is clear that in the case of non-standard design forms this gem does not work, but for standard forms it fits perfectly.
When building a form, I specify only the necessary: list of fields and their type. Texts for labels, placeholders, and submit are automatically substituted — just write the correct keys in the translation file:
ru: attributes: created_at: activerecord: attributes: user: name: helpers: submit: create:
Now more about their inputs.
For example, all text forms must contain at least 10 lines:
class BigTextareaInput < SimpleForm::Inputs::TextInput def input_html_options { rows: 10 } end end
This is a very simple example, input can be much more complicated. For example, the choice of the state in which the model can be translated (gem state_machines).
SimpleForm also allows you to connect your form builders:
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 is responsible for displaying only those fields to which the current user of the application has access. I will talk more about this in the ACL chapter.
Serializers
Let's now consider a more specific task, namely designing http json api. Here are the easiest ways:
Model#to_json
- controller serialize_model method
All of these methods contradict the principle of sole responsibility and the MVC pattern. The model and the controller should not be engaged in displaying - it is the responsibility of the representations.
I see 2 solutions:
- jbuilder templates
- serializers, both gem of the same name, and just serializer objects (serializers?)
class CommentSerializer < ActiveModel::Serializer attributes :name, :body belongs_to :post end
Those. views are not only templates and helper, but also other objects that deal with the presentation of data, such as serializers.
Presenters
So we smoothly approached the following approach: the use of presenters. In rails, they are used as a complement to helper.
gem drapper introduced confusion: its developers called presenters as decorators. Although these patterns are similar, they have a significant difference: decorators do not change the interface. There are also many
problems with this gem (you can see the list of issues).
I found a
simple, elegant and understandable way to implement presenters . Below I will describe my implementation.
# 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
A presenter is an object that wraps the model and delegates methods to it. As a model there can be any object, even another decorator. The base class
Delegator is included in the standard library.
In addition to the model, the presenter contains view_context, which for convenience is called 'h'.
This is self, available in helpers and views. Accordingly, in the presenters you can use all the helper.
# 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
The present helper passes the present object to the block or as a result.
Transmission through the unit is convenient to use in templates:
# 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
A similar approach can be used if you have very complex mapping logic and helpers do not help. Or there is no object to display. For example, displaying complex menus or event schedules.
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
In this part, I looked at how to organize the logic of representations. In the next one I will show you how to organize the logic of the controllers. In the subsequent - I will tell about models. Namely: form-objects, services, ACL, query-objects, interaction with various storages.