MVC: что это такое и какое отношение имеет к пользовательскому интерфейсу. Контроллеры

08.07.2019

Контроллеры

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

В инфраструктуре ASP.NET MVC Framework контроллеры являются классами.NET, которые содержат логику, требуемую для обработки запроса. Роль контроллера заключается в инкапсуляции логики приложения. Другими словами, контроллеры отвечают за обработку входящих запросов, выполнение операций над моделью предметной области и выбор представлений для визуализации пользователю.

Пример приложения

Для целей этой и следующих статей мы создадим новый проект MVC по имени ControllersAndActions с использованием шаблона Empty (Пустой), отметив флажок MVC в разделе Add folders and core references for (Добавить папки и основные ссылки для), а также проект модульного тестирования под названием ControllersAndActions.Tests. Модульные тесты, которые будут создаваться, не требуют имитированных реализаций, поэтому пакет Moq устанавливать не придется, но нужно установить пакет MVC, чтобы тесты имели доступ к базовым классам контроллеров.

В окне консоли диспетчера пакетов NuGet среды Visual Studio введите следующую команду:

Install-Package Microsoft.Aspnet.Mvc -version 5.0.0 -projectname ControllersAndActions.Tests

После создания проекта выберите пункт ControllersAndActions Properties (Свойства ControllersAndActions) в меню Project (Проект) среды Visual Studio, в открывшемся диалоговом окне перейдите на вкладку Web (Веб) и отметьте переключатель Specific Page (Определенная страница) в категории Start Action (Начальное действие). Вводить какое-либо значение не нужно - достаточно только выбора переключателя.

Понятие контроллера

Вы сталкивались со случаями использования контроллеров почти во всех предшествующих статьях, посвященных ASP.NET MVC. Наступило время заглянуть "за кулисы".

Создание контроллера с помощью интерфейса IController

В MVC Framework классы контроллеров должны реализовать интерфейс IController из пространства имен System.Web.Mvc , показанный в примере ниже:

Using System.Web.Routing; namespace System.Web.Mvc { public interface IController { void Execute(RequestContext requestContext); } }

Чтобы получить определение этого интерфейса, необходимо загрузить исходный код MVC Framework , который чрезвычайно полезен для выяснения внутренней работы средств инфраструктуры.

Как видите, интерфейс IController очень прост. Его единственный метод Execute() вызывается, когда запрос направляется этому классу контроллера. Инфраструктура MVC Framework выясняет, на какой класс ориентирован запрос, за счет чтения значения свойства controller, сгенерированного данными маршрутизации, или через специальные классы маршрутизации.

Создавать классы контроллеров можно путем реализации интерфейса IController, однако поскольку этот интерфейс является низкоуровневым, потребуется проделать немало работы, чтобы получить, в конечном счете, что-нибудь полезное. Тем не менее, интерфейс IController полезен для демонстрации оперирования контроллеров и с этой целью в папке Controllers создается новый файл класса по имени BasicController.cs, содержимое которого приведено в примере ниже:

Using System.Web.Mvc; using System.Web.Routing; namespace ControllersAndActions.Controllers { public class BasicController: IController { public void Execute(RequestContext requestContext) { string controller = (string)requestContext.RouteData.Values["controller"]; string action = (string)requestContext.RouteData.Values["action"]; requestContext.HttpContext.Response.Write(string.Format("Контроллер: {0}, Метод действия: {1}", controller, action)); } } }

Методу Execute() интерфейса IController передается объект RequestContext , предоставляющий информацию о текущем запросе и маршруте, который ему соответствует (и приводит к вызову данного контроллера для обработки этого запроса). В классе RequestContext определены два свойства, описанные в таблице ниже:

Объект HttpContextBase предоставляет доступ к набору объектов, описывающих текущий запрос, которые называются объектами контекста; мы еще вернемся к ним позже. Объект RouteData описывает маршрут. Важные свойства класса RouteData перечислены в таблице ниже:

В статье Настройка системы маршрутизации было показано, как использовать типы RouteBase и IRouteHandler для настройки системы маршрутизации. В рассматриваемом примере с помощью свойства Values получаются значения переменных сегментов controller и action, которые затем записываются в ответ.

Часть проблемы, возникающей при создании специальных контроллеров, связана с отсутствием доступа к таким средствам, как представления. Это означает, что придется работать на более низком уровне, чем и объясняется запись содержимого напрямую в ответ клиенту. Свойство HttpContextBase.Response возвращает объект HttpResponseBase , который позволяет конфигурировать и добавлять содержимое к ответу, предназначенному для отправки клиенту. Это еще одна точка соприкосновения между платформой ASP.NET и инфраструктурой MVC Framework.

Если запустить приложение и перейти на URL вида /Basic/Index, то специальный контроллер сгенерирует вывод, показанный на рисунке ниже:

Реализация интерфейса IController позволяет создать класс, который MVC Framework распознает как контроллер и отправляет ему запросы, безо всяких ограничений относительно того, как запрос обрабатывается, и какой ответ для него генерируется. Это удачный пример, поскольку он показывает, насколько расширяемой может быть инфраструктура MVC Framework даже для ключевых строительных блоков вроде контроллеров, однако написание сложного приложения подобным образом может быть сопряжено с немалыми трудностями.

Классы с именами, заканчивающимися на Base

При обработке запросов инфраструктура MVC Framework полагается на платформу ASP.NET, что имеет большой смысл, т.к. эта платформа является проверенной и многофункциональной реализацией, которая тесно интегрируется с сервером приложений IIS. Проблема заключается в том, что классы, применяемые платформой ASP.NET для предоставления информации о запросах, не очень хорошо подходят для модульного тестирования - основного преимущества использования MVC Framework.

В Microsoft решили ввести возможность тестирования, поддерживая при этом совместимость с существующими приложениями ASP.NET Web Forms, так что в результате появились классы Base . Эти классы так называются из-за того, что они имеют те же самые имена, как у основных классов платформы ASP.NET, за которыми следует слово Base.

Так, например, платформа ASP.NET предоставляет контекстную информацию о текущем запросе и ряде ключевых служб приложения через объект HttpContext. Соответствующим ему классом Base является HttpContextBase, экземпляр которого передается методу Execute(), определенному в интерфейсе IController (в последующих примерах будут продемонстрированы и другие классы Base). В первоначальных классах и классах Base определены одни и те же свойства и методы, но классы Base всегда абстрактны , а это значит, что их легко применять для модульного тестирования.

Иногда вы получите экземпляр одного из первоначальных классов ASP.NET, такого как HttpContext. В таком случае необходимо создать дружественный к MVC класс Base, подобный HttpContextBase. Это делается с использованием одного из классов Wrapper , которые имеют такие же имена, как первоначальные классы, дополненные словом Wrapper, например, HttpContextWrapper . Классы Wrapper являются производными от классов Base и имеют конструкторы, которые принимают экземпляры первоначальных классов:

Первоначальные классы, классы Base и классы Wrapper определены в пространстве имен System.Web, поэтому платформа ASP.NET может гладко поддерживать приложения MVC Framework и более старые приложения Web Forms.

Создание контроллера за счет наследования от класса Controller

Как было продемонстрировано в предыдущем примере, инфраструктура MVC Framework допускает практически неограниченную настройку и расширение. Чтобы обеспечить любой требуемый вид обработки запросов и генерации результатов, можно реализовать интерфейс IController. Вам не нравятся методы действий? Вы не хотите беспокоиться по поводу визуализированных представлений? В таком случае можете взять дело в свои руки и реализовать лучший, быстрый и более элегантный способ обработки запросов. Либо же вы можете воспользоваться средствами, предлагаемыми командой разработчиков MVC Framework из Microsoft, и унаследовать свои контроллеры от класса System.Web.Mvc.Controller .

Класс Controller обеспечивает поддержку обработки запросов, которая знакома большинству разработчиков приложений MVC. Она применялась во всех примерах, рассмотренных в предыдущих статьях. Класс Controller предоставляет три ключевых средства, которые описаны ниже:

Методы действий

Поведение контроллера разнесено по множеству методов (вместо реализации в виде единственного метода Execute()). Каждый метод действия отображается на соответствующий URL и вызывается с параметрами, извлеченными из входящего запроса.

Результаты действий

Можно возвращать объект, описывающий результат выполнения действия (например, визуализация представления либо перенаправление на другой URL или метод действия), и затем обрабатывать его каким угодно образом. Разделение между указанием результатов и их выполнением упрощает модульное тестирование.

Фильтры

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

Если только вы не имеете дело со специфичным требованием, то лучшим подходом к созданию контроллеров будет их наследование от класса Controller, что, как и можно было ожидать, делает среда Visual Studio, когда создает новый класс в ответ на выбор пункта меню Add --> Scaffold (Добавить --> Шаблон).

В примере ниже приведен код простого контроллера под названием DerivedController, созданного подобным образом. Он сгенерирован с применением варианта MVC 5 Controller - Empty (Контроллер MVC 5 - Пустой) с несколькими простыми изменениями, предназначенными для установки свойства ViewBag и выбора представления:

Using System; using System.Web; using System.Web.Mvc; namespace ControllersAndActions.Controllers { public class DerivedController: Controller { public ActionResult Index() { ViewBag.Message = "Привет из контроллера DerivedController метода действия Index"; return View("MyView"); } } }

Класс Controller также обеспечивает связь с системой представлений Razor. В этом примере мы возвращаем результат вызова метода View(), которому в качестве параметра передается имя представления для визуализации клиенту. Чтобы создать это представление, создайте папку Views/Derived, щелкните на ней правой кнопкой мыши и выберите в контекстном меню пункт Add --> MVC 5 View Page (Razor) (Добавить --> Страница представления MVC 5 (Razor)). Укажите в качестве имени MyView.cshtml и щелкните на кнопке ОК для создания файла представления.

Приведите содержимое файла в соответствие с примером:

@{ ViewBag.Title = "Index"; }

MyView

Сообщение от контроллера: « @ViewBag.Message »

После запуска приложения и перехода на URL вида /Derived/Index этот метод действия вызывается, а представление MyView визуализируется:

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

В данной статье мы разберемся с понятием MVC, и как, на примере, можно применить это в PHP.

Понятие MVC

MVC (Model-view-controller, «Модель-представление-поведение », «Модель-представление-контроллер ») — это шаблон проектирования приложений, при котором управляющая логика поделена на три отдельных компонента таким образом, что модифицирование одного из них дает минимальное влияние на остальные.

Шаблон MVC хорошо применять при создании сложных проектов, где необходимо отделить работу php программиста (или разделить группу программистов на отделы), дизайнера, верстальщика, и т.д.

Шаблон MVC разделяет представление, данные, и обработку действий пользователя на три отдельных компонента:

MVC Модель (Model). Модель предоставляет данные (обычно для View), а также реагирует на запросы (обычно от контроллера), изменяя своё состояние.

MVC Представление (View). Отвечает за отображение информации (пользовательский интерфейс).

MVC Поведение (Controller). Интерпретирует данные, введённые пользователем, и информирует модель и представление о необходимости соответствующей реакции.

Для наглядности схемы действия шаблона MVC, ниже предоставлена иллюстрация.

Такие компоненты как представление и поведение зависят от модели, но никак не влияют на нее. Модель может иметь несколько представлений. Может быть, концепция MVCсложная для понимания, но если ее осмыслить, она становиться незаменимой при разработке приложений на PHP.

MVC в PHP

Особенностью при использовании MVC в PHP, является то, что существует одна точка входа в php приложение, которая, например, достигается следующим образом. Создается index.php через который будут обрабатываться все запросы, для этого создаем в папке с индексом файл.htaccess и помещаем в него такой код:

RewriteEngine on RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?route=$1

В предоставленном коде, первой строкой, проверяется существование запрашиваемого файла, и если его нет, то идет перенаправление на index.php, иначе даже запросы картинок сайта будут перенаправляться на индекс. Последняя строка кода преобразовывает запросы вида index.php?route=chat/index у вид index.php/chat/index. Если у вас нет возможности использовать ModRewrite в своем приложении, то вам придется делать переадресацию вручную.

PHP Модель

Данные о PHP модели содержаться в ее атрибутах и могут быть изменены только через специальные функции. Модель может содержать в себе несколько представлений. Как правило, phpмодель это класс работающий с БД, конкретнее: запись, чтение, удаление. Естественно чтение информации с БД может быть реализовано несколькими представлениями (функциями). Как пример модель статей на сайте: можно получить конкретную статью из БД, список последних, популярных, какой-то категории… это все представления модели. Для наглядности ниже предоставлен пример php модели.

PHP контролер (Поведение)

PHP контролеры получают запросы пользователей, которые мы направляли через index.php, и в соответствии с ними, корректируют работу модели. Правильнее сказать контролируют работу php приложения.

PHP Представление

Представление отслеживает изменение в модели и создает или меняет интерфейс php приложения.

List of Datas

firstname ?>

lastname?>

Как работает данный PHP MVC шаблон?

При обращении пользователем по нужному url выбирается соответственный контролер, который обращается к представлению и модели, и выводится информация. Другими словами контролер в mvc есть связующим звеном модели и представления.

Преимущества MVC шаблона при создании PHP приложения

