Паттерны для новичков: MVC vs MVP vs MVVM. Введение в MVC онлайн урок

Что такое MVC?

Итак, MVC - это про пользовательский интерфейс (UI). Не обязательно графический, голосовое управление тоже годится. Не забудем, что программа может не иметь пользовательского интерфейса, может иметь программный интерфейс (API) или вообще никакого не иметь и всё ещё быть полезной.

Но если у нас есть пользователь, значит должен быть пользовательский интерфейс. Что же такое интерфейс? Это смежная граница между двумя системами. В нашем случае: с одной стороны - программа, с другой - пользователь. Вот они.

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

Юзкейсы

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

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

Но на бирже нельзя изменить заявку, в предметной области нет такого понятия. Заявку можно только выставить и отменить. Чтобы дать пользователю возможность в один клик менять заявку, надо запоминать старые значения, снимать заявку, давать редактировать то, что запомнили, и выставлять новую заявку. Такая комбинация. Но для пользователя она выглядит как одно простое действие: изменение заявки. Это называется - use case.

Дополним нашу диаграмму местом под юзкейсы.

Ещё пользователю надо дать возможность дёргать эти юзкейсы и получать результат. Это могут быть кнопки и другие графические элементы ввода-вывода, жесты, распознавание и синтез речи. Любой вариант обмена данными и командами. Вуаля:

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

Так где же тут все-таки MVC?

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

Когда модель публикует изменения, её не волнует для кого, она ничего не знает про View. Вместо или вместе со View на том конце может быть другая подсистема.

Теперь немного частностей.

Это был классический вариант MVC - Active Model. Бывает и так, что модель не оповещает об изменениях. Тогда эту обязанность берёт на себя контроллер. Он знает, какие манипуляции производит над моделью, и, очевидно, знает, какие изменения в состоянии модели могут последовать. Это Passive Model.

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

За материал благодарим нашего подписчика Станислава Ильичева

Паттерн 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!

SRC:
O’Rеillу. ActionScript 3.0 Design Patterns.
Глава 12. Model-View-Controller Pattern.


Что такое шаблон Модель-Представление-Контроллер?

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

Модель (Model)
Содержит данные и логику приложения для управления состоянием этого приложения

Представление (View)
Реализует пользовательский интерфейс и состояние приложения, наблюдаемые на экране

Контроллер (Controller)
Обрабатывает действия пользователя, влияющие на состояние приложения.

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

Модель

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

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

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

Контроллер

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

Допустим, наш цифровой плеер имеет кнопки Громче и Тише в Представлении. Громкость звука является переменной состояния. Модель будет отслеживать эту переменную, чтобы менять значение этой переменной в соответствии с логикой приложения. Если значение громкости звука проградуировать от 0 до 10, Контроллер определит, насколько нужно прибавить или убавить звук при одиночном нажатии на одну из этих кнопок. Поведение может сообщить Модели, что нужно увеличить громкость на 0,5 или на 0,1, или любое другое значение, как задано программно. В таком ключе, Контроллер - это специфичные реализации, которые определяют, каким образом приложение ответит на ввод пользователя.

Хотя каждый элемент в триаде MVC имеет отдельную и уникальную зону ответственности, они не функционируют в изоляции. На самом деле, с тем чтобы составить шаблон MVC, каждому элементу следует коммуницировать с остальными. Что это значит, мы рассмотрим ниже.

Взаимодействие элементов MVC

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

  1. Пользователь взаимодействует с элементом интерфейса (например, нажимает на кнопку в Представлении).
  2. Представление отсылает событие нажатия Контроллеру, чтобы решить, как это нажатие обработать.
  3. Контроллер меняет Модель на основе того, что он решил относительно нажатия кнопки.
  4. Модель информирует Представление о том, что состояние Модели изменилось.
  5. Представление читает информацию о состоянии в Модели и самостоятельно видоизменяется.

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

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

Другой особенностью шаблона MVC является то, что каждая такая Модель может иметь более одного Представления, ассоциированного с ней. Например, в нашем портативном плеере установки громкости могут быть отображены на дисплее с помощью индикатора уровня. А кроме того, уровень звука на аудио-выходе соотносится с громкостью звука в наушниках. И дисплей, и звук в наушниках являются Представлениями состояния устройства.

Взгляните на Рисунок 12-1 и обратите внимание на направление стрелок. Они показывают, кто инициирует взаимодействие между элементами. Для того, чтобы один MVC-элемент смог сообщаться с другим, ему нужно знать о нем и владеть ссылкой на этот элемент.

