Иерархия контроллеров

    В большинстве встречавшихся мне rails проектов, структура контроллеров не имеет никакой организации и проект растет как придется. В больших проектах это приводит к тому что контроллеры становятся огромными (с десятками actions), а условные фильтры растягиваются на весь экран. Разобраться в таком коде бывает очень не просто.

    Поработав с большим количеством rails проектов, у меня был сформирован подход к организации иерархии контроллеров, которая позволила унифицировать их структуру и упростить код.



    Верхний уровень



    В первую очередь удобно на верхнем уровне расположить основные неймспейсы проекта, такие как web, mobile, api, feeds и другие. При этом модуль admin будет находиться в web.



    Внутри апи можно так же выделить подмодули, разделенные по версии, например, v1, v2. Либо делать папки на верхнем уровне api1, api2 и т.п.
    link: freelancing-gods.com/posts/versioning_your_ap_is

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

    Для каждого модуля создается Namespace::ApplicationController для того чтобы не смешивать логику подходящую для конкретных неймспейсов.



    Но в роутинге будут небольшие отличия для разных модулей. Web задается как scope. В результате мы получим хелперы без лишних префиксов, а вот для остальных используется namespace.



    Вложенные ресурсы



    Начну сразу с примера: профайл пользователя на сайте. Он может быть доступен по такому урлу /users/:id и его контроллер UsersController. Список постов пользователя доступен по /users/:id/posts. Соответственно мы имеем PostsController с index внутри которого получаем посты по юзеру. Затем у нас появляется задача выводить все посты одним списком. Мы используем тот же контроллер posts и добавляем туда метод index_all. При этом юзер тут уже не нужен. В конце концов, добавляя множество представлений, появится часть методов которые выводят эти посты (и обрабатывают их) для юзера, а часть без него. Дальше появятся фильтры, например, before_filter :load_user, :only => [:index …], и так до тех пор, пока не останется людей способных понимать что тут происходит.

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

    В данном случае так же будет разумным создать модули для соответствующих ресурсов.



    И для каждого неймспейса будет собственный ApplicationController содержащий общие методы модуля.



    Для всех контроллеров находящихся в этом модуле будет доступен current_section без фильтров.

    Профит:

    Контроллеры становятся меньше и проще.
    Глядя на структуру контроллеров уже много можно сказать о том как организован проект и чего можно ожидать в конкретном контроллере.
    Уходит много условных фильтров.
    Упрощается тестирование;
    Единый организационный подход;

    Так же, для удобства и единообразия, входные контроллеры неймспейса обзываются welcome. И подключаются в роутах через root :to => ‘welcome#index’ внутри соответствующего scope.
    Реальный пример:

    Рассмотрим как организовать контроллеры на реальном примере. Допустим потребовалось сделать интеграцию с facebook.

    При подходе в лоб, был бы сделан какой нибудь контроллер FacebookController, в который помещались бы все методы для работы с фейсбуком. В итоге мы бы получили часть методов, которая должна работать только из аккаунта пользователя, а часть только для тех кто не авторизован и т.п.

    Подход с разделением:

      resource :facebook, :only => [:new, :create]
    


    В данном случае о пользователе еще ничего не известно, а вот при изменении интеграции (например настройка автошаринга) или удалении мы работаем с конкретным пользователем. Это можно реализовать, например, так:

    # app/controllers/web/account/facebook_controller.rb
    resource :account do
      scope :module => :account do
        resource :facebook, :only => [:edit, :update, :destroy]
      end
    end
    


    А если понадобится дать возможность интегрироваться уже авторизованному юзеру? Тогда в последний пример добавляем :new, :create.

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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 29

      –7
      REST? не, не слышал
        0
        Дяденька, вы бы хоть почитали и попробовали вникнуть перед тем как комменты такие оставлять.
          0
          Сыночек, я прочитал и вникнул.
          А вот вам советую прочитать поглубже и узнать, что такое REST кроме index+show+edit+update+new+create+destroy.

          есть сущность, у неё есть url /entities. вычленить из запроса API это, web или mobile вы можете многими способами, будь то параметры, заголовки или на крайний случай поддомен. но url должен быть машиночитаемым и обозначать тип сущности.
            0
            << Сыночек, я прочитал и вникнул.

            Давай с этим остановимся. Мы тут не для этого собрались.

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

            Рельсы на это:
            resources :users do
            resources :posts
            end

            контроллеры UsersController и PostsController.

            Я предлагаю UsersController, и Users::PostsController.

            Поэтому мне не понятен ваш выпад, который к теме внутренней организации не имеет прямого отношения.
              +3
              я это понимаю. я к тому, что вы написали, что у вас в приложении есть web в скопе, и есть api, mobile и админка в namespace'ах. итого:
              /users/1/posts
              /api/users/1/posts
              /mobile/users/1/posts
              /admin/users/1/posts
              зачем, если можно сделать один рест-контроллер, и уже к примеру CanCan'ом смотреть, с каких запросов что разрешать смотреть, что редактировать и т.д.?
                0
                Я не стал затрагивать эту тему потому что это отдельный разговор.

                В простых приложениях да сработает один контроллер в котором мы что то выбираем. Но один канкан тут не спасет. В вебе результатом может быть редирект в mobile еще что то, в апи что то свое. В больших приложениях респонды превращаются в ад, сама логика может быть сложной и зависеть от варианта использования. Список постов может быть общедоступным (по юзеру) и у него будет один дизайн, а может быть список в аккаунте и там совершенно все по другому. Версий апи бывает несколько. Ну и опять же ифы ифы ифы.

                К такому разделению я пришел не сразу, но сейчас говорю про то что это работает, но не утверждаю что это единственный путь.
                  0
                  Для перенаправлений и прочего был создан respond_to и format'ы.

                  View вообще очень гибкая тема, и не является аргументом.

                  Версии api — вообще спорный вопрос. Моё имхо, вот эти версии — этакое google-java legacy. REST API как раз был придуман, чтобы избегать этих зоопарков версий, перенесённых из XML-RPC. И статья Freelancing Gods здесь не аргумент. Ребята молодцы, но лучшее, что они написали — это Thinking Sphinx, в код которого достаточно заглянуть, чтобы больше не читать их статьи.

                  И вообще, все известные евангелисты так называемого Rails Way уже устали твердить, что отдельная админка для приложений — моветон.
                    0
                    «Для перенаправлений и прочего был создан respond_to и format'ы.»

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

                    «И вообще, все известные евангелисты так называемого Rails Way уже устали твердить, что отдельная админка для приложений — моветон.»

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

                    В любом случае мы друг друга не переубеждаем, а обозначаем свои позиции. Я понял вашу, надеюсь вы поняли мою.
                      0
                      Версии API используются даже в гитхабе, а там вполне образцовое применение, ИМХО. API там вообще сказка.
                      Конечно, неизвестно, как оно сейчас изнутри сделано, но версии, как таковые, присутсвуют — и никто не пытается отправлять POST запросы напрямую на github.com/username/projectname/issues/add.xml или что то подобное.
                        +1
                        угу. и в третьей версии они как раз перешли на чистый REST API без всяких /api/ и /v2/ в запросах
                          0
                          Ага а фейсбук ушел с реста на graph api.

                          Это все лишь означает что для них это работает лучше. Тут как и везде, нет серебряной пули.
                            +1
                            в том, что они называли REST API, от REST было только название.
                            в Graph API гораздо больше рестоподобности. в общем-то он фактически и является REST'ом за исключением того, что они не разбивают сущности на типы, а сразу делают graph.facebook.com/ENTITY_ID/(action|subresource)
                              0
                              «рестоподобности»
                              «общем-то он фактически и является REST'ом»
                              «за исключением того, что они не разбивают сущности на типы»
                              «но url должен быть машиночитаемым и обозначать тип сущности.»

                              Так все таки рест или не рест?)

                              Вопрос на самом деле риторический, понятно что все что есть это некое приближение к ресту.
                            0
                            Ага, а «api.» в имени домена уже в запрос не входит.
                              0
                              я про возможность использования поддоменов писал выше. они конвенции REST не нарушают, а необходимость этого поддомена вызвана сложившейся традицией путей вида github.com/user_name/repo_name.
                      +3
                      А разгадка одна — желание сделать код как можно более легким для поддержания. CanCan — норм, когда речь идет о простых вещах. Сложные контроллеры порождают кучу условных фильтров, мусорных спецификаций (типа блоков respond_to) и соотношение неполезного кода к полезному становится не в пользу последнего. Некоторых людей это раздражает.

                      Использовать такое разделение с самого начала проекта, конечно, спорное решение. Особенно, когда непонятно, появятся такие сущности или нет. Но, например, автор статьи несколько месяцев назад подал идею разнести отдачу html и js по разным контроллерам. А именно, js-ответы вынести в API. Совсем не типично для Rails Way, но результат на практике мне очень нравится.

                      А в целом — присоединяюсь к комментатору ниже, написавшему про Engines. Особенно с Rails 3.1+
                        0
                        А почему все забывают про respond_with? Кода меньше в разы а результат тот-же и не нужно городить кучу сущностей.
                          0
                          Ну и presenters добавить не забываем.
                            +1
                            Про respond_to никто не забывает. Полезная штука. Но, к сожалению, тоже не универсальная.
                            Презентеры — это уже немного про другое, в любом случае, возникает вопрос о том, как хранить эти самые презентеры, чтобы удержать контроль над кодом.
                            Раз уж заговорили про презентеры, то вот — blog.carbonfive.com/2012/01/10/does-my-rails-app-need-a-service-layer/
                            Rails way, Rails wayем, а то что реально решает проблемы, пропускать не стоит.
                              0
                              respond_with конечно же
                +3
                Интересное решение задачи, но, по-моему, feed и mobile несколько иного рода группировка чем, например, api.
                Не имеет смысла дублировать контроллеры когда можно выводить ответ (response) в разных форматах и использовать соответствующие типы шаблонов. Если же задача состоит в инкапсуляции или изоляции данного раздела приложения, удобнее вывести часть кода в движок (engine).
                  0
                  Имеет. Как я уже выше написал в сложных и больших приложениях респонды начинают только усложнять и запутывать. В вебе у вас может быть выбрано очень много данных для разных блоков, в mobile немного. Уже нужно разделять, а все может быть и гораздо сложнее.

                  Мой посыл в том что эта схема работает когда сложность приложения высокая, для большинства проектов это конечно же избыточно.
                  0
                  У меня аналогичная схема организации контроллеров, только не ApplicationController, а Base контроллер и у меня «форматы ответа» не отражаются в иерархии, там скорее «точки зрения» на приложения или упрощенно — роли пользователей. Пока что работает идеально и новые фичи имплементятся прекрасно — с cancan разделением «ролей» было бы хуже — проверяли.
                    0
                    оговорился BaseController
                      0
                      А можно вас попросить написать об этом подробнее, или дать пример посмотреть?
                      Сам думаю либо вариант toxicmt, либо ваш построить, пока к какому-то решению не пришел.
                      0
                      Спасибо, интересный подход. А еще если активно юзать inherited_resources, то вообще круто.
                        0
                        Оффтоп, но подскажите, что за шрифт у вас на скриншотах?
                        0
                        Спасибо, и правда он.

                        Only users with full accounts can post comments. Log in, please.