Как упоминалось выше это, прежде всего дифференциация разработчиков php сайта на отделы. Также увеличивается скорость работы php приложения, если создается крупный проект. Ну и то, что касается непосредственно самого php разработчика, это правильная структуризация php кода (все на своих местах, так легче для понимания).

MVC пример

Особо не будем зацикливаться на примере работы MVC, так как уже имеется Добавлю лишь еще пару схем для более глубокого понимания.

Еще одна схема работы MVC шаблона на PHP, она более чем доступна для понимания.

В номере

    защитный элемент - Водяной знак: традиции и инновации

    место встречи - Это деньги завтрашнего дня

    точка зрения - Премьеры и тенденции

    ноу-хау - Двуликая защита

    ноу-хау - Весь секрет в линзах

    документ - Канадский паспорт: искусство технологий

    разработки - Защитные волокна: новые возможности

    марки - Изразцы, никель и стихи Бродского

    знаки истории - Открытки: путь от «почтовой телеграммы» до агитационного плаката

    экскурсия - Армянские драмы: деньги иллюстрируют историю

Просто проверить, сложно повторить

В последние годы Гознак активно разрабатывает и продвигает на рынок наиболее эффективные защитные технологии. Среди них выделяются два направления: по созданию элементов, видимых на просвет, и признаков, полученных за счет сочетания офсетной и металлограф

Сегодня в банкнотной отрасли как никогда востребованы эффективные технологические и защитные решения. Гознак в последние годы уделяет особое внимание разработке и продвижению на рынок таких решений, концепцию которых можно сформулировать так: разработка простых для идентификации, но сложных в воспроизведении защитных признаков, получаемых на стандартном оборудовании. Этот подход к разработке эффективных защитных технологий проиллюстрируем на примере развития двух направлений: получения оптически-переменных признаков, видимых на просвет, и признаков, полученных за счет сочетания офсетной и металлографской печати.

Просветленные технологии

Водяной знак, наблюдаемый в бумаге в проходящем свете, остается наиболее популярным защитным признаком у населения. При этом он технологичен, а опыт производства бумаги с водяными знаками исчисляется более чем семью столетиями. Именно поэтому за последние годы благодаря появлению новых технологий изготовления формных изделий этот защитный признак получил новое развитие. Многотоновые водяные знаки практически во всех модернизированных банкнотах уступили свое место водяным знакам, полученным за счет комбинирования многотоновых и филигранных знаков. А в настоящее время активно внедряется технология получения водяных знаков за счет сложных многоуровневых филиграней. Журнал «Водяной знак» неоднократно рассказывал об этих водяных знаках. Эта технология дает возможность получить не только контрастные светлые участки знака, но и изображения с высокой, нетипичной для водяных знаков линиатурой.

Защитные нити, как и контролируемые на просвет водяные знаки, имеют, в отличие от последних, не столь давнюю историю – чуть больше полутора веков. Однако они прочно обосновались на верхнем уровне защитных технологий. Это связано как с хорошей технологичностью изготовления бумаги с защитными нитями, так и с их широчайшими возможностями выступать в качестве носителя различных защитных признаков и эффектов. При кажущихся неоспоримых преимуществах по сравнению с водяными знаками значительно более высокая стоимость самих защитных нитей снижает эффективность их использования, особенно на низких номиналах, где, как правило, применяются наиболее простые и, соответственно, более дешевые варианты нитей. Именно поэтому одним из направлений развития защитных нитей на Гознаке стало получение в бумаге прозрачного окна большой площади за счет введения при отливе бумаги широкой прозрачной полимерной ленты с последующим использованием стандартных печатных технологий для получения защитных эффектов в области окна. Поскольку полимерная пленка, из которой изготавливается лента, относится к массово выпускаемой продукции и не содержит эксклюзивных защитных признаков, ее стоимость существенно ниже стоимости нитей, имеющих цветопеременные, оптически-переменные и другие высокотехнологичные признаки. При этом аналогичные высокозащищенные оптически-переменные признаки можно получить с использованием традиционных печатных технологий, что делает это техническое решение более эффективным.

Именно такой подход был реализован в памятной банкноте «Сочи 2014». В прозрачном окне, полученном за счет введения широкой полимерной ленты в бумагу во время отлива, выполнен металлографским способом оптически-переменный защитный элемент «Зебра». При рассматривании на просвет и плавном повороте банкноты можно увидеть, как изображение снежинки в окне меняется с негативного на позитивное.

А что, если не вводить полимерную ленту для получения оптически-переменного признака, контролируемого на просвет? Или, по-другому, как получить в бумаге оптически-переменный элемент, контролируемый на просвет, с использованием традиционных банкнотных технологий? Именно такая задача была поставлена перед сотрудниками Дирекции по защитным технологиям и специалистами НИИ Гознака в 2014 году. Цель очевидна: избавиться от «дополнительного» элемента – широкой полимерной ленты и сложной технологии ее введения в бумагу, т. е. сделать защитное решение еще более эффективным.

Задача оказалась очень сложной, поскольку, с одной стороны, на конечный результат влияло большое количество факторов, а, с другой стороны, основные факторы оказались не только тесно связаны друг с другом, но и находились в противоречии друг с другом. Пришлось искать нестандартные решения. К концу 2014 года после проведенной научно-исследовательской работы была доказана принципиальная возможность получения таких защитных элементов. В 2015 году ФГУП «Гознак» выпущена рекламная банкнота «Русский Авангард», представленная заместителем генерального директора по науке и развитию А. Б. Курятниковым во втором номере журнала «Водяной знак» за 2015 год. В рекламной банкноте реализован защитный элемент «Силуэт» – оптически переменный элемент, видимый в проходящем свете и выполненный с использованием традиционных полиграфических банкнотных технологий в полупрозрачном окне, полученном с использованием традиционной технологии изготовления банкнотной бумаги. В настоящее время проводится работа так как по совершенствованию технологии получения полупрозрачного окна, и по оптимизации дизайна печатных элементов.

Игра в кубики

MVC, MVC+, HMC… Эти аббревиатуры названий защитных признаков, разработанных во ФГУП «Гознак», регулярно появляются на страницах журнала начиная с 2004 года. И если собрать все статьи, написанные на эту тему, получится целая история рождения, становления и развития одного из самых эффективных, на наш взгляд, защитных направлений. Особенность этого направления заключается в том, что для воспроизведения защитных элементов используется комбинация в виде согласованных по геометрическим параметрам линий, отпечатанных офсетным и металлографским способами печати.

Появившийся в модернизированных банкнотах Банка России защитный признак MVC Moire Variable Color – был предназначен, в первую очередь, для защиты от копирования. Напомним, как работает признак: на изначально однородном поле при наклоне банкноты появляются муаровые цветные полосы. На копии этот оптически-переменный эффект отсутствует, т. е. или цветные муаровые полосы не появляются, или обнаруживаются сразу, и картина остается без изменений при любых наклонах и поворотах банкноты. Потенциал этого признака оказался гораздо выше первоначально предполагаемого благодаря высокой стойкости к имитациям, технологичности, износостойкости и возможностям его дальнейшей модернизации. Простота его реализации в банкнотах городской серии модернизации 2004 г. и ожидаемые специалистами Гознака в связи с этим скорые имитации заставили модернизировать этот защитный признак в направлении создания более сложной для воспроизведения фальшивомонетчиками муаровой картины, обусловленной применением нелинейной структуры линий и применением комбинации бескрасочного тиснения и красочной металлографской печати. Так появилась следующая генерация оптически-переменного признака MVC+. Этот защитный элемент имеет две согласованные между собой области. В нижней области рисунок муара виден под любым углом, а в верхней области, как и в случае MVC, он появляется только под определенным углом. Очень важно знать, что при наклоне банкноты рисунок муара верхней и нижней частей должен образовать одну неразрывную картину без смещения муаровых линий на границе этих двух областей. Кроме того, этот защитный признак усилен кассовым уровнем защиты. При рассматривании элемента MVC+ под воздействием УФ-излучения можно наблюдать точно такой же муарообразующий эффект, как и при дневном свете. Защитный элемент MVC+ применен в банкнотах Банка России номиналом 1000 и 5000 рублей модернизации 2010 года.

Параллельно с MVC+ велись разработки нового защитного элемента, обладающего большим визуальным эффектом. И к 2010 году был создан новый защитный признак HMC (Hidden Multi Color), который стал еще более эффективным защитным элементом в этой серии признаков. Благодаря изменению геометрических параметров офсетных и металлографских линий при наклоне банкноты изначально однородное поле разбивается на отдельные фрагменты, окрашенные в разные цвета. В качестве цветных фрагментов используются цифры, текстовые символы, геометрические фигуры, любые произвольные области. Обычно применяется не более 2–3 цветов. Важной особенностью этого защитного признака является возможность дополнительной проверки его подлинности. Если запомнить цвета, видимые при наклоне банкноты, а потом развернуть банкноту в ее плоскости на 180 градусов, то можно увидеть совершенно другие цвета фрагментов. Этот эффект получен благодаря специальной форме линий и использованию уникального оборудования для изготовления металлографских форм. Как и у элемента MVC+, у защитного элемента HMC существует дополнительный кассовый уровень проверки подлинности: под воздействием УФ-излучения можно увидеть точно такие же оптически-переменные эффекты, как и при дневном свете. Защитный элемент HMC был внедрен в защитный комплекс банкноты Банка России номиналом 500 рублей модификации 2010 года.

Для получения защитных элементов серии MVC – HMC используются металлографские линии с достаточно большой глубиной рельефа. В условиях очень высокого давления при металлографской печати бумага деформируется, принимая форму профиля металлографских линий. Образующийся при этом рельеф возникает и на лицевой, и на оборотной стороне печатного листа. Если рельеф лицевой стороны «работает» в защитных признаках серии MVC – HMC, то оборотный рельеф до недавнего времени не использовался. Специалисты Гознака предложили интересное решение – создание оптически-переменных элементов и на лицевой, и на оборотной стороне банкноты при металлографской печати только с лицевой стороны. Такой элемент был разработан и реализован на рекламной банкноте «195 лет Гознака». Подробное описание этого элемента, получившего название CHMC (Сombined HMC) приведено в журнале «Водяной знак» №3 за 2013 г. Кроме получения оптически-переменных признаков на двух сторонах банкноты за счет использования важной технологической особенности офсетной печатной машины – обеспечения точной приводки печати лицевой и оборотной сторон, – получен элемент для контроля совмещения лицевой и оборотной сторон. Таким образом, CHMC – это «три в одном», т. е. оптические признаки с обеих сторон банкноты и элемент для контроля совмещения лицевой и оборотной сторон. Важной особенностью этого элемента является то, что на лицевой и оборотной сторонах банкноты можно получать независимо как MVC, так и HMC или их комбинации. Так, на рекламной банкноте «Русский Авангард» на лицевой стороне применен элемент HMC, а на оборотной – комбинация MVC и HMC.

Для получения наилучшего визуального эффекта при создании признаков серии MVC – HMC, особенно HMC, необходимо использовать при печати офсетных линий яркие контрастные цвета. Идеальный случай – применять цвета CMY. Однако часто при модернизации банкнот заказчик не разрешает менять цвета или использовать такие яркие цвета для офсетной печати. Поэтому приходится идти на компромисс между дизайном и визуальным эффектом. Особенно это актуально для элемента HMC. Именно для таких «сложных» в цветовом отношении банкнот были разработаны двух- и даже однокрасочные оптически-переменные элементы HMC. При этом однокрасочный элемент формально является двухкрасочным, поскольку в качестве второй краски используется пробел, т. е. цвет бумаги. Поэтому при наклоне банкноты цвет не меняется, появляется позитивное или негативное изображение.

Кроме того, любой из элементов серии MVC – HMC может быть дополнен скрытым или латентным изображением.

Таким образом, сегодня создана серия эффективных оптически-переменных защитных элементов, получаемых комбинацией офсетной и металлографской печати. Это своеобразный набор из кубиков, который можно использовать для построения уникального защитного комплекса для различных банкнот.

Развитие оптически-переменных защитных элементов серии MVC – HMC продолжается. Есть новые идеи. И вполне возможно, что в новой рекламной банкноте или каком-либо тиражном изделии в скором времени появится новая реализация защитного признака, основанного на комбинации офсетной и металлографской печати.

Паттерн Model-View-Controller (MVC) является крайне полезным при создании приложений со сложным графическим интерфейсом или поведением. Но и для более простых случаев он также подойдет. В этой заметке мы создадим игру сапер, спроектированную на основе этого паттерна. В качестве языка разработки выбран Python, однако особого значения в этом нет. Паттерны не зависят от конкретного языка программирования и вы без труда сможете перенести получившуюся реализацию на любую другую платформу.

Реклама

Коротко о паттерне MVC

Как следует из названия, паттерн MVC включает в себя 3 компонента: Модель, Представление и Контроллер. Каждый из компонентов выполняет свою роль и является взаимозаменяемым. Это значит, что компоненты связаны друг с другом лишь некими четкими интерфейсами, за которыми может лежать любая реализация. Такой подход позволяет подменять и комбинировать различные компоненты, обеспечивая необходимую логику работы или внешний вид приложения. Разберемся с теми функциями, которые выполняет каждый компонент.

