[Из песочницы] Как Дэвид Хайнемайер Хенссон организовывает контроллеры
Эта статья тут потому что я искал ответы на некоторые вопросы. Для меня понимание статьи не обошлось беглым чтением, пришлось переводить вручную. Возможно, кого-нибудь будет интересовать эта тема, но владение английским будет хуже моего. Перевод вам в помощь.
Оригинал статьи можно прочитать на сайте Jerome’s Adventures in Software.
В недавнем интерьвью «с Full stack radio» наш гуру и спаситель DHH объяснил как он организовывает структуру rails контроллеров в последней версии Basecamp. Вот расшифровка его святых слов:
Я пришел что бы рассказать, то что будучи почти фундаменталистом в создании контроллеров, я остаюсь приверженцем REST’у который постоянно меня выручает.
Каждый раз когда я недоволен своими контроллерами, это потому что у меня их мало. Я стараюсь их слишком сильно перегружать.
Итак, на Basecamp 3 мы выходили за контроллеры каждый раз, когда были даже своего рода под-ресурсы (subresource), что имело смысл. Они могли выступать в качестве фильтров (filters).
Предположим у вас есть этот код и он выглядит так:
class BananasController < ApplicationController
def index
render plain: '\_(:-))_/'
end
end
Ну если вы примените несколько фильтров и выпадающее меню, то это будет уже что-то другое. Иногда мы берем это что-то и делаем для него совсем новые контроллеры.
Вот Эвристика, которой я пользуюсь именно для работы: всякий раз, когда у меня появляется желание добавить новый экшен в контроллере, этот экшен понятное дело не входит в один из пяти экшенов по умолчанию, то есть, я просто удовлетворяю это желание и создаю новый контроллер! Так это и называю.
Итак, давайте представим, у вас есть InboxController (контроллер почтового ящика) и в нем экшен index который показывает все что в почтовом ящике; и вы могли бы сделать другой экшен «О я хочу добавить новый экшен pending, он будет представлять собой что-то типа ожидающего сообщения электронной почты или что-то в этом роде».
И вы добавляете этот экшен pendings:
class InboxesController < ApplicationController
def index
end
def pendings
end
end
Это очень общий шаблон, правда? И образец который я использую может объяснить больше. Сейчас я скажу вам «нет нет нет», и сделаю новый контроллер который называется Inboxes: PendingsController и он имеет один экшен index:
class InboxesController < ApplicationController
def index
end
end
class Inboxes::PendingsController < ApplicationController
def index
end
end
И вот что я обнаружил, что свобода, дающая вам то что каждый контроллер теперь имеет свою область применения со своими собственными наборами фильтров которые применяются…
Итак, мы имеем большое распространение контроллера и особенно распространение контроллера в пространстве имен. Давайте предположим что у нас есть MessagesController и также мы можем иметь контроллер Messages: DraftsController, а еще Messages: TrashesController и тут мы можем иметь все эти под-контроллеры и под-ресурсы в пределах одного и того же контроллера. По моему это круто.
Аминь.
По существу, он сказал что контроллер должен иметь только стандартные CRUD экшены index, show, new, edit, create, update, destroy. Любые другие действия должны приводить к созданию контроллера который имеет только CRUD экшены.
Что я думаю по этому поводу
Дальше будут мои собственные убеждения. Однозначно, есть некоторые различия в мнениях. Просто скажу, только не называйте меня фанатиком. Успокойтесь ребята.
Во всяком случае я был счастлив узнать это, я использую «DHH way» (путь Дэвида Хайнемайер Хенссона) для организации контроллеров уже более года и по настоящее время #DHHFanboy. Примеры он упоминает только о фильтрации хотя очевидно что примеры для простой логики контроллера — избыточны. Общий способ использования фильтров в REST — это использование параметров запроса, например (e.g. GET /inboxes? state=pending).
В целом, я бы придерживался того что код должен быть коротким и простым (как только он становился бы длинным и сложным или очень смешанным в действиях и отношениях — я бы сделал тоже самое, что и Дэвид). Но я согласен с главной идеей разделения контроллеров, и имею для этого несколько причин.
Это помогает создавать более простой код
С этой техникой вы можете создавать столько контроллеров сколько вам угодно. Используйте свои личные убеждения, хотя: если есть контроллер по умолчанию (с CRUD экшенами) относительно краткости и простоты (как например scaffolded в rails), то тут вероятно не нужно извлекать каждый index/show/ и тд в свой собственный контроллер.
Техника разделения контроллера становится лучше, когда сам контроллер становится мощнее, даже несмотря на единственный недостаток — CRUD экшены. Что делать в таком случае? Просто запиши этот код в предназначенный для него контроллер.
Например, здесь наш самый сложный контроллер выглядит как в моей текущей компании (мы используем маленькие модели и достаточно большие контроллеры, YMMV) YMMV — your mileage may vary (одну и ту же вещь можно использовать по разному). Это позволит вам покупать продукты в нашем API приложении:
class Api::V1::PurchasesController < Api::V1::ApplicationController
rescue_from Stripe::StripeError, with: :log_payment_error
def create
load_product
load_device
load_or_create_user
create_order
create_payment
authorize_payment
confirm_address
render json: @order, status: :created
end
private
def load_product
@product = Product.find_by!(uuid: params[:product_id])
end
# …
end
Тут только один public метод CRUD (по умолчанию create экшен) Нет преждевременной абстракции в «толстых» моделях, нет service classes, нет обсерверов (observers), нет ничего, никакой фигни. Все здесь. Удобно помещено в контроллер. Не нужно прыгать по куче файлов что бы понять что происходит. Вам только нужно открыть этот один файл в вашем редакторе. А поскольку контроллеры являются точкой входа для любого кода веб-приложений, вы должны открыть этот файл в любом случае во время кодинга. #ObviousCode
Но вы наверное спросите «Хоспади, какой большой этот класс, вы что, все в него впихнули?» Конечно, он очень большой, прям как… большой короче, не так ли? Нет, всего-то 144 строки И это самый сложный контроллер. Прям худших из худших. Мы конечно же могли бы делить код на маленькие куски, однако для нас это вполне нормально (YMMV). Остальная часть наших контроллеров гораздо проще, от 6 до 103 строк, в среднем один контроллер — 15 строк. (у нас есть 150 контроллеров на данный момент).
Помните проекты в которых вы работали, где больше 200+ строк кода в контроллерах и это лишь малая часть запроса — остальное рассеивается между бесконечными объектами обслуживания, моделями и обсерверами (service objects, observers, and models)? Такое д*рьмо не происходит здесь благодаря этой техники разделения контроллеров среди простых вещей также, как правило трех.
Действительно, дублирование принесет меньше вреда чем неправильная абстракция, и это одна из причин почему я считаю что DRY и SRC (в том числе и DHH way) сильно переоценены и полны презренным д*рьмом и они сосут шары и и и … Вернемся к нашей теме!
Это делает ваш код более однообразным
Знание того что может быть только куча CRUD экшенов в контроллере не особо помогает. Не нужно больше гадать и скроллить в больших контроллерах что бы найти один непонятный экшен. Не более интересно как/если обычный экшен отображается в роутах (route).
Я не люблю удивляться когда делаю простую организацию. Я люблю однообразие кода, мне нравится равномерный код и сильная «convention over configuration» — это одна из причин почему я предпочитаю rails другим Ruby фреймворкам. Всё организованно так же, так что вы тратите меньше времени выполняя рутинные решения, и добиваетесь большего успеха в тех областях которые действительно важны для бизнеса.
В теории, это также означает что вы можете переходить от одного кода к другому и быть на 100% продуктивным в очень короткий промежуток времени. В дикой природе люди вступают в схватку с «ужасными Rails приложениями». Одна компания может использовать архитектурные шаблоны такие как обсерверы (не моя чашка чая), другая может использовать дополнительную архитектуру такую как Trailblazer (не моя чашка чая, но у меня есть несколько интересных идей на этот счет), третья использует еще какие-нибудь инструменты, четвертая вообще что-то свое и т.д.
Все это из-за того что люди недовольны и несчастливы так называемым «отсутствием структуры» в ванильных Rails приложениях. Таким образом они ищут дополнительную структуру в другом месте. Гайз! Решение прямо под носом. Разделение контроллеров и использование только CRUD экшенов. Простой как двадждыдва способ и дружественный как джуниор разработчик.
Rails могли бы сделать лучше эту работу по продвижению эвристических разделений контроллеров. Их документация кратко говорит »… вы должны обычно использовать ИЗОБРЕТАТЕЛЬНЫЕ маршрутизации …» Но сама идея CRUD экшенов и RESTful’а явно прослеживается в их документациях с давних пор.
Если вы RTFM’ed (RTFM — read the fucking manual) то наверняка, хотя бы один раз думали что добавление пользовательских экшенов (кроме CRUD) не очень «Rails way» Разделение контроллеров — хорошая тема для раздумий по этому поводу.
Это заставляет вас думать о REST
Многие люди любят REST, потому что этот архитектурный стиль однороден и прост. После того как вы поймете (на самом деле поймете) RESTful, вам будет проще понять другое.
В теории, хотя бы: аутентификацию каждого? ;-) Бизнес логика очевидно отличается между приложениями, так что вы должны понять это, но как вы поймете что логика везде похожа. То есть вы создаете расходы в Stripe (вы берете чью-то деньги), вы создаете SMS в Twilio (т.е. вы отправляете его), вы получаете хранилище в Github и т.д.
Вы должны сначала немного напрячь мозг что бы использовать REST употребляя имя существительное вместо действий: не «платить (pay)», а «создать платеж (create a payment)» не «пополнить баланс (add funds to your balance)», а «создать капитал в балансе («create a fund in a balance)» и тд. Может быть немного странно, но я бы заплатил бы эту цену в любой день неделе чем вернулся бы к SOAP, WSDL и прочей чепухе (ex-Java / JEE разработчики знают, о чем я говорю).
К тому же, я думаю, что, имея всю бизнес-логику интерфейса (не обязательно реализацию), диктуемую REST, который создан для более чистой и простой бизнес-логики, вы можете иметь только объекты с множеством контроллеров, «не больше не меньше». До тех пор вы знаете что можете выражать что угодно с REST, а и это должно быть понятно. Это освободит от ограничений.
Вот несколько примеров RESTful Rails роутов которые представляют разделенные контроллеры использующие только CRUD экшены.
resources :purchases, only: :create
resources :costs_calculations, only: :create
namespace :company do
resource :account_details, only: :update
resource :website_details, only: :update
resource :contact_details, only: :update
end
namespace :balance do
resources :funds, only: :create
end
resource :bank_account, only: :update
Для лучшего REST проектирования (особенно когда дочерние ресурсы связаны) Я обычно опускаю REST в написании экшена+ресурс в начале временной метки (POST /balance/funds) не беспокоясь о реализации. Тогда, когда имена присвоены — я спокоен. Переводим все в rails роуты, которые очень удобны, так как rails имеет хорошую поддержку REST.
Заключение
Разделение ваших контроллеров когда они имеют очень специфичные масштабы и слишком много логики или слишком много трудностей может иметь хорошие эффекты в вашем коде.
Это не значит «забудьте про абстракцию». Она просто будет проходить немного ниже. Некоторые пункты и логика нуждаются в смешанных контроллерах. Иногда даже разделенные контроллеры только с одним public экшеном выглядят большими и т.п. Это касается методов модели, и даже — пусть Бог простит меня — тут служебные объекты вступают в игру (к счастью служебные объекты должны быть редкостью, если вы осторожны).
Чем больше ваше приложение растет, тем больше времени вам нужно будет потратить, чтобы понять это, независимо от того, насколько чист код. Но разделение контроллеры делает вещи проще.