Думайте о Модели, Представлении и Контроллере как о трех разных классах. Давайте посмотрим, каким классам нужно иметь ссылки на остальные классы:

Представление
Ему нужно иметь ссылку и на Модель, и на Контроллер

Контроллер
Ему необходимо владеть ссылкой на Модель

Мы начали с того что заявили, что MVC – это составной шаблон, который объединяет несколько шаблонов. Вам, должно быть, интересно, какие шаблоны вошли в данный составной шаблон. Или, точнее, чем они могут быть представлены? Главным преимуществом использования шаблона MVC является возможность разъединять его на составляющие три элемента. Это позволяет нам увязать несколько Представлений с одной Моделью, заменять Модели и Контроллеры, не затрагивая другие элементы. Но некоторые элементы в триаде MVC должны поддерживать ссылки на остальные элементы, а также поддерживать активный обмен данными между ними. Как мы можем назвать такое разделение? Это имеет отношение к паттернам Observer (Обозреватель) , Strategy (Стратегия) и Composite (Компоновщик) .

Внедрение шаблонов в MVC

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

Каждая Модель может иметь несколько Представлений, с ней связанных. Так же, Представления могут быть сложными, с несколькими окнами или панелями, внутри которых находятся другие элементы пользовательского интерфейса. Например, такие элементы интерфейса как кнопки, текстовые поля, списки, ползунки и т.п. могут быть сгруппированы в панель с закладками, а панель в свою очередь может быть частью окна наравне с другими панелями. Каждая кнопка или группа кнопок может быть Представлением. То же самое с коллекцией текстовых полей. Представляется полезным обращаться с панелью или окошком, которые содержат коллекции простых Представлений, таким же образом, как мы обращаемся с любыми другими Представлениями. Именно здесь использование шаблона Компоновщик сэкономит нам много сил (см. Главу 6). Почему реализация шаблона Компоновщик столь полезна в данном контексте? Если Представления могут быть вложенными, а они таковы, если созданы с применением шаблона Компоновщик, процесс обновления упрощается. Событие обновления автоматически обойдет все потомственные Представления. Создание сложных Представлений становится проще, когда нет необходимости рассылать индивидуальные сообщения об обновлении каждому вложенному Представлению.

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

Минималистичный пример шаблона MVC

Этот простой пример отслеживает нажатия клавиш. Когда нажимается очередная клавиша, это приводит к изменению Модели и информированию Представления о необходимости обновиться. Представление задействует стандартное окно вывода Output во Flash, чтобы поместить туда код символа, который ввел пользователь. Код символа - это числовое значение символа в кодовой таблице текущей раскладки. Этот пример объясняет, каким образом шаблоны Обозреватель, Стратегия и Компоновщик интегрированы в MVC.

Модель как Конкретный Субьект в шаблоне Обозреватель

Взаимоотношения между Моделью и Представлением - это связь между Субъектом и Обозревателем (См. Главу 8). Модель должна реализовать интерфейс Субъекта, который является частью шаблона Обозреватель. К счастью, ActionScript 3.0 имеет встроенные классы, уже реализующие этот принцип, используя модель событий ActionScript чтобы информировать Обозревателей об изменениях.

Класс EventDispatcher в ActionScript 3.0

Класс EventDispatcher снабжен интерфейсом IEventDispatcher. Наравне с другими методами, интерфейс IEventDispatcher определяет следующие методы, необходимые для субъекта в шаблоне Обозреватель. (См. документацию по AS3 для подробной информации о всех параметрах методов).

addEventListener(type :String , listener:Function , useCapture:Boolean = false , priority:int = 0 , useWeakReference:Boolean = false ) :void removeEventListener(type :String , listener:Function , useCapture:Boolean = false ) :void dispatchEvent(event:Event) :Boolean

Чтобы Модель могла выступить в качестве Конкретного Субъекта в шаблоне Обозреватель, необходимо реализовать интерфейс IEventDispatcher. Однако, самый простой способ для определенного пользовательского класса заполучить способность распространять события – это наследовать от класса EventDispatcher.

Обозреватель регистрирует методы слушателя, чтобы получать уведомлении от объектов EventDispatcher, методом addEventListener().

Модель

Наша Модель сохраняет код символа, соответствующий нажатой клавише, в свойстве. Необходимо реализовать сеттер и геттер, чтобы стало возможным Представлению и Контроллеру получить доступ к этому свойству и изменять его. Давайте определим для нашей Модели (Пример 12-1).

Пример 12-1. IModel.as

package { import flash.events .* ; public interface IModel extends IEventDispatcher { function setKey(key :uint) :void ; function getKey() :uint; } }