Модель

Отвечает за внутреннюю логику работы программы. Здесь мы можем скрыть способы хранения данных, а также правила и алгоритмы обработки информации.

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

Представление

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

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

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

Контроллер

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

Фактически на каждое действие, которое может сделать пользователь в Представлении, должен быть определен обработчик в Контроллере. Этот обработчик выполнит соответствующие манипуляции над моделью и в случае необходимости сообщит Представлению о наличии изменений.

Реклама

Спецификации игры Сапер

Достаточно теории. Теперь перейдем к практике. Для демонстрации паттерна MVC мы напишем несложную игру: Сапер. Правила игры достаточно простые:

  1. Игровое поле представляет собой прямоугольную область, состоящую из клеток. В некоторых клетках случайным образом расположены мины, но игрок о них не знает;
  2. Игрок может щелкнуть по любой клетке игрового поля левой или правой кнопками мыши;
  3. Щелчок левой кнопки мыши приводит к тому, что клетка будет открыта. При этом, если в клетке находится мина, то игра завершается проигрышем. Если в соседних клетках, рядом с открытой, расположены мины, то на открытой клетке отобразится счетчик с числом мин вокруг. Если же мин вокруг открытой клетки нет, то каждая соседняя клетка будет открыта по тому же принципу. То есть клетки будут открываться до тех пор, пока либо не упрутся в границу игрового поля, либо не дойдут до уже открытых клеток, либо рядом с ними не окажется мина;
  4. Щелчок правой кнопки мыши позволяет делать пометки на клетках. Щелчок на закрытой клетке помечает ее флажком, который блокирует ее состояние и предотвращает случайное открытие. Щелчок на клетке, помеченной флажком, меняет ее пометку на вопросительный знак. В этом случае клетка уже не блокируется и может быть открыта левой кнопкой мыши. Щелчок на клетке с вопросительным знаком возвращает ей закрытое состояние без пометок;
  5. Победа определяется состоянием игры, при котором на игровом поле открыты все клетки, за исключением заминированных.

Пример того, что у нас получится приведен ниже:

UML-диаграммы игры Сапер

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

Диаграмма Состояний игровой клетки

Любая клетка на игровом поле может находиться в одном из 4 состояний:

  1. Клетка закрыта;
  2. Клетка открыта;
  3. Клетка помечена флажком;
  4. Клетка помечена вопросительным знаком.

Здесь мы определили лишь состояния, значимые для Представления. Поскольку мины в процессе игры не отображаются, то и в базовом наборе соответствующего состояния не предусмотрено. Определим возможные переходы из одного состояния клетки в другое с помощью UML Диаграммы Состояний:

Диаграмма Классов игры Сапер

Поскольку мы решили создавать наше приложение на основе паттерна MVC, то у нас будет три основных класса: MinesweeperModel , MinesweeperView и MinesweeperController , а также вспомогательный класс MinesweeperCell для хранения состояния клетки. Рассмотрим их диаграмму классов:

Организация архитектуры довольно проста. Здесь мы просто распределили задачи по каждому классу в соответствии с принципами паттерна MVC:

  1. В самом низу иерархии расположен класс игровой клетки MinesweeperCell . Он хранит позицию клетки, определяемую рядом row и столбцом column игрового поля; одно из состояний state , которые мы описали в предыдущем подразделе; информацию о наличии мины в клетке (mined) и счетчик мин в соседних клетках counter . Кроме того, у него есть два метода: nextMark() для циклического перехода по состояниям, связанным с пометками, появляющимися в результате щелчка правой кнопкой мыши, а также open() , который обрабатывает событие, связанное с щелчком левой кнопкой мыши;
  2. Чуть выше расположен класс Модели MinesweeperModel . Он является контейнером для игровых клеток MinesweeperCell . Его первый метод startGame() подготавливает игровое поле для начала игры. Метод isWin() делает проверку игрового поля на состояние выигрыша и возвращает истину, если игрок победил, иначе возвращается ложь. Для проверки проигрыша предназначен аналогичный метод isGameOver() . Методы openCell() и nextCellMark() всего лишь делегируют действия соответствующим клеткам на игровом поле, а метод getCell() возвращает запрашиваемую игровую клетку;
  3. Класс Представления MinesweeperView включает следующие методы: syncWithModel() - обеспечивает перерисовку Представления для отображения актуального состояния игрового поля в Модели; getGameSettings() - возвращает настройки игры, заданные пользователем; createBoard() - создает игровое поле на основе данных Модели; showWinMessage() и showGameOverMessage() соответственно отображают сообщения о победе и проигрыше;
  4. И наконец класс Контроллера MinesweeperController . В нем определено всего три метода на каждое возможное действие игрока: startNewGame() отвечает за нажатие на кнопке "Новая игра" в интерфейсе Представления; onLeftClick() и onRightClick() обрабатывают щелчки по игровым клеткам левой и правой кнопками мыши соответственно.

Реализация игры Сапер на Python

Пришло время заняться реализацией нашего проекта. В качестве языка разработки выберем Python. Тогда класс Представления будем писать на основе модуля tkinter .

Но начнем с Модели.

Модель MinsweeperModel

Реализация модели на языке Python выглядит следующим образом:

