Project complexity management on ruby on rails. Part 1
The main problem of RoR projects is that, as a rule, they try to fit all the logic into models, controllers and representations. Those. the code is found only in models (ActiveRecord :: Base), controllers, helpers and templates. This approach leads to sad consequences: the code becomes confusing, features take a long time to make, regressions appear, and developers lose motivation. As an example, you can look at the source code of redmine .
The way out of this situation is pretty obvious. We will do projects not on ruby on rails, but using ruby on rails. How it will look: we are not going away from MVC and Rails, just revise Model, View, Controller. To begin with, we expand the concept of a model. A model is not just an ORM derived 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 further. We will also expand the presentation. Views are templates, presenters, helpers, form builders. Controllers are all that are connected with request processing: controllers, responders.
In addition to these techniques, knowledge of SOLID, ruby style guide, rails conventions, ruby object model, ruby metaprogramming, and basic patterns are useful.
Helpers
The simplest advice is to use helpers. Using them, it is convenient to describe 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 the policies. You can expand 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 name of the attribute 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 am using gem simple_form to render forms. This gem takes on all the work of displaying forms. It is clear that in the case of non-standard design forms, this gem will not work, but it suits perfectly for standard forms.
When building a form, I indicate only what is needed: a list of fields and their type. Texts for labels, placeholders, submit are substituted automatically - just enter 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; inputs can be much more complicated. For example, choosing which state the model can be transferred to (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 that the current application user has access to. I will talk about this in more detail in the chapter on ACLs.
Serializers
Let's now look at a more specific task, namely designing http json api. Here are the easiest ways:
- method
Model#to_json - serialize_model controller method
All of these methods contradict the principle of sole responsibility and the MVC pattern. The model and the controller do not have to deal with the display - this is the duty of the views.
I see 2 solutions:
- jbuilder templates
- serializers, both gem of the same name and just serializing objects (serializers?)
class CommentSerializer < ActiveModel::Serializer
attributes :name, :body
belongs_to :post
end
Those. Representations are not only templates and helpers, but also other objects that represent data, such as serializers.
Presenters
So we gradually approached the following approach: the use of presenters. In rails, they are used as a complement to helpers.
gem drapper was confusing: its developers called presenters decorators. Although these patterns are similar, they have a significant difference: decorators do not change the interface. There are many problems with this gem (you can look at 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 a model and delegates methods to it. Any object, even another decorator, can be a model. The base class Delegator is included in the standard library.
In addition to the model, the presenter contains view_context, which is called 'h' for convenience.
This is self available in helpers and views. Accordingly, all helpers can be used in presenters.
# 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 presenting object into a block or as a result.
The transfer through the block is convenient to use in the 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
You can use a similar approach if you have very complex display 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 ways to organize presentation logic. In the next, I will show how you can organize the logic of controllers. In the following - I will tell you about the model. Namely: form-objects, services, ACL, query-objects, interaction with various repositories.