Интерфейс IModel, показанный в Примере 12-1, расширяет интерфейс IEventDispatcher и определяет пару методов для прочтения и установки кода символа последней нажатой клавиши. Поскольку интерфейс IModel расширяет IEventDispatcher, любой класс, реализующий его, должен реализовать все методы, определенные в обоих интерфейсах. Класс Model, показанный в Примере 12-2, реализует интерфейс IModel.

Пример 12-2. Model.as

package { import flash.events .* ; public class Model extends EventDispatcher implements IModel { private var lastKeyPressed:uint=0 ; public function setKey(key :uint) :void { this .lastKeyPressed =key ; dispatchEvent(new Event(Event.CHANGE ) ) ; // распространяется событие } public function getKey() :uint { return lastKeyPressed; } } }

Класс Model расширяет класс EventDispatcher, который уже реализовал интерфейс IEventDispatcher. Обратите внимание, что функция dispatchEvent() вызывается внутри метода setKey(). Она отсылает событие CHANGE всем зарегистрированным обозревателям, как только значение lastKeyPressed изменится внутри метода setKey().

Контроллер как Конкретная Стратегия в шаблоне Стратегия.

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

Контроллер

В нашем минималистичном примере поведение, требуемое от Контроллера, это всего лишь принятие события о нажатии клавиши. IKeyboardInputHandler - это интерфейс Стратегии (Пример 12-3), где определен единственный метод keyPressHandler().

Пример 12-3. IKeyboardInputHandler.as

package { import flash.events .* ; public interface IKeyboardInputHandler { function keyPressHandler(event:KeyboardEvent) :void ; } }

Конкретным Контроллером будет класс Controller (Пример 12-4), который реализует интерфейс IKeyboardInputHandler.

Пример 12-4. Controller.as

package { import flash.events .* ; public class Controller implements IKeyboardInputHandler { private var model:IModel; public function Controller(aModel:IModel) { this .model =aModel; } public function keyPressHandler(event:KeyboardEvent) :void { model.setKey (event.charCode ) ; // изменяем модель } } }

Обратите внимание, что Контроллер имеет конструктор, который в качестве параметра принимает экземпляр Модели. Это необходимо для того, чтобы Контроллер смог установить связь с Моделью, как это показано на Рисунке 12-1. Поэтому необходимо хранить ссылку на Модель.

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

Представление как Конкретный Обозреватель в шаблоне Обозреватель и Контекст в шаблоне Стратегия

Представление, возможно, наиболее сложный элемент в шаблоне MVC. Он играет роль интегрирующей части в реализации шаблонов как Обозревателя, так и Стратегии, что формирует основу его взаимоотношения с Моделью и Контроллером. Класс View, показанный в Примере 12-5, реализует Представление в минималистичном примере.

Пример 12-5. View.as

package { import flash.events .* ; import flash.display .* ; public class View { private var model:IModel; private var controller:IKeyboardInputHandler; public function View(aModel:IModel,oController:IKeyboardInputHandler,target :Stage ) { this .model =aModel; this .controller =oController; // подписывается на получение уведомлений от Модели model.addEventListener (Event.CHANGE ,this .update ) ; // подписывается на получение нажатий клавиш от сцены target .addEventListener (KeyboardEvent.KEY_DOWN ,this .onKeyPress ) ; } private function update(event:Event) :void { // получение данных от Модели и обновление Представления trace (model.getKey () ) ; } private function onKeyPress(event:KeyboardEvent) :void { // обработка передается в Контроллер (Стратегия) на обработку controller.keyPressHandler (event) ; } } }

Представление нуждается в ссылках на Модель и на Контроллер для взаимодействия с ними, как показано на Рисунке 12-1. И экземпляр Модели, и экземпляр Контроллера передаются Представлению в его конструкторе. К тому же, Представление в нашем примере нуждается в ссылке на сцену (Stage), чтобы зарегистрировать себя как получателя событий нажатия клавиш.

Кроме того что класс View рисует пользовательский интерфейс, он выполняет еще пару важных задач. Он регистрируется у Модели для получения событий об обновлении, и делегирует Контроллеру обработку ввода пользователя. В нашем примере Представление не имеет внешнего видимого присутствия на сцене, но отображает состояние Модели в окне вывода Output. Ему нуждается в получении события нажатия клавиши, и регистрирует метод onKeyPress() для получения события KEY_DOWN от сцены. Вторая задача – это зарегистрировать метод слушателя update() для получения события CHANGE от модели. При получении уведомления об изменении, метод update() прочитывает код последней нажатой клавиши из Модели и печатает его в окне вывода, используя функцию trace().

Построение триады MVC

Мы рассмотрели реализацию каждой из трех составляющих шаблон MVC частей по отдельности. Однако, должен существовать клиент, который инициализирует каждый элемент и построит модель MVC. На самом деле, никакого сложного построения не будет - все что нужно уже сделано при написании классов Модели, Представления и Контроллера. В Примере 12-6 приводится класс Flash-документа, который иллюстрирует элементы MVC.

Пример 12-6. Main.as (основной класс минималистичного примера)

package { import flash.display .* ; import flash.events .* ; /** * Main Class * @ purpose: Document class for movie */ public class Main extends Sprite { public function Main() { var model:IModel=new Model ; var controller:IKeyboardInputHandler=new Controller(model) ; var view:View=new View(model,controller,this .stage ) ; } } }

После того как Модель, Контроллер и Представление инициализированы, они установят связь друг с другом и начнут работать. Нажатие клавиши на клавиатуре приведет к выводу кода соответствующего символа в окне Output.

Вам нужно запретить шорткаты для тестирования нажатий клавиш. В противном случае пользовательский интерфейс Flash перехватит события нажатия клавиш, которые соответствуют шорткатам. Чтобы запретить шорткаты, выберите Disable Keyboard Shortcuts из меню Control во время выполнения ролика.

Обратите внимание, что экземпляр Модели передается Контроллеру. Подобным образом экземпляры Модели и Контроллера передаются Представлению. Мы можем просто заместить существующие Модель и Контроллер другими, при условии, что они реализуют интерфейсы IModel и IKeyboardInputHandler. Дополнительные Представления также могут быть безболезненно добавлены прямо в отношения Субъект-Обозреватель между Моделью и Представлением. Модель ничего не знает о Представлениях, так как это забота Представления - зарегистрировать себя в качестве слушателя уведомлений об изменении Модели. Это большой плюс шаблона MVC; Модель, Представление и Контроллер разделены, слабо связаны, что придает гибкости в их использовании.

Вложенные Представления и узлы шаблона Компоновщик

Как вы помните, Представление, возможно, самый сложный элемент в триаде MVC, поскольку в контексте MVC он задействован как в реализации шаблона Обозреватель, так и в Стратегии. Наши элементы Представления способны быть более сложными, поскольку могут реализовать третий шаблон - Компоновщик (см. примеры шаблона Компоновщик в Главе 6). Реализация Представлений как элементов шаблона Компоновщик позволяет разобраться со сложными пользовательскими интерфейсами, которые содержат множественные Представления. Вложение Представлений превносит некоторые преимущества в процесс обновления пользовательского интерфейса, так как обновления могут распространяться по ветвям структурного дерева составного Представления. Также составные Представления могут добавлять и удалять вложенные Представления, основываясь на режиме работы приложения и пользовательских настройках. Хорошим примером сложного интерфейса является панель Properties Inspector в среде разработки Flash. Содержимое Properties Inspector зависит от контекста, и элементы интерфейса появляются и исчезают в зависимости от того, какой объект выделен на сцене.

Компонент и составное Представление

Первым шагом будет создание компонента и составных классов для Представления. Эти классы должны быть описаны как абстрактные, должны быть подклассами и не должны порождать экземпляры, как показано в Примере 12-7.

Пример 12-7. ComponentView.as

package { import flash.errors .IllegalOperationError ; import flash.events .Event ; import flash.display .Sprite ; public class ComponentView extends Sprite { { protected var model:Object ; protected var controller:Object ; public function ComponentView(aModel:Object ,aController:Object =null ) { this .model =aModel; this .controller =aController; } public function add (c:ComponentView) :void { throw new IllegalOperationError("add operation not supported" ) ; } public function remove(c:ComponentView) :void { throw new IllegalOperationError("remove operation not supported" ) ; } public function getChild(n:int ) :ComponentView { throw new IllegalOperationError("getChild operation not supported" ) ; return null ; } // АБСТРАКТНЫЙ метод(должен быть замещен в классе-потомке) public function update(event:Event=null ) :void { } } } }

Класс ComponentView из Примера 12-7 определяет абстрактный интерфейс для Представления компонента. Это похоже на класс классического компонента из Главы 6, но с несколькими ключевыми отличиями. Класс ComponentView хранит ссылку на Модель и Представление, и содержит конструктор. Не все Представления обрабатывают ввод пользователя, и компонентное Представление может быть сконструировано с простой передачей экземпляра Модели. Поэтому параметр aController принимает в конструкторе значение null по умолчанию. Также обратите внимание, что класс ComponentView унаследован от класса Sprite. Это логично, поскольку большинство Представлений рисуют пользовательский интерфейс на сцене. Мы можем использовать свойства и методы, реализованные в классе Sprite, для рисования и добавления объектов в список отображения.

Метод update() должен вести себя как абстрактный метод. Дочерние Представления, являющиеся потомками ComponentView, должны заместить и реализовать метод update(), чтобы уметь обновлять свою часть пользовательского интерфейса. По этой причине методу передается параметр типа Event. Этот параметр также по умолчанию установлен в null, что позволяет вызывать update() без передачи события как параметра. Такой подход полезен, когда изначально отрисованный пользовательский интерфейс находится в своем состоянии по умолчанию, и наш следующий пример иллюстрирует это.

Класс CompositeView расширяет ComponentView и замещает методы, которые отвечают за дочерние Представления.

Пример 12-8. CompositeView.as

package { import flash.events .Event ; // АБСТРАКТНЫЙ класс (от него нужно наследовать, не создавая экземпляра данного класса) public class CompositeView extends ComponentView { private var aChildren:Array ; public function CompositeView(aModel:Object ,aController:Object =null ) { super (aModel,aController) ; this .aChildren =new Array ; } override public function add (c:ComponentView) :void { aChildren.push (c) ; } override public function update(event:Event=null ) :void { for each (var c:ComponentView in aChildren) { c.update (event) ; } } } }

Обратите внимание на замещаемую (override) функцию update() класса CompositeView в Примере 12-8. Она вызывает метод update у всех дочерних классов. Поэтому, вызов функции update() в корне структуры составного Представления приведет к распространению обновления по структуре и обойдет дерево компонента, обновляя все Представления. Давайте расширим классы CompositeView и ComponentView, и создадим структуру Представлений, чтобы посмотреть как это работает

В этом руководстве Вы узнаете, как построить простую систему по архитектуре MVC (Model-View-Controller, Модель-Отображение-Контроллер) на PHP 5.1 с использованием возможностей библиотеки SPL (Standard PHP Library, Стандартная Библиотека PHP).

Введение

Добро пожаловать в первое полноценное руководство для PHP 5. Вам понадобится PHP 5.1 с установленной библиотекой SPL, так как мы воспользуемся некоторыми из самых последних возможностей PHP 5.

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

Одна точка входа

Одной из важных вещей в MVC является одна точка входа в приложение вместо кучи PHP-файлов, делающих примерно следующее:

У нас будет один файл, обрабатывающий все запросы. Это значит, что нам не придётся мучиться с подключением global.php каждый раз, когда нам нужно создать новую страницу. Эта «одна точка входа» будет называться index.php и на данный момент будет такой:

Как Вы можете заметить, этот скрипт пока ещё ничего не делает, но погодите минутку.

Чтобы направить все запросы на главную страницу, мы воспользуемся mod_rewrite и установим в.htaccess директиву RewriteRule. Вставим следующий код в файл.htaccess и сохраним его в той же директории, что и index.php:

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

Сперва мы проверяем, существует ли запрашиваемый файл, используя директиву RewriteCond, и, если нет, то перенаправляем запрос на index.php. Такая проверка на существование файла необходима, так как иначе index.php будет пытаться обрабатывать все запросы к сайту, включая запросы на изображения. А это нам как раз и не надо.

Если у Вас нет возможности использовать.htaccess или mod_rewrite, то Вам придётся вручную адресовать все запросы к index.php. Другими словами, все ссылки должны будут иметь вид «index.php?route=[здесь-идёт-запрос]». Например, «index.php?route=chat/index».

Теперь, когда все запросы идут через одну точку входа, мы можем начать написание скрипта index.php. Первая вещь, которую мы должны сделать, это инициализация системы. Создадим директорию includes, а в ней файл startup.php (он будет у нас файлом инициализации). Вставим следующий код в index.php:

В этом примере мы объявляем некоторую константу, узнаём, где лежат файлы системы, а также проверяем, что версия PHP, ну, хотя бы, 5.1.

Следующая вещь, которую необходимо сделать, это объект Registry (журнал, реестр) для хранения глобальных значений. Он будет передаваться в отдельные объекты системы и использоваться для доступа к глобальным значениям, причём без необходимости обозначать переменные как «global» или обращаться к массиву $GLOBALS. Почитайте статью «Использование глобальных значений в PHP» для более подробной информации об объекте реестра.

Добавьте следующий код в файл startup.php после того кода, что приведён в предыдущем примере:

$registry = new Registry;

Если сейчас попробовать запустить систему, то можно увидеть следующую ошибку:

Fatal error: Class "Registry" not found in g:\Projects\PHP\content\simple mvc php5\demo\includes\startup.php on line 12

Это, конечно, не большой сюрприз для нас, ведь мы ещё не написали сам класс Registry. Файл с классом можно было бы просто подключить, используя функцию include() (Прим. пер.: кстати говоря, include() не такая уж и функция, а всё-таки выражение языка, управляющая структура, если смотреть по ману), но давайте воспользуемся одной из новых возможностей PHP 5: __autoload().

Волшебная функция __autoload() используется для динамической загрузки классов. Когда PHP обнаруживает несуществующий класс, он сначала вызывает функцию __autoload() и только затем выдаёт ошибку. Мы можем воспользоваться такой возможностью для загрузки классов «на лету».

Вставьте этот код перед кодом из предыдущего примера:

// Загрузка классов «на лету» function __autoload($class_name) { $filename = strtolower($class_name) . ".php"; $file = site_path . "classes" . DIRSEP . $filename; if (file_exists($file) == false) { return false; } include ($file); }

Наша функция __autoload() берёт имя класса, переданное ей как аргумент, и проверяет, существует ли файл с похожим именем в директории с классами. Если файла нет, то функция просто вернёт false и выскочит фатальная ошибка. Но если файл существует, он будет загружен. Т.е. объявится необходимый класс, и никакой ошибки не будет.

Мы ещё не создали сам класс Registry, поэтому ошибка всё ещё будет появляться. Давайте же займёмся этим.

Создание класса Registry

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

Для начала создадим директорию classes и в ней файл registry.php. Вставим следующий код в registry.php:

Теперь у нас есть «скелет» класса Registry и нужно нагрузить его методами. Напишем 2 метода: set(), чтобы устанавливать значения и get(), чтобы значения получать. Также можно написать метод remove() для удаления значений. Добавим эти методы в класс Registry:

Function set($key, $var) { if (isset($this->vars[$key]) == true) { throw new Exception("Unable to set var `" . $key . "`. Already set."); } $this->vars[$key] = $var; return true; } function get($key) { if (isset($this->vars[$key]) == false) { return null; } return $this->vars[$key]; } function remove($var) { unset($this->vars[$key]); } ?>

Эти методы простые, они устанавливают, получают и удаляют элементы из массива $vars, который является атрибутом класса. В методе set() мы заодно проверяем, не существует ли уже значение с указанным ключом, и, если существует, то мы генерируем исключение. Это нужно, чтобы избежать случайной перезаписи значений.

Теперь у нас есть полноценный класс Registry, но мы не будем останавливаться на этом. Воспользуемся одной из возможностей библиотеки SPL: ArrayAccess. SPL (сокращённо от Standard PHP Library, Стандартная Библиотека PHP) - это коллекция интерфейсов и классов, предназначенных для решения стандартных проблем. Один из интерфейсов SPL, ArrayAccess, может быть использован, чтобы предоставить доступ к объекту, как к обычному массиву. Посмотрим на такой пример:

set ("name", "Dennis Pallett"); // Получаем значение, используя get() echo $registry->get ("name"); // Получаем значение, используя доступ как к массиву echo $registry["name"] ?>

Фокус в том, что $registry становится как бы массивом, хотя на самом деле это объект. Конечно, ArrayAccess не даёт никаких особых преимуществ, но он позволяет сократить объём кода, так как не придётся каждый раз писать «->get()». Чтобы воспользоваться этим интерфейсом, нужно исправить первую строчку класса («Class Registry») таким образом:

Class Registry Implements ArrayAccess {

Ключевое слово «Implements» говорит интерпретатору, что этим классом мы реализуем интерфейс, чем на самом деле ArrayAccess и является.

Класс, реализующий интерфейс ArrayAccess, должен иметь следующие методы:

Function offsetExists($offset) { return isset($this->vars[$offset]); } function offsetGet($offset) { return $this->get($offset); } function offsetSet($offset, $value) { $this->set($offset, $value); } function offsetUnset($offset) { unset($this->vars[$offset]); }

Эти методы должны быть понятны сами по себе. Дополнительную информацию можно найти в документации SPL.

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

get ("name"); // Получаем значение, используя доступ как к массиву echo $registry["name"] ?>

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

Модель

«M» или модель – часть MVC-системы, которая отвечает за запросы к базе данных (или другому внешнему источнику) и предоставление информации контроллеру. Можно было бы загружать необходимую модель в зависимости от запроса, но я предпочитаю немного стереть границы между моделью и контроллером именно в этом месте, т.е. контроллер работает с БД непосредственно через библиотеку взаимодействия с БД, нежели чем через отдельную модель. Может быть, Вам захочется сделать это по-другому, тут дело вкуса.

Нам нужно написать код, необходимый для установки соединения с БД и поместить его в index.php. Существует множество замечательных библиотек для работы с БД (включая мою собственную, AutoCRUD), но в PHP 5 уже есть такая библиотека – PDO. Поэтому нет нужды использовать какую-либо другую.

Вставим следующий код в файл index.php (после подключения файла инициализации):

# Соединяемся с БД $db = new PDO("mysql:host=localhost;dbname=demo", "", ""); $registry->set ("db", $db);

В этом примере мы сначала создаём новый экземпляр библиотеки PDO и соединяемся с нашей БД MySQL. Потом делаем переменную $db доступной глобально при помощи нашего класса Registry.

Модельная компонента нашей системы готова, поэтому давайте перейдём к написанию контроллера.

Написание контроллера подразумевает также и написание класса Router, ответственного за загрузку нужного контроллера в зависимости от запроса (вспомните, в index.php через URL передаётся переменная $route).

Класс Router

Класс Router будет разбирать запрос, а потом загружать требуемый контроллер. Создадим «скелет» класса:

registry = $registry; } } ?>

Затем добавим следующие строки в index.php:

# Загружаем router $router = new Router($registry); $registry->set ("router", $router);

Первая вещь, которую мы напишем, это метод setPath() для установки директории, где будут лежать все наши контроллеры. Метод выглядит следующим образом и должен быть добавлен в класс Router:

Function setPath($path) { $path = trim($path, "/\\"); $path .= DIRSEP; if (is_dir($path) == false) { throw new Exception ("Invalid controller path: `" . $path . "`"); } $this->path = $path; }

Потом добавим следующие строки в index.php:

$router->setPath (site_path . "controllers");

Теперь, когда мы установили путь до наших контроллеров, напишем сам метод, ответственный за загрузку контроллера. Этот метод будет называться delegate(), он и будет анализировать запрос. Первый кусочек этого метода такой:

Function delegate() { // Анализируем путь $this->getController($file, $controller, $action, $args);

Как Вы можете видеть, он использует ещё один метод, getController(), чтобы получить название контроллера и несколько других переменных. Этот метод выглядит так:

Private function getController(&$file, &$controller, &$action, &$args) { $route = (empty($_GET["route"])) ? "" : $_GET["route"]; if (empty($route)) { $route = "index"; } // Получаем раздельные части $route = trim($route, "/\\"); $parts = explode("/", $route); // Находим правильный контроллер $cmd_path = $this->path; foreach ($parts as $part) { $fullpath = $cmd_path . $part; // Есть ли папка с таким путём? if (is_dir($fullpath)) { $cmd_path .= $part . DIRSEP; array_shift($parts); continue; } // Находим файл if (is_file($fullpath . ".php")) { $controller = $part; array_shift($parts); break; } } if (empty($controller)) { $controller = "index"; }; // Получаем действие $action = array_shift($parts); if (empty($action)) { $action = "index"; } $file = $cmd_path . $controller . ".php"; $args = $parts; }

Пробежимся по этому методу. Сначала он берёт значение переменной $route из запроса, потом разбивает его на части с помощь функции explode(). Например, запрос «members/view» преобразуется в такой массив: array(‘members’, ‘view’).

Потом при помощи цикла foreach он проходит по каждой части и проверяет, является ли эта часть директорией. Если является, то он приписывает её к пути до файла и проверяет следующую часть. Это позволяет поместить контроллеры в поддиректориях и, таким образом, получить иерархию контроллеров. Если же текущая часть запроса не является директорией, но является файлом, она сохраняется в переменную $controller, и мы выходим из цикла, так как нашёлся контроллер, который нам нужен.

После цикла мы проверяем переменную с именем контроллера. Если она пустая, то используем контроллер «index», который будет у нас контроллером по умолчанию. Потом метод определяет действие, которое необходимо выполнить. Контроллер – это класс, который состоит из нескольких методов. Действие же указывает на конкретный метод. Если действие не указано, будем использовать «index» - действие по умолчанию.

И, наконец, получаем полный путь до файла контроллера, объединяя три переменные: путь, имя контроллера и расширение «php».

Теперь, когда мы проанализировали запрос, пора вызывать метод delegate() для загрузки контроллера и выполнения действия. Полностью метод delegate() выглядит так:

Function delegate() { // Анализируем путь $this->getController($file, $controller, $action, $args); // Файл доступен? if (is_readable($file) == false) { die ("404 Not Found"); } // Подключаем файл include ($file); // Создаём экземпляр контроллера $class = "Controller_" . $controller; $controller = new $class($this->registry); // Действие доступно? if (is_callable(array($controller, $action)) == false) { die ("404 Not Found"); } // Выполняем действие $controller->$action(); }

Проанализировав запрос при помощи метода getController(), мы проверяем, существует ли в действительности файл, и, если нет, то возвращаем простое сообщение об ошибке.

После этого мы подключаем файл с контроллером и создаём экземпляр его класса, называться который должен «Controller_[имя]». Чуть позже мы поговорим о контроллерах более подробно.

Потом мы проверяем, есть ли указанное действие (т.е. метод) и возможно ли к нему обратиться (используем для этого функцию is_callable()). Наконец, мы выполняем непосредственно само действие, на чём роль класса Router и завершается.

Написав полностью метод delegate(), добавим следующую строчку в файл index.php:

$router->delegate();

Если попробовать сейчас запустить систему, то мы увидим следующую ошибку (разумеется, если директории controllers ещё нет):

Fatal error: Uncaught exception "Exception" with message "Invalid controller path: `g:\Projects\PHP\content\simple mvc php5\demo\controllers\`" in g:\Projects\PHPit\content\simple mvc php5\demo\classes\router.php:18 Stack trace: #0 g:\Projects\PHP\content\simple mvc php5\demo\index.php(13): Router->setPath("g:\Projects\PHP...") #1 {main} thrown in g:\Projects\PHP\content\simple mvc php5\demo\classes\router.php on line 18

Или же мы увидим ошибку «404 Not Found», так как ещё нет ни одного контроллера. Но этим-то мы сейчас и займёмся.

Контроллер

Контроллеры в нашей MVC-системе будут достаточно простыми и потребуют совсем немного времени. Во-первых, удостоверимся, что директория controllers существует. Создадим файл controller_base.php в директории classes и вставим в него следующий код:

registry = $registry; } abstract function index(); } ?>

Этот абстрактный класс будет родительским классом для всех наших контроллеров. Он будет делать всего лишь две вещи: сохранять локальную копию класса Registry и при помощи абстрактного метода index() заставлять все дочерние контроллеры реализовывать этот метод.

Напишем наш первый контроллер. Создадим файл index.php в директории controllers и вставим в него такой код:

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

Это означает, что класс Router выполнил свою работу и запустил требуемое действие из требуемого контроллера. Давайте напишем ещё один контроллер, который будет соответствовать запросу «members/view». Создадим файл members.php в директории контроллеров и вставим в него такой код:

Теперь зайдём в нашу MVC-систему по запросу «members/view» или же «index.php?route=members/view». Мы должны увидеть такой результат:

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

Теперь, когда у нас есть контроллеры, осталась лишь одна вещь: «V» или «View» («Отображение»).

Отображение

Как и в случае с моделями, есть несколько различных вариантов создания компоненты View в MVC-системе. Мы могли бы научить класс Router автоматически загружать ещё один файл, названный как-нибудь так: «view_{имя}.php». Но чтобы сделать руководство более понятным, напишем класс Template, который будет заниматься выводом шаблонов.

Сначала создадим файл template.php в директории classes и вставим в него следующий код:

registry = $registry; } } ?>

Теперь у нас есть основная структура нашего класс Template. Следующим шагом добавим такой код в файл index.php прямо перед строками, связанными с классом Router:

# Создаём объект шаблонов $template = new Template($registry); $registry->set ("template", $template);

Так как нам понадобится использовать значения из моделей и контроллеров, то напишем метод set() для установки переменных, доступных в шаблонах. Посмотрим на пример:

Function set($varname, $value, $overwrite=false) { if (isset($this->vars[$varname]) == true AND $overwrite == false) { trigger_error ("Unable to set var `" . $varname . "`. Already set, and overwrite not allowed.", E_USER_NOTICE); return false; } $this->vars[$varname] = $value; return true; } function remove($varname) { unset($this->vars[$varname]); return true; }

Методы set() и remove() достаточно простые и используются, соответственно, для установки и удаления переменных.

Займёмся написанием метода show(), который будет отображать шаблоны. Простейший путь – это создать отдельную директорию templates, где хранить все файлы шаблонов, и использовать include() для вывода шаблона. Разумеется, Ваш собственный метод show() может быть совершенно другим и загружать шаблоны из базы данных или делать что-нибудь ещё. Посмотрим на кусо

Голосов: 745 | Просмотров: 8080

Паттерн 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 .



Просмотров