MIN_ROW_COUNT = 5 MAX_ROW_COUNT = 30 MIN_COLUMN_COUNT = 5 MAX_COLUMN_COUNT = 30 MIN_MINE_COUNT = 1 MAX_MINE_COUNT = 800 class MinesweeperCell: # Возможные состояния игровой клетки: # closed - закрыта # opened - открыта # flagged - помечена флажком # questioned - помечена вопросительным знаком def __init__(self, row, column): self.row = row self.column = column self.state = "closed" self.mined = False self.counter = 0 markSequence = [ "closed", "flagged", "questioned" ] def nextMark(self): if self.state in self.markSequence: stateIndex = self.markSequence.index(self.state) self.state = self.markSequence[ (stateIndex + 1) % len(self.markSequence) ] def open(self): if self.state != "flagged": self.state = "opened" class MinesweeperModel: def __init__(self): self.startGame() def startGame(self, rowCount = 15, columnCount = 15, mineCount = 15): if rowCount in range(MIN_ROW_COUNT, MAX_ROW_COUNT + 1): self.rowCount = rowCount if columnCount in range(MIN_COLUMN_COUNT, MAX_COLUMN_COUNT + 1): self.columnCount = columnCount if mineCount < self.rowCount * self.columnCount: if mineCount in range(MIN_MINE_COUNT, MAX_MINE_COUNT + 1): self.mineCount = mineCount else: self.mineCount = self.rowCount * self.columnCount - 1 self.firstStep = True self.gameOver = False self.cellsTable = for row in range(self.rowCount): cellsRow = for column in range(self.columnCount): cellsRow.append(MinesweeperCell(row, column)) self.cellsTable.append(cellsRow) def getCell(self, row, column): if row < 0 or column < 0 or self.rowCount <= row or self.columnCount <= column: return None return self.cellsTable[ row ][ column ] def isWin(self): for row in range(self.rowCount): for column in range(self.columnCount): cell = self.cellsTable[ row ][ column ] if not cell.mined and (cell.state != "opened" and cell.state != "flagged"): return False return True def isGameOver(self): return self.gameOver def openCell(self, row, column): cell = self.getCell(row, column) if not cell: return cell.open() if cell.mined: self.gameOver = True return if self.firstStep: self.firstStep = False self.generateMines() cell.counter = self.countMinesAroundCell(row, column) if cell.counter == 0: neighbours = self.getCellNeighbours(row, column) for n in neighbours: if n.state == "closed": self.openCell(n.row, n.column) def nextCellMark(self, row, column): cell = self.getCell(row, column) if cell: cell.nextMark() def generateMines(self): for i in range(self.mineCount): while True: row = random.randint(0, self.rowCount - 1) column = random.randint(0, self.columnCount - 1) cell = self.getCell(row, column) if not cell.state == "opened" and not cell.mined: cell.mined = True break def countMinesAroundCell(self, row, column): neighbours = self.getCellNeighbours(row, column) return sum(1 for n in neighbours if n.mined) def getCellNeighbours(self, row, column): neighbours = for r in range(row - 1, row + 2): neighbours.append(self.getCell(r, column - 1)) if r != row: neighbours.append(self.getCell(r, column)) neighbours.append(self.getCell(r, column + 1)) return filter(lambda n: n is not None, neighbours)

В верхней части мы определяем диапазон допустимых настроек игры:

MIN_ROW_COUNT = 5 MAX_ROW_COUNT = 30 MIN_COLUMN_COUNT = 5 MAX_COLUMN_COUNT = 30 MIN_MINE_COUNT = 1 MAX_MINE_COUNT = 800

Вообще, эти настройки можно было сделать тоже частью Модели. Однако размеры поля и количество мин достаточно статичная информация и вряд ли будет часто меняться.

Затем мы определили класс игровой клетки MinesweeperCell . Она оказалась достаточно простой. В конструкторе класса происходит инициализация полей клетки значениями по умолчанию. Далее для упрощения реализации циклических переходов по состояниям мы используем вспомогательный список markSequence . Если клетка находится в состоянии "opened" , которое не входит в этот список, то в методе nextMark() ничего не произойдет, иначе клетка попадает в следующее состояние, причем, из последнего состояния "questioned" она "перепрыгивает" в начальное состояние "closed" . В методе open() мы проверяем состояние клетки, и если оно не равно "flagged" , то клетка переходит в открытое состояние "opened" .

Далее следует определение класса Модели MinesweeperModel . Метод startGame() осуществляет компоновку игрового поля по переданным ему параметрам rowCount , columnCount и mineCount . Для каждого из параметров происходит проверка на попадание в допустимый диапазон значений. Если переданное значение находится вне диапазона, то сохраняется то значение параметра игрового поля не меняется. Следует отметить, что для числа мин предусмотрена дополнительная проверка. Если переданное количество мин превышает размер поля, то мы ограничиваем его количеством клеток без единицы. Хотя, конечно, такая игра особого смысла не имеет и будет закончена в один шаг, поэтому вы можете придумать какое-нибудь свое правило на такой случай.

Игровое поле хранится в виде списка списков клеток в переменной cellsTable . Причем, обратите внимание, что в методе startGame() у клеток устанавливается лишь значение позиции, но мины еще не расставляются. Зато определяется переменная firstStep со значением True . Это нужно для того, чтобы убрать элемент случайности из первого хода и не допускать мгновенный проигрыш. Мины будут расставляться после первого хода в оставшихся клетках.

Метод getCell() просто возвращает клетку игрового поля по строке row и столбцу column . Если значение строки или столбца неверно, то возвращается None .

Метод isWin() возвращает True , если все оставшиеся не открытые клетки игрового поля заминированы, то есть в случае победы, иначе вернется False . А метод isGameOver() просто возвращает значение атрибута класса gameOver .

В методе openCell() происходит делегирование вызова open() объекту игровой клетки, которая расположена на игровом поле в позиции, указанной в параметрах метода. Если открытая клетка оказалось заминированной, то мы устанавливаем значение gameOver в True и выходим из метода. Если игра еще не окончена, то мы смотрим, а не первый ли это ход, проверяя значение firstStep . Если ход и правда первый, то произойдет расстановка мин по игровому полю с помощью вспомогательного метода generateMines() , о которой мы поговорим немного позже. Далее мы подсчитываем количество заминированных соседних клеток и устанавливаем соответствующее значение атрибута counter для обрабатываемой клетки. Если счетчик counter равен нулю, то мы запрашиваем список соседних клеток с помощью метода getCellNeighbours() и осуществляем рекурсивный вызов метода openCell() для всех закрытых "соседей", то есть для клеток со статусом "closed" .

Метод nextCellMark() всего лишь делегирует вызов методу nextMark() для клетки, расположенной на переданной позиции.

Расстановка мин происходит в методе generateMines() . Здесь мы просто случайным образом выбираем позицию на игровом поле и проверяем, чтобы клетка на этой позиции не была открыта и не была уже заминирована. Если оба условия выполнены, то мы устанавливаем значение атрибута mined равным True , иначе продолжаем поиск другой свободной клетки. Не забудьте, что для того, чтобы использовать на Python модуль random нужно явным образом его импортировать командой import random .

Метод подсчета количества мин countMinesAroundCell() вокруг некоторой клетки игрового поля полностью основывается на методе getCellNeighbours() . Запрос "соседей" клетки в методе getCellNeighbours() тоже реализован крайне просто. Не думаю, что у вас возникнут с ним проблемы.

Представление MinesweeperView

Теперь займемся представлением. Код класса MinesweeperView на Python представлен ниже:

