(статья очень старая, по поднятые в ней вопросы актуальны по сей день и регулярно поднимаются в различных обсуждениях)
Многие из вас наверняка заметили, что я пишу книгу о Zend Framework. Недавно я закончил черновики двух глав: «Архитектура приложений на Zend Framework» и «Понимая Zend Framework». В первой главе объясняется архитектурный шаблон Model-View-Controller (MVC) и причины, по которым он стал стандартом де-факто для веб-приложений. Во второй исследуется связь MVC с компонентами Zend Framework, их структурой и взаимодействием.
Завершив обе главы я осознал, что большую часть времени описывал модель и ее фактическое отсутствие в Zend Framework. На самом деле ни один веб-фреймворк не предлагает нам полноценную модель (по причинам, которые я объясню чуть позже). И ни в одном из них не дается внятного объяснения этому обстоятельству. Вместо этого они последовательно связывают понятие модели с родственным, но не идентичным понятием доступа к данным, что изрядно всех запутывает.
Эта сторона фреймворков никогда не привлекала особого внимания. И все же именно она лежит в основе целого класса проблем в тех приложениях, которые пытаются использовать MVC по образу и подобию фреймворков для веб-приложений. Более того, попытки донести идею модели до других разработчиков нередко напоминают битье головой о стену. Я не хочу сказать, что все разработчики тупые или не понимают саму идею, просто никто из них (вне зависимости от того, работают они с PHP или нет) не связывает модели с той областью, которая наделяет их смыслом — принципами объектно-ориентированного программирования.
В этой записи я исследую модели в свете того, как разработчики соотносят их с контролерами и представлениями в приложениях и опишу несколько стратегий, которые можно использовать вместе с правильными моделями.
Модели непоняты
Модели можно описать по-разному. На самом деле только об этом можно написать целую книгу, многие именно так и поступали! Как правило описываются две роли модели:
1. Модель отвечает за сохранения состояния между HTTP-запросами
По сути дела любые данные — в базе данных, файле, сохраненные в сессии или закешированные внутри APC, должны быть сохранены между запросами в виде состояния приложения на момент последнего запроса. Помните, модель не ограничивается базой данных. Даже получаемые из веб-сервисов данные могут быть представлены в виде модели! Да, даже ленты новостей в формате Atom! Стремящиеся побыстрее познакомить с моделью фреймворки этого никогда не объясняют, усиливая непонимание.
Возьмем в качестве примера разрабатываемый мной компонент под названием Zend_Feed_Reader, который на самом деле является моделью. Он читает ленты новостей, обрабатывает их, интерпретирует данные, добавляет ограничения, правила и по большому счету создает удобное представление нижележащих данных. Без него мы имеем Zend_Feed (лучшее средство для чтения новостных лент на данный момент), который требует большого количества работы для получения полноценной модели. Другими словами, Zend_Feed_Reader является моделью, в то время как Zend_Feed ограничивается доступом к данным.
2. Модель включает в себя все правила и ограничения, управляет поведением и использованием данной информации.
Для примера, вы пишете бизнес-логику для модели заказа в снабженческом приложении и по внутренним правилам компании на покупки за наличные может быть наложено ограничение в 500 евро. Покупки на сумму более 500 евро должны быть запрещены в вашей модели заказа (для них может требоваться одобрение вышестоящего начальства). У модели должны быть средства для установки подобных ограничений.
Все станет предельно ясно, как только вы задумаетесь над смыслом слова «модель». В климатологии есть модели климата, описывающие данные, процессы, предполагаемое поведение и позволяющие рассчитать возможные результаты. М в MVC называется моделью не просто так. Модель представляет не только данные, она представляет всю систему, в которой полезны эти данные. Само собой система может быть настолько сложной, что ей понадобится несколько взаимодействующих моделей, но вы поняли идею.
Прочитав эти два пункта вы скорее всего начали осознавать нечто поразительное. За исключением интерфейса, любое приложение можно представить в виде моделей. Именно в них сосредоточены данные, основанные на них правила поведения и, в некоторых случаях, даже вывод этих данных. Именно модель способна понять, истолковать и изложить данные, обеспечив им осмысленное использование.
В программировании толстые модели предпочтительнее моделей с нулевым размером
Джамис Бак (Jamis Buck, автор Capistrano, сейчас работающий в 37signals) в свое время описал концепцию «Тощего контроллера, толстой модели». Крис Хартжес (Chris Hartjes) тоже написал статью на эту тему. Мне всегда нравилась простота этой концепции, так как она иллюстрирует ключевую особенность MVC. В рамках этой концепции считается, что по мере возможности логику приложения (вроде бизнес-логики из примера выше) лучше всего помещать в модель, а не контроллер или представление.
Представление должно заниматься только созданием и отображением интерфейса, через который пользователи смогут сообщить модели о своих намерениях. Контроллеры — это организаторы, связывающие введенные в интерфейс данные с действиями модели и передающие вывод обратно, какое бы представление ни отображало эту модель. Контроллеры должны определять поведение приложения только в плане связи ввода пользователя с вызовами модели, но в остальном должно быть ясно, что вся логика приложения находится в модели. Контроллеры — это скромные существа с минимумом кода, которые обеспечивают условия для упорядоченной работы.
По большому счету, php-разработчики не совсем понимают, что такое модель. Многие считают модель красивым словом для обозначения доступа к базе данных, другие приравнивают ее к разным шаблонам для доступа к базе данных, вроде Active Record, Data Mapper и Table Data Gateway. Фреймворки очень часто продвигают это заблуждение, ненамеренно, я уверен, но энергично. Не полностью понимая, что такое модель, почему это столь великолепная идея, и как ее надо разрабатывать и развертывать, разработчики непреднамеренно вступают на темный путь, ведущий к таким методикам разработки, которые иначе чем убогими и не назовешь.
Небольшое мысленное упражнение даст вам почву для размышлений. Представьте, что вы только что написали самое замечательное в мире веб-приложение с использованием Zend Framework. Клиент поражен, его восторги (и деньги) крайне приятны. К несчастью они забыли упомянуть, что их новый технический директор требует использовать Symfony во всех новых приложениях и предлагает крайне интересную сумму за преобразование вашего приложения. Вопрос: насколько это будет просто? Задумайтесь об этом на секунду…
Если логика вашего приложения завязана на модель — вы на коне! Symfony, подобно многим (но не всем) фреймворкам, принимает модели вне зависимости от того, поверх чего они написаны. Вы можете перенести вашу модель, ее юнит-тесты и вспомогательные классы на Symfony ничего или почти ничего не меняя. Если вы связали все это с контроллерами, у вас проблемы. Вы действительно считаете, что Symfony сможет использовать контроллеры Zend Framework? Что каким-то волшебным образом заработают функциональные тесты, использующие PHPUnit-расширение Zend Framework? Оба-на. Вот почему контроллеры не способны заменить модели. Их практически невозможно использовать повторно.
Непонятые, недооцененные, нелюбимые: модели в депрессии
Так как разработчики очень часто занижают роль модели, ограничивая ее доступом к базе данных, как это по умолчанию делается в 99,9% фреймворков, нет ничего удивительного, что их не впечатляют связанные с ней теоретические идеалы. Сосредотачиваясь на доступе к данным разработчики полностью пускают один очень важный момент: классы моделей не связаны с текущим фреймворком. Им не требуется сложная установка, вы просто создаете и используете их объекты.
Возможно нам не стоит винить во всем среднего разработчика. У веб-приложений есть определенные поведенческие схемы, делающие эту кривую дорогу более привлекательной — большинство их них всего лишь очень большие считыватели данных. Если данные почти не обрабатываются, только читаются, то модель будет очень похожа на старый добрый доступ к данным. Не самое удачное стечение обстоятельств, так что не позволяйте простоте чтения данных одурачить вас. Не все приложения ограничиваются чтением — некоторые, вне всяких сомнений, должны делать с данными что-то выходящее за пределы отображения их в неизменном, соответствующем базе виде.
Модели в PHP — неудачники. С момента появления Smarty и его сородичей все увлечены представлениями. Контроллеры тоже очень важны, так как они считывают данные из базы и передают в шаблоны (общепринятая интерпретация VC). Да-да, контроллеры — это логичная эволюция въевшегося в
А модели? Так как у них нет идеологической привлекательности или схожести со старыми привычками, люди рассматривают их как банальный «доступ к данным». Подобно ссылочным типам в PHP, указывающим на одно и то же значение в памяти. Язык изменился, но старые идеи все еще прячутся за кулисами, сбивая с толку наши нейронные сети.
Но постойте… ведь разработчики все-таки пишут работающие приложения! И если они не используют модели, содержащие логику приложения, то что же, черт побери, они используют?!
Толстые, тупые, уродливые контроллеры: смиритесь (Fat Stupid Ugly Controllers: SUC It Up)
Так как разработчики почти ничего не знали о моделях, они изобрели новое понятие: толстые тупые уродливые контроллеры (ТТУК). Столь яркое определение я придумал не просто так, оно кажется очень забавным в 10 вечера, после нескольких кружек пива. И все равно вежливее того, что я о них на самом деле думаю. (Fat Stupid Ugly Controllers — FSUC — FUC). Их изобрели потому, что модели были непривычными, чуждыми и похожими на террористов сущностями, которым никто не решался доверить хоть что-то выходящее за пределы доступа к данным.
Типичный ТТУК читает данные с базы (используя уровень абстракции данных, который разработчики называют моделью), обрабатывает их, проверяет, пишет и передает в представление для вывода на экран. Он невероятно популярен. Я бы сказал, что большинство пользователей фреймворков создают их так же естественно, как раньше создавали контроллеры страницы (Page Controllers). Они популярны, потому что разработчики осознали, что они могут обращаться с контроллерами почти так же, как с контроллерами страницы — это практически не отличается от древней методики использования отдельных php-файлов для каждой «страницы» приложения.
Не заметили ничего необычного? ТТУК выполняет все возможные действия над данными. Почему? Потому что в отсутствие модели вся логика приложения перемещается в контроллер, что делает его своеобразной моделью-мутантом! Я не просто так употребил слово «мутант». ТТУКи очень большие, громоздкие, уродливые и определенно толстые. Есть псевдо-программистский термин, очень точно описывающий происходящее — "раздутые". Они выполняют задачи, для которых никогда не были предназначены. Это полная противоположность всем принципам объектно ориентированного программирования. И они бессмысленны! По каким-то загадочным причинам разработчики предпочитают использовать ТТУКи вместо моделей, несмотря на тот факт, что такие контроллеры на самом деле просто модели-мутанты.
Помните наше мысленное упражнение? Если вы поместите все в контроллеры, перенос приложения на другой фреймворк станет крайне непростым занятием. Столь жесткое связывание фанаты Кента Бека (Kent Beck) называют «кодом с запашком» (code smell). И это не единственный источник запаха в ТТУКах. ТТУКи большие, с массивными методами, множественными ролям (как правило по одной на каждый метод), множественными повторами кода, не вынесенной во внешние классы функциональностью… ночной кошмар тестировщика. Так как фреймворков много, вы даже не можете правильно применять разработку через тестирование (TDD) без написания собственных дополнений и необходимости обрабатывать объекты запросов, сессии, куки и сбрасывать контроллер входа (Front Controller resets). И даже в этом случае вам придется тестировать созданное контроллером представление, так как у него нет способов вывода, независимых от представлений!
Продолжим задавать вопросы! Как вы тестируете контроллер? Как вы рефакторите контроллер? Как вы ограничиваете роли контроллера? Какова производительность контроллера? Можно ли создать экземпляр контроллера вне приложения? Могу ли я последовательно объединить несколько контроллеров в один процесс и не сойти с ума? Почему я не использую более простые классы и не называю их моделями? Я вообще задумывался над этими вопросами?
Модели неизбежны (как смерть и налоги)
В ТТУКах совершается классическая ошибка. Считая, что идея модели глупа и простой доступ к данными работает лучше всего, разработчики неосознанно завязывают всю логику приложения на контроллер. Мои поздравления! Вы только что создали модель, дрянную, уродливую модель-мутант, настаивая, что она контроллер. Но этот самозваный контроллер крайне сложно переносить, тестировать, поддерживать и рефакторить (считая рефакторинг понятием из реального мира… такое бывает!) Природа контроллеров такова, что они тесно связаны с лежащим в их основе фреймворком. Вы можете выполнить контроллер только после инициализации всего набора MVC данного фреймворка (что скорее всего означает зависимость от десятков других классов!)
Вы можете пойти другим путем — вынести куда-нибудь логику приложения. Переместив ее из контроллера в модель вы получите много классов, не зависящих от используемого фреймворка. Теперь вы можете сутками тестировать эти рассадники глюков с использованием PHPUnit, ни разу не увидев контроллер или представление и не мучая себя глупыми перезапусками фреймворка после каждого теста. Считая их настоящими классами с четко определенными ролями, вы сможете взглянуть на них с правильной точки зрения, произвести соответствующий рефакторинг и написать по-настоящему поддерживаемый код, не дублируя его по множеству классов.
Модели неизбежны. Кто-то может называть ТТУК контроллером, но нас самом деле это контроллер+модель, крайне неэффективная замена модели. Некоторые люди просто посмеются над всеми этими дураками, рассуждающими о необходимости хороших и независимых моделей предметной области (good independent domain models), и продолжат писать запутанный код. Пусть смеются. Ведь именно им придется поддерживать и тестировать свой бардак.
Именно здесь большинство фреймворков для веб-приложений подводят своих пользователей. Они окружены огромным количеством маркетинговой чепухи, неявно предполагающей, что они предлагают полноценную модель. Вы хоть раз видели фреймворк, прямо говорящий нечто иное? В конце концов, это же MVC-фреймворк. Признание того, что разработчик должен писать М самостоятельно, может произвести плохое впечатление. Так что они прячут правду в множестве подробностей, разбросанных по документации доступа к данным или вообще не упоминают о ней.
На самом деле они предлагают только классы доступа к данным — настоящая модель отражает особенности конкретного приложения и должна разрабатываться независимо, после общения с клиентами (можете сами подобрать для этого красивое название — лично я предпочитаю «экстремальное программирование»). Ее надо тестировать, проверять, обновлять и вероятность успеха/провала будет неизменной, вне зависимости от используемого фреймворка. Плохое приложение на Rails останется плохим приложением на Code Igniter.
Контроллеры не должны охранять данные
Еще одним следствием всеобщего недоверия к модели является то, что разработчики стараются использовать ее по минимуму и доверяют контроллерам новую роль хранителей данных (одна из главных причин их мутации в ТТУК). Хотите, я еще сильнее разожгу огонь всеобщего несогласия со мной?
Некоторое время назад я писал проект (Zend_View Enhanced), который рано или поздно будет принят в Zend Framework для внесения объектно-ориентированного подхода в создание сложных представлений, и начал жаловаться на то, что контроллеры являются единственным методом передачи данных из моделей в представления. Я считал, что представления могут обойтись без посредника и использовать вместо него помощников представлений (View Helpers) для чтения данных напрямую из моделей. Это приведет к архитектуре, в которой для многих страниц, доступных только для запросов на чтение, ваше действие контроллера (Controller Action) будет… пустым. В нем не будет никакого кода. Аминь!
Самый лучший контроллер для меня — это отсутствие контроллера.
Я немедленно столкнулся с массовым сопротивлением. Очень немногие поняли, что пустой контроллер, где все взаимодействие с моделью вынесено в простые и повторно используемые помощники представления, сократит повторяющийся код в контроллерах (очень большое количество повторяющегося кода!) и избавит от необходимости выстраивания цепочек действий контроллеров. Вместо этого я услышал немало заковыристых выражений. Многие считали, что MVC работает следующим образом: запросы идут к контроллеру, контроллер получает данные из модели, контроллер передает данные из модели в представление, контроллер отрисовывает представление. Другими словами: контроллер, контроллер, контроллер, контроллер. Однажды я заметил, что сообщество просто одержимо контроллерами. До сих пор очень сложно добиться того, чтобы кто-нибудь дал представлениям объекты с данными и позволил им самостоятельно читать данные из моделей…
Примечание: не все так плохо — некоторые люди поняли о чем речь
Никто из возражающих не заметил, что на самом деле это очень старая идея. В Java термин помощник представления ввели много лет назад, как шаблон проектирования в J2EE, показав, что помощники представлений могут помогать представлениям в доступе к моделям (только в чтении, так как все операции записи должны проходить через контроллер!) без посредника-контроллера. Все в MVC говорит о том, что представления должны знать не только о массивах, которые контроллер кладет им в рот, но и о моделях, которые они отображают.
Так почему бы не пойти дальше! Скольким представлениям хватает одной модели? Многие мои представления используют несколько моделей, обращения к которым очень часто повторяются. Помощник представления — это один класс, но для добавления повторяющихся обращений в контроллеры нам надо повторять эти обращения во множестве методов!
Для избавления от помощника представлений было придумано одно сумасшедшее решение, при котором действия контроллера переизобрели как многократно используемые команды. Если представлению требуются несколько моделей, вы просто последовательно вызываете несколько контроллеров. Эту идею я нередко называю сцеплением контроллеров (Controller Chaining) — его смысл в создании служебного кода, делающего возможным повторное использование контроллеров. Можете перевести его как: многократное использование любого класса, необходимого для выполнения конкретного действия контроллера. Не забывайте — ни один контроллер невозможно использовать без инициализации всего фреймворка. Хотя есть (всегда есть!) исключения.
Модели — классы, контроллеры — процессы
Мои нестандартные идеи наверное вас уже утомили, но вышеупомянутое сцепление контроллеров требует более подробного рассмотрения. Сцепление часто используется для доступа к нескольким моделям или объединения результатов нескольких представлений или и для того и для другого одновременно. Последнее случается чаще всего — если вы не используете помощники представлений для упрощения процесса, то контроллеры почти всегда обращаются к моделям и передают данные в представления.
Представьте, что вы создали три контроллера, каждый из которых создает представление. Чтобы создать некую новую веб-страницу вам необходимо объединить три представления в единую страницу по шаблону (или макету). Это делается последовательным вызовом всех трех контроллеров через Zend_Layout (или какое-нибудь другое решение для сборки представлений в один макет/секцию). А теперь посмотрим, что получилось — три контроллера означают, что мы три раза выполняем стек MVC. В зависимости от приложения это может привести к значительным затратам ресурсов. Просто для примера величины этих затрат, Symfony использует «компоненты» (“Components”) как специализированные типы контроллеров, предназначенные исключительно для смягчения удара по производительности, но в Zend Framework нет ничего подобного. Схожая идея в Rails вызывала массу жалоб на падения производительности. Расхожая мудрость гласит, что использование нескольких полных контроллеров в фреймворках чудовищно неэффективно и является последней надеждой в тех случаях, когда нет иных стратегий повторного использования.
Повторюсь, сцепление контроллеров — это код с запашком. Оно неэффективно, неуклюже и как правило не нужно.
Альтернативой, само собой, является использование частичных представлений (кусочков шаблона, способных объединиться в одно родительское представление), способных напрямую взаимодействовать с моделью через помощник представления. Избавьтесь от контроллеров в целом — в конце концов в них нет никакой логики приложения, кроме передачи пользовательского ввода соответствующему вызову модели (за исключением ТТУКов).
Основная идея в том, что модель — это просто набор слабо связанных классов. Вы можете создавать и использовать их экземпляры где угодно — в других моделях, контроллерах и даже представлениях! С другой стороны, контроллер является неотъемлемой частью общего процесса. Вы не можете повторно использовать контроллер не запуская весь процесс создания объектов запроса, диспетчеризации, применения помощников действий, инициализации представления и обработки возвращаемых объектов ответа. Это затратно и неуклюже.
В завершение разговора
Как вы могли заметить, эта статья была жизненно необходима. Я твердо уверен в необходимости внедрения изящных принципов объектно ориентированного программирования в MVC фреймворки. Именно поэтому я всеми силами продвигал Zend_View Enhanced в Zend Framework и видел, как его одобряли, обсуждали и крайне успешно использовали. Во многом это произошло благодаря Мэтью Вейнеру О'Финли (Matthew Weier O’Phinney) и Ральфу Шиндлеру (Ralph Schindler), присоединившимся к продвижению этой идеи. Забыв о простых методиках и принципах ООП мы завязнем в борьбе с MVC, забыв о его смысле. MVC — это великий архитектурный шаблон, но в конце концов любые наши толкования MVC и привычные убеждения производны от принципов ООП. Забыв об этом, мы начнем делать Плохие Вещи (ТМ).
Надеюсь этот поток мыслей о модели в модели-контроллере-представлении окажется чем-то просветляющим и заставит вас задуматься. Вашей целью должно быть самостоятельно
The M in MVC: Why Models are Misunderstood and Unappreciated
Автор: Vedomir