Back to Home

Project complexity management on ruby ​​on rails. Part 1

mvc · ruby · rails · solid · presenter · decorator · patterns

Project complexity management on ruby ​​on rails. Part 1

    In this series of articles, I will collect most of my Ruby on Rails development experience. These techniques allow you to control complexity and facilitate the maintenance of the project. 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 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.

    Read Next