Class MinesweeperView(Frame): def __init__(self, model, controller, parent = None): Frame.__init__(self, parent) self.model = model self.controller = controller self.controller.setView(self) self.createBoard() panel = Frame(self) panel.pack(side = BOTTOM, fill = X) Button(panel, text = "Новая игра", command = self.controller.startNewGame).pack(side = RIGHT) self.mineCount = StringVar(panel) self.mineCount.set(self.model.mineCount) Spinbox(panel, from_ = MIN_MINE_COUNT, to = MAX_MINE_COUNT, textvariable = self.mineCount, width = 5).pack(side = RIGHT) Label(panel, text = " Количество мин: ").pack(side = RIGHT) self.rowCount = StringVar(panel) self.rowCount.set(self.model.rowCount) Spinbox(panel, from_ = MIN_ROW_COUNT, to = MAX_ROW_COUNT, textvariable = self.rowCount, width = 5).pack(side = RIGHT) Label(panel, text = " x ").pack(side = RIGHT) self.columnCount = StringVar(panel) self.columnCount.set(self.model.columnCount) Spinbox(panel, from_ = MIN_COLUMN_COUNT, to = MAX_COLUMN_COUNT, textvariable = self.columnCount, width = 5).pack(side = RIGHT) Label(panel, text = "Размер поля: ").pack(side = RIGHT) def syncWithModel(self): for row in range(self.model.rowCount): for column in range(self.model.columnCount): cell = self.model.getCell(row, column) if cell: btn = self.buttonsTable[ row ][ column ] if self.model.isGameOver() and cell.mined: btn.config(bg = "black", text = "") if cell.state == "closed": btn.config(text = "") elif cell.state == "opened": btn.config(relief = SUNKEN, text = "") if cell.counter > 0: btn.config(text = cell.counter) elif cell.mined: btn.config(bg = "red") elif cell.state == "flagged": btn.config(text = "P") elif cell.state == "questioned": btn.config(text = "?") def blockCell(self, row, column, block = True): btn = self.buttonsTable[ row ][ column ] if not btn: return if block: btn.bind("", "break") else: btn.unbind("") def getGameSettings(self): return self.rowCount.get(), self.columnCount.get(), self.mineCount.get() def createBoard(self): try: self.board.pack_forget() self.board.destroy() self.rowCount.set(self.model.rowCount) self.columnCount.set(self.model.columnCount) self.mineCount.set(self.model.mineCount) except: pass self.board = Frame(self) self.board.pack() self.buttonsTable = for row in range(self.model.rowCount): line = Frame(self.board) line.pack(side = TOP) self.buttonsRow = for column in range(self.model.columnCount): btn = Button(line, width = 2, height = 1, command = lambda row = row, column = column: self.controller.onLeftClick(row, column), padx = 0, pady = 0) btn.pack(side = LEFT) btn.bind("", lambda e, row = row, column = column: self.controller.onRightClick(row, column)) self.buttonsRow.append(btn) self.buttonsTable.append(self.buttonsRow) def showWinMessage(self): showinfo("Поздравляем!", "Вы победили!") def showGameOverMessage(self): showinfo("Игра окончена!", "Вы проиграли!")

Наше Представление основано на классе Frame из модуля tkinter , поэтому не забудьте выполнить соответствующую команду импорта: from tkinter import * . В конструкторе класса передаются Модель и Контроллер. Сразу же вызывается метод createBoard() для компоновки игрового поля из клеток. Скажу заранее, что для этой цели мы будем использовать обычные кнопки Button . Затем создается Frame , который будет выполнять роль нижней панели для указания параметров игры. На эту панель мы последовательно помещаем кнопку "Новая игра", обработчиком которой становится наш Контроллер с его методом startNewGame() , а затем три счетчика Spinbox для того, чтобы игрок мог указать размер игрового поля и число мин.

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

Кроме того, обратите внимание, что для представления открытой клетки мы используем стиль кнопки SUNKEN . А в случае проигрыша открываем местоположение всех мин на игровом поле, показывая соответствующие кнопки черным цветом, а кнопку, отвечающую последней открытой клетке с миной, выделяем красным цветом:

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

Метод getGameSettings() всего лишь возвращает значения размещенных в нижней панели счетчиков с размером игрового поля и количеством мин.

Создание представления игрового поля осуществляется в методе createBoard() . В первую очередь идет попытка удаления старого игрового поля, если оно существовало, а также мы пробуем установить значения счетчиков из панели в соответствии с текущей конфигурацией Модели. Затем создается новый Frame , который мы назовем board , для представления игрового поля. Таблицу кнопок buttonsTable мы компонуем по тому же принципу, что и игровые клетки в Модели с помощью двойного цикла. Обработчики каждой кнопки привязываются к методам Контроллера onLeftClick() и onRightClick() для щелчка левой и правой кнопок мыши соответственно.

Последние два метода showWinMessage() и showGameOverMessage() всего лишь отображают диалоговые окна с соответствующими сообщениями с помощью функции showinfo() . Для того, чтобы ей воспользоваться вам понадобится импортировать еще один модуль: from tkinter.messagebox import * .

Контролер MinesweeperController

Вот мы и дошли до реализации Контроллера:

Class MinesweeperController: def __init__(self, model): self.model = model def setView(self, view): self.view = view def startNewGame(self): gameSettings = self.view.getGameSettings() try: self.model.startGame(*map(int, gameSettings)) except: self.model.startGame(self.model.rowCount, self.model.columnCount, self.model.mineCount) self.view.createBoard() def onLeftClick(self, row, column): self.model.openCell(row, column) self.view.syncWithModel() if self.model.isWin(): self.view.showWinMessage() self.startNewGame() elif self.model.isGameOver(): self.view.showGameOverMessage() self.startNewGame() def onRightClick(self, row, column): self.model.nextCellMark(row, column) self.view.blockCell(row, column, self.model.getCell(row, column).state == "flagged") self.view.syncWithModel()

Для привязки Представления к Контроллеру мы добавили метод setView() . Это объясняется тем, что если бы мы хотели передать Представление в конструктор, то это Представление должно было бы уже существовать до момента создания Контроллера. А тогда подобное решение с дополнительным методом для привязки просто перешло бы от Контроллера к Представлению, в которым бы появился метод setController() .

Метод-обработчик для нажатия на кнопке "Новая игра" startNewGame() сначала запрашивает параметры игры, введенные в Представление. Параметры игры возвращаются в виде кортежа из трех компонент, которые мы пытаемся преобразовать в int . Если все пройдет нормально, то мы передаем эти значения в метод Модели startGame() для построения игрового поля. Если же что-то пойдет не так, то мы просто пересоздадим игровое поле со старыми параметрами. А в завершении мы направляем запрос на создание нового отображения игрового поля в Представлении с помощью вызова метода createBoard() .

Обработчик onLeftClick() сначала указывает Модели на необходимость открыть игровую клетку в выбранной игроком позиции. Затем сообщает Представлению о том, что состояние Модели изменилось и предлагает все перерисовать. Затем происходит проверка Модели на состояние победы или проигрыша. Если что-то из этого произошло, то сначала в Представление направляется запрос на отображение соответствующего уведомления, а затем происходит вызов обработчика startNewGame() для начала новой игры.

Щелчок правой кнопкой мыши обрабатывается в методе onRightClick() . В первой строке происходит вызов метода Модели nextCellMark() для циклической смены метки выбранной игровой клетки. В зависимости от нового состояния клетки Представлению отправляется запрос на установку или снятие блокировки на соответствующую кнопку. А в конце вновь обеспечивается обновление вида Представления для отображения актуального состояния Модели.

Комбинируем Модель, Представление и Контроллер

Теперь осталось лишь соединить все элементы в рамках нашей реализации Сапера на основе паттерна MVC и запустить игру:

Model = MinesweeperModel() controller = MinesweeperController(model); view = MinesweeperView(model, controller) view.pack() view.mainloop()

Заключение

Вот мы и рассмотрели паттерн MVC. Коротко прошлись по теории. А потом по шагам создали полноценное игровое приложение, пройдя путь от постановки задачи и проектирования архитектуры до реализации на языке программирования Python с использованием графического модуля tkinter .

Паттерн Model-View-Controller (MVC) , открытый в в конце 1970-х, представляет собой шаблон проектирования архитектуры программного обеспечения, основной задачей которого является отделение функций работы с данными от их представления. Теоретически, грамотно спроектированное MVC-приложение позволит фронтенд и бэкенд разработчикам в ходе работы не вмешиваться в зоны ответственности друг друга, то есть фронтенд-разработчику не понадобиться что-либо знать о «кухне» своего бэкенд-коллеги и наоборот.

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

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

Понимание MVC

Как уже было сказано, название паттерна происходит от аббревиатуры трёх слов: Model (модель), View (представление) и Controller (контроллер) . Вкратце принцип работы паттерна можно проиллюстрировать одной схемой ( можно найти на Википедии):

Эта схема наглядно показывает однонаправленность потока информации в паттерне, а также описывает роли каждого компонента.

Модель

Модель используется для доступа и манипулирования данными. В большинстве случаев модель — это то, что используется для доступа к хранилищу данных (например, базе данных). Модель предоставляет интерфейс для поиска данных, их создания, модификации и удаления из хранилища. В контексте паттерна MVC модель является посредником между представлением и контроллером.

Крайне важной чертой модели является то, что технически она не имеет никаких знаний ни о том, что происходит с данными в контроллере и представлении. Модель никогда не должна делать или ожидать каких-либо запросов в/из других компонентов паттерна.

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

Представление

Представление — это то, где данные, полученные от модели, выводятся в нужном виде. В традиционных веб-приложениях, разработанных в рамках MVC-паттерна, представление — это часть системы, где выполняется генерация HTML-кода. Представление также отвечает за получение действий от пользователя с тем чтобы отправить их контроллеру. Например, представление отображает кнопку в пользовательском интерфейсе, а после её нажатия вызывает соответствующее действие контроллера.

Существуют некоторые заблуждения относительно предназначения представления, особенно в среде веб-разработчиков, которые только начинают строить свои приложения с использованием MVC. Одним из наиболее часто нарушаемых правил является то, что представление никоим образом не должно общаться с моделью , а все данные, получаемые представлением должны поступать только от контроллера . На практике же разработчики часто игнорируют эту концепцию, стоящую в основах MVC-паттерна. В статье Fabio Cevasco наглядно показан этот сбивающий с толку подход к MVC на примере фреймворка CakePHP, одним из многих нестандартных MVC-фреймворков:

Крайне важно понимать, что для того, чтобы получить правильную MVC-архитектуру, не должно быть никаких прямых взаимодействий между представлениями и моделями. Вся логика обмена данными между ними должна быть реализована в контроллерах.

Помимо этого, существует распространённое заблуждение о том, что представление — это просто темплейт-файл. Как заметил Tom Butler, это заблуждение имеет огромный масштаб из-за того, что многие разработчики с самого начала неправильно понимают структуру MVC, после чего начинают вливать эти «знания» дальше, массы начинающих разработчиков. В действительности представление — это гораздо больше, чем просто темплейт, однако много фреймворков, построенных на базе MVC-паттерна, настолько исказили концепцию представления, что уже всем пофигу, насколько правильными являются их приложения с точки зрения MVC-паттерна.

Также важным моментом является то, что представление никогда не работает с «чистыми» данными от контроллера, то есть контроллер никогда не работает с представлением в обход модели. В процессе взаимодействия контроллера и представления модель всегда должна находиться между ними.

Контроллер

Контроллер — это последняя часть связки MVC. Задачей контроллера является получение данных от пользователя и манипуляция моделью. Именно контроллер, и только он, является той частью системы, которая взаимодействует с пользователем.

В двух словах контроллер можно описать как сборщик информации, передающий её модели для обработки и хранения. Он не должен делать ничего с данными, а только лишь уметь получать их от пользователя. Контроллер связан с одним представлением и одной моделью, организуя таким образом однонаправленный поток данных, контролируя его на каждом этапе.

Очень важно запомнить, что что контроллер начинает свою работу только в результате взаимодействия пользователя с представлением, которое вызывает соответствующую функцию контроллера. Самая распространённая ошибка среди разработчиков заключается в том, что контроллер рассматривается просто как шлюз между представлением и моделью. В результате чего контроллер наделяется теми функциями, который должны выполняться представлением (кстати, вот откуда растут ноги у идеи, что представление — это просто темплейт-файл). Вдобавок ко всему многие вообще сваливают всю логику обработки данных, забывая о том, для чего в паттерне MVC предназначена модель.

MVC в PHP

Предлагаю попробовать реализовать описанное выше в небольшом приложении. Начнём с того, что создадим классы модели, представления и контроллера:

string = "MVC + PHP = Awesome!"; } } controller = $controller; $this->

" . $this->model->string . "

"; } } model = $model; } }

Основные классы готовы. Теперь давайте свяжем их вместе и запустим наше приложение:

output();

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

Давайте немного расширим приложение, добавив немного интерактивности, чтобы увидеть, как работает контроллер:

string = “MVC + PHP = Awesome, click here!”; } } controller = $controller; $this->model = $model; } public function output() { return "

model->string . "

"; } } model = $model; } public function clicked() { $this->model->string = “Updated Data, thanks to MVC and PHP!” } }

И в завершение немного модернизируем связующий код:

{$_GET["action"]}(); } echo $view->output();

Итоги

В этой небольшой статье мы рассмотрели основные концепции шаблона проектирования MVC и разработали простенькое приложение на его базе, хотя конечно, нам ещё очень далеко до того, чтобы использовать это в реальной жизни. В следующей статье мы рассмотрим основные затруднения, с которыми вы столкнётесь, если плотнее займётесь построением архитектуры приложения на базе MVC-паттерна. Stay tuned!