Разделение кода. Модель MVC

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

Ссылки
Первое, с чего я бы хотел начать - это ссылки на внешние материалы, которыми я руководствовался в процессе написания этой статьи:
Введение
Во времена, когда солнце светило ярче, а трава была зеленее, на тот момент команда студентов, как автор этой статьи, разрабатывали программное обеспечение, писав сотни строк кода непосредственно в интерфейсе продукта. Иногда использовались сервисы и менеджеры для работы с данными и тогда решение получалось с использованием паттерна Document-View. Поддержка такого кода требовала колоссальных затрат, т. к. нового разработчика надо обучить (рассказать), какой код за что в продукте отвечает, и ни о каком модульном тестировании и речи не было. Команда разработки - это 4 человека, которые сидят в одной комнате.
Прошло время, менялась работа. Разрабатываемые приложения становились больше и сложнее, из одной сплоченной команды разработчиков стало много разных команд разработчиков, архитекторов, юзабилистов, дизайнеров и PMов. Теперь каждый ответственен за свою область: GUI, бизнес-логика, компоненты. Появился отдел анализа, тестирования, архитектуры. Стоимость разработки ПО возросла в сотни и даже тысячи раз. Такой подход к разработке требует наличие стойкой архитектуры, которая бы синхронизировала разные функциональные области продукта между собой.
Паттерны
Учитывая цель уменьшения трудозатрат на разработку сложного программного обеспечения, предположим, что необходимо использовать готовые унифицированные решения. Ведь шаблонность действий облегчает коммуникацию между разработчиками, позволяет ссылаться на известные конструкции, снижает количество ошибок.
По словам Википедии , паттерн (англ. design pattern) - повторимая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто возникающего контекста.

Начнем с первого главного – Model-View-Controller. MVC - это фундаментальный паттерн, который нашел применение во многих технологиях, дал развитие новым технологиям и каждый день облегчает жизнь разработчикам.

Впервые паттерн MVC появился в языке SmallTalk. Разработчики должны были придумать архитектурное решение, которое позволяло бы отделить графический интерфейс от бизнес логики, а бизнес логику от данных. Таким образом, в классическом варианте, MVC состоит из трех частей, которые и дали ему название. Рассмотрим их:

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

Модель обладает следующими признаками:

  • Модель - это бизнес-логика приложения;
  • Модель обладает знаниями о себе самой и не знает о контроллерах и представлениях;
  • Для некоторых проектов модель - это просто слой данных (DAO, база данных, XML-файл);
  • Для других проектов модель - это менеджер базы данных, набор объектов или просто логика приложения;
Представление (View)
В обязанности Представления входит отображение данных полученных от Модели. Однако, представление не может напрямую влиять на модель. Можно говорить, что представление обладает доступом «только на чтение» к данным.

Представление обладает следующими признаками:

  • В представлении реализуется отображение данных, которые получаются от модели любым способом;
  • В некоторых случаях, представление может иметь код, который реализует некоторую бизнес-логику.
Примеры представления: HTML-страница, WPF форма, Windows Form.
Различия MVP & MVVM & MVP
Наиболее распространенные виды MVC-паттерна, это:
  • Model-View-Controller
  • Model-View-Presenter
  • Model-View-View Model

Рассмотрим и сравним каждый из них.

Model-View-Presenter

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

Признаки презентера:

  • Представление взаимодействует напрямую с презентером, путем вызова соответствующих функций или событий экземпляра презентера;
  • Презентер взаимодействует с View путем использования специального интерфейса, реализованного представлением;
  • Один экземпляр презентера связан с одним отображением.

Реализация:
Каждое представление должно реализовывать соответствующий интерфейс. Интерфейс представления определяет набор функций и событий, необходимых для взаимодействия с пользователем (например, IView .ShowErrorMessage(string msg)). Презентер должен иметь ссылку на реализацию соответствующего интерфейса, которую обычно передают в конструкторе.
Логика представления должна иметь ссылку на экземпляр презентера. Все события представления передаются для обработки в презентер и практически никогда не обрабатываются логикой представления (в т.ч. создания других представлений).

Пример использования: Windows Forms.

Model-View-View Model


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

Признаки View-модели:

  • Двухсторонняя коммуникация с представлением;
  • View-модель - это абстракция представления. Обычно означает, что свойства представления совпадают со свойствами View-модели / модели
  • View-модель не имеет ссылки на интерфейс представления (IView). Изменение состояния View-модели автоматически изменяет представление и наоборот, поскольку используется механизм связывания данных (Bindings)
  • Один экземпляр View-модели связан с одним отображением.

Реализация:
При использовании этого паттерна, представление не реализует соответствующий интерфейс (IView).
Представление должно иметь ссылку на источник данных (DataContex), которым в данном случае является View-модель. Элементы представления связаны (Bind) с соответствующими свойствами и событиями View-модели.
В свою очередь, View-модель реализует специальный интерфейс, который используется для автоматического обновления элементов представления. Примером такого интерфейса в WPF может быть INotifyPropertyChanged.

Пример использования: WPF

Model-View-Controller

Основная идея этого паттерна в том, что и контроллер и представление зависят от модели, но модель никак не зависит от этих двух компонент.

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

  • Контроллер определяет, какие представление должно быть отображено в данный момент;
  • События представления могут повлиять только на контроллер.контроллер может повлиять на модель и определить другое представление.
  • Возможно несколько представлений только для одного контроллера;

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

Пример использования: MVC ASP.NET

Резюме
Реализация MVVM и MVP-паттернов, на первый взгляд, выглядит достаточно простой схожей. Однако, для MVVM связывание представления с View-моделью осуществляется автоматически, а для MVP - необходимо программировать
MVC, по-видимому, имеет больше возможностей по управлению представлением.
Общие правила выбора паттерна
MVVM
  • Используется в ситуации, когда возможно связывание данных без необходимости ввода специальных интерфейсов представления (т.е. отсутствует необходимость реализовывать IView);
  • Частым примером является технология WPF.
MVP
  • Используется в ситуации, когда невозможно связывание данных (нельзя использовать Binding);
  • Частым примером может быть использование Windows Forms.
MVC
  • Используется в ситуации, когда связь между представление и другими частями приложения невозможна (и Вы не можете использовать MVVM или MVP);
  • Частым примером использования может служить ASP.NET MVC.
Заключение
В заключении, автор этой статьи хотел бы отметить, что строго придерживаться только одному паттерну - не всегда лучший выбор. Например, представьте, что Вы хотели бы использовать MVVM для разработки приложений с использованием Windows Forms через свойство контролов Bindings. Ваша цель - это отделить представление от бизнес логики и логики, которая их связывает. Приложение должно быть легко тестируемым и поддерживаемым, а для аналитиков - понятным (ведь на вопрос «в чем измеряется работа жесткого диска» существует единственный правильный ответ - в Джоулях (абстрактный пример Модели -> Представления)).

Большое спасибо за уделенное время, приятного чтения!

По всему интернет-миру разбросаны миллионы веб-приложений. Есть совсем простые, есть такие, что сам «архитектор матрицы ногу сломит». Но их объединяет одно — MVC .

Самый популярный архитектурный паттерн в мире среди веб-приложений — модель-представление-контроллер (Model View Controller или просто MVC). Впервые, он был использован ещё в конце 70-х двадцатого века, в приложениях на языке Smalltalk . А затем, его приютили программисты Java и расшарили для всего мира и всех языков программирования. PHP не стал исключением. Сегодня, только малая часть программистов, коллекционирующих раритетный PHP-код может себе позволить не смотреть в сторону MVC.

Таким популярным он стал неспроста. Он просто рождён для создания гибких и масштабируемых приложений, которые легко сопровождать и достраивать.
Цель нашего тьюториала — показать на простом примере, как работает паттерн MVC.

Чтобы выполнить задания, вам потребуются следующие программы:

Примечания:

  • Мы предполагаем, что у вас есть базовые знания PHP.

Паттерн MVC

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

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

Схематично потоки данных в этой модели можно представить так:

Вход в реальность

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

Мы не будем сейчас рассматривать архитектуру всей социальной сети. Мы возьмём только маленькую подзадачку, представим всю её серьёзность и применим к ней паттерн MVC.

Как только мы начинаем его использовать, то сразу задумываемся — а как бы нам расположить скрипты нашего решения так, что бы всё было под рукой? Для этого, разместим каждый из трёх разделов нашей MVC-системы по отдельным папкам и, таким образом, получим простую структуру каталогов, в которой легко найти то, что нам нужно. Кроме того, эти три папки поместим в каталог lib, и вынесем его выше корневого веб-каталога www:

/lib --/controller ---- FrendCnt.php --/model ---- Frend.php ---- FrendList.php --/view ---- frendlist.php ---- frendone.php /www -- index.php -- .htaccess

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

Контроллер

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

В нашем простом примере, контроллер будет сконцентрирован в одном классе FrendCnt . Подробнее его опишем позже.А сейчас немного о точке входа в веб-приложение — это, конечно, будет файл index.php . В нём, мы определим точку отсчёта для подключения наших скриптов. Создадим экземпляр контроллера, и вызовем у него метод, который начнёт обрабатывать HTTP-запрос и определит что делать дальше.

Листинг №1 (файл index.php):

$baseDir = dirname(__FILE__) . "/.."; include_once($baseDir . "/lib/controller/FriendCnt.php"); $controller = new FriendCnt(); $controller->invoke();

Теперь о контроллере. У нас — это класс FriendCnt . Вы уже заметили, что экземпляр этого класса создаётся в index.php . Он имеет только один метод invoke() , который вызывается сразу после создания экземпляра. В конструкторе контроллера, создаётся объект на основе класса модели — FrendList (список друзей) для оперирования с данными.

В функции invoke() , на основе пришедшего HTTP-запроса, принимается решение: какие данные потребуются от модели. Затем происходит вызов метода извлекающего данные. Далее происходит подключение шаблонов для отображения, которым передаются данные из контроллера. Обратите внимание, что контроллер ничего не знает о базе данных или о том, как страница генерится.

Листинг №2 (файл контроллера FriendCnt.php):

Require_once($baseDir . "/lib/model/FriendList.php"); class FriendCnt { public $oFriendList; public function __construct() { $this->oFriendList = new FriendList(); } public function invoke() { global $baseDir; $oFriendList = $this->oFriendList; if(isset($_GET["key"])) { $oFriendList->setKey($_GET["key"]); $oFriend = $oFriendList->fetch(); include $baseDir . "/lib/view/friendone.php"; }else { $aFriend = $oFriendList->fetch(); include $baseDir . "/lib/view/friendlist.php"; } } }

Модель и сущности

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

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

У нас к модели относятся два скрипта, в каждом из которых определён свой класс. Центральный класс FriendList и класс-сущность Friend . В центральном классе, происходит манипуляция с данными: получение данных от контроллера и их обработка. Класс-сущность служит контейнером для переноса данных между моделью и представлением, а также определяет их формат. При хорошей реализации паттерна MVC, классы сущности не должны упоминаться в контроллере, и они не должны содержать какую-либо бизнес-логику. Их цель - только хранение данных.
В классе FriendList , работающем со списком друзей, мы создали функцию, которая моделирует взаимодействие этого класса с базой данных. Метод getFriendList() возвращает массив из объектов, созданных на основе класса Friend . Для обеспечения удобства работы с данными, также была создана функция, индексирующая массив объектов. Контроллеру оказались доступны только два метода: setKey() — устанавливает поле ключа, по которому возвращаются детальные данные о друге; fetch() — возвращает или конкретный объект или весь список друзей.

Листинг №3 (файл модели FriendList.php):

Require_once($baseDir . "/lib/model/Friend.php"); class FriendList { private $oneKey; private function getFriendList() { return array(new Friend("Александр", "1985", "[email protected]"), new Friend("Юрий", "1987", "[email protected]"), new Friend("Алексей", "1989", "[email protected]"),); } private function getIndexedList() { $list = array(); foreach($this->getFriendList() as $val) { $list[$val->getKey()] = $val; } return $list; } public function setKey($key) { $this->oneKey = $key; } public function fetch() { $aFriend = $this->getIndexedList(); return ($this->oneKey) ? $aFriend[$this->oneKey] : $aFriend; } }

В зависимости от реализации объектов Сущности, данные о ней, могут быть оформлены в виде XML-документа или JSON-объекта.

Листинг №4 (файл сущности Friend.php):

Class Friend { private $key; private $name; private $yearOfBirth; private $email; public function __construct($name, $yearOfBirth, $email) { $this->key = md5($name . $yearOfBirth . $email); $this->name = $name; $this->yearOfBirth = $yearOfBirth; $this->email = $email; } public function getKey() { return $this->key; } public function getName() { return $this->name; } public function getYearOfBirth() { return $this->yearOfBirth; } public function getEmail() { return $this->email; } }

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

Теперь нам нужно представить данные в наилучшем свете для пользователя.

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

Главное здесь показать, что представление отделено от контроллера и модели. При этом контроллер занимается передачей данных от модели к представлению.

В нашем примере представление содержит только два файла: для отображения детальной информации о друге и для отображения списка друзей.

Листинг №5 (файл для вывода списка друзей friendlist.php):

Мои друзья

Имя Год рождения
getKey() ?>"> getName() ?> getYearOfBirth() ?>

Листинг №6 (файл для вывода списка друзей friendone.php):

<?php echo $oFriend->getName() ?> : Мой друг getName() . "
"; echo "Год рождения: " . $oFriend->getYearOfBirth() . "
"; echo "Email: " . $oFriend->getEmail() . "
"; ?> Список

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

Мы могли бы реализовать детальный просмотр с помощью AJAX, тогда бы у нас была всего одна страница, и мы формировали бы часть представления через JSON-объекты непосредственно на компьютерах клиентов. Существует куча вариантов на этот счёт.

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

Здесь лежат файлы проекта, качайте сравнивайте:

Ну как? Какие мысли? Комментируем, не стесняемся.

Создание HTML-элементов

Итак, в предыдущей статье мы рассмотрели создание и настройку HTML-дескрипторов веб-форм. HTML-форма не имеет особого смысла до тех пор, пока не будут созданы некоторые элементы управления вводом (например, ). В таблице ниже описаны базовые вспомогательные методы, которые доступны для создания элементов , и приведены примеры генерируемой ими HTML-разметки. Во всех этих вспомогательных методах первый аргумент используется для установки значений атрибутов id и name элемента , а второй аргумент служит для установки значения атрибута value.

Базовые вспомогательные методы HTML для создания элементов управления вводом
Метод Пример Генерируемый HTML-элемент
Html.CheckBox() Html.CheckBox("myCheckbox", false)
Html.Hidden() Html.Hidden("myHidden", "val")
Html.RadioButton() Html.RadioButton("myRadiobutton", "val", true)
Html.Password() Html.Password("myPassword", "val")
Html.TextArea() Html.TextArea("myTextarea", "val", 5, 20, null)
Html.TextBox() Html.TextBox("myTextbox", "val")

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

в предыдущей статье.

Обратите внимание, что вспомогательный метод для флажка (Html.CheckBox()) генерирует два элемента : собственно флажок и скрытый элемент с тем же самым именем. Причина в том, что браузеры не отправляют значения для флажков, если те не отмечены. Наличие скрытого элемента управления гарантирует, что ASP.NET MVC Framework получит значение из скрытого поля, когда такое произойдет.

Использование этих базовых вспомогательных методов для создания элементов управления вводом демонстрируется в примере ниже:

Создать пользователя

@Html.TextBox("userId", @Model.UserId)
@Html.TextBox("firstName", @Model.FirstName)
@Html.TextBox("lastName", @Model.LastName)
}

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

...

....

Генерация элемента управления вводом из свойства модели

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

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

@model HelperMethods.Models.User @{ ViewBag.Title = "CreateUser"; }

Создать пользователя

@using (Html.BeginRouteForm("FormRoute", null, FormMethod.Post, new { @class = "userCssClass", data_formType="user" })) {
@Html.TextBox("userId")
@Html.TextBox("firstName")
@Html.TextBox("lastName")
}

Аргумент string применяется для поиска в данных представления, в объекте ViewBag и в модели представления соответствующего элемента данных, который может использоваться в качестве основы для элемента input. Таким образом, например, если вызвать @Html.TextBox("DataValue"), то инфраструктура ASP.NET MVC Framework попытается найти элемент данных, который соответствует ключу DataValue. Будут проверяться следующие местоположения:

    ViewBag.DataValue

    @Model.DataValue

Первое найденное значение используется для установки атрибута value генерируемой HTML-разметки. (Последняя проверка, в @Model.DataValue, предпринимается, только если модель представления содержит свойство или поле по имени DataValue.)

Если указать строку вроде DataValue.First.Name, поиск становится более сложным. Инфраструктура ASP.NET MVC Framework опробует различные комбинации элементов, разделяемых точками, такие как перечисленные ниже:

    ViewBag.DataValue.First.Name

    ViewBag.DataValue["First"].Name

    ViewBag.DataValue["First.Name"]

    ViewBag.DataValue["First"]["Name"]

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

Использование строго типизированных вспомогательных методов для создания элементов управления вводом

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

Строго типизированные вспомогательные методы для создания элементов управления вводом
Метод Пример Генерируемый HTML-элемент
Html.CheckBoxFor() Html.CheckBoxFor(х => х.IsApproved)
Html.HiddenFor() Html.HiddenFor(x => x.FirstName)
Html.RadioButtonFor() Html.RadioButtonFor(x => x.IsApproved, "val")
Html.PasswordFor() Html.PasswordFor(x => x.Password)
Html.TextAreaFor() Html.TextAreaFor(x => x.Bio, 5, 20, new{})
Html.TextBoxFor() Html.TextBoxFor(x => x.FirstName)

Строго типизированные вспомогательные методы для создания элементов управления вводом работают с лямбда-выражениями. В такое выражение передается объект модели представления, в котором можно выбрать поле или свойство, используемое для установки атрибута value. В примере ниже показано, как этот вид вспомогательного метода применяется в представлении CreateUser.cshtml из примера приложения:

@model HelperMethods.Models.User @{ ViewBag.Title = "CreateUser"; }

Создать пользователя

@using (Html.BeginRouteForm("FormRoute", null, FormMethod.Post, new { @class = "userCssClass", data_formType="user" })) {
@Html.TextBoxFor(user => user.UserId)
@Html.TextBoxFor(user => user.FirstName)
@Html.TextBoxFor(user => user.LastName)
}

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

Создание элементов выбора

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

Вспомогательные методы HTML, генерирующие элементы выбора
Метод Описание Пример Генерируемый HTML-элемент
Html.DropDownList() Раскрывающийся список Html.DropDownList("myList",
new SelectList(new {"A", "B"}), "Выбрать")
Html.DropDownListFor() Раскрывающийся список Html.DropDownListFor(x => x.Gender,
new SelectList(new {"M", "F"}))
Html.ListBox() Html.ListBox("myList",
Html.ListBoxFor() Список с множественным выбором Html.ListBoxFor(x => x.Vals,
new MultiSelectList(new {"A", "B"}))

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

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

Разработка приложения в соответствии с шаблоном проектирования MVC (модель-представление-контроллёр) характерна для Java и применительно к DroidScript кажется непонятной и ненужной. Для чего всё усложнять? Ореол сложности и "магичности" MVC приобрёл по причине использования при его рассмотрении красивых, но непонятных слов (концепция, модель, бизнес-логика, паттерн) и сложных демонстраций в контексте Java. Всё намного проще: MVC - это один из шаблонов проектирования, при котором производится дополнительное разделении кода в объектно-ориентированной среде .

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

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

С точки зрения пользователя его работа с приложением при использовании MVC не изменилась: он также нажимает на кнопки, выбирает данные и способ их отображения. Изменения могут касаться удобства этой работы. А на стороне разработки изменения ощутимы: взаимодействие между данными и их отобржением в концепции MVC происходит через контроллёр и под его управлением .

Рассмотрим для начала простой пример использования MVC в однофайловом приложении.

Однофайловая реализация MVC-модели

Возьмём простое приложение.

Function OnStart(){ var _lay = app.CreateLayout("linear", "VCenter,FillXY"); var _btnShowVersion = app.CreateButton("Показать версию", 0.3, 0.1); _btnShowVersion.SetBackColor("#66778976"); _btnShowVersion.SetMargins(0, 0.05, 0, 0); _btnShowVersion.SetOnTouch(function(){ _btnShowVersion.SetText("Версия приложения 1.0"); }); _lay.AddChild(_btnShowVersion); app.AddLayout(_lay); }

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

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

Одна из задач шаблона MVC как раз и состоит в разграничении доступа: сначала определяется модуль (или блок кода), являющийся источником ошибки, а затем даётся доступ только к нему. Для чего давать доступ к электронике и мотору автомобиля, если нужно заменить колесо?

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

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

  1. Модель
  2. Представление
  3. Контроллёр
//+++ модель (function(){ var _obj = ; //+++ данные var _version = "Версия приложения 1.0"; var _titleShowVersion = "Показать версию"; //--- данные
//+++ открытые методы для доступа к данным _obj.getVersion = function(){ return _version; } _obj.btnGetTitle = function(){ return _titleShowVersion; } //--- открытые методы для доступа к данным window.model = _obj; // открываем доступ к локальному объекту })(); //--- модель //+++ представление (function (){ var _lay = app.CreateLayout("linear", "VCenter,FillXY"); var _btnShowVersion = app.CreateButton(window.model.btnGetTitle(), 0.3, 0.1); _btnShowVersion.name = "_btnShowVersion"; _btnShowVersion.SetBackColor("#66778976"); _btnShowVersion.SetMargins(0, 0.05, 0, 0); _lay.AddChild(_btnShowVersion); app.AddLayout(_lay);

})(); //--- представление //+++ контроллёр (function(p_object){ var _obj = ; // открытый метод поиска объекта _obj.findObjectById = function(p_name){ var _objectList = app.GetObjects(); for (var _i in _objectList){ if(_objectList[_i].name == p_name){ return _objectList[ _i]; } } return null; } window.control = _obj; })(); function OnStart(){ var _buttonShowVersion = window.control.findObjectById("_btnShowVersion"); //+++ действие _buttonShowVersion.SetOnTouch(function(){ this.SetText(window.model.getVersion()); }); // --- действие } //--- контроллёр

Из-за разделения функций код приложения увеличился в несколько раз.

Изначально все переменные сделаны закрытыми и только в конце при необходимости открывается доступ к ним через глобальный объект window, что позволяет обойтись без глобальных переменных.

В примере реализован поиск виджета, как в Java, но можно поступить проще и сделать код более эффективным, открыв доступ к объекту через глобальный ассоциативный массив:

Window.controls = ;
window.controls.buttonShowVersion = _btnShowVersion;

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

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

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

Для более глубокого понимания преимуществ использования модели MVC рассмотрим разделение кода по отдельным файлам.

Трёхфайловая реализация MVC-модели

Разделение кода по разным файлам используется для более удобной работы с ним. Огромное количество мелких файлов, которые можно видеть в MVC проектах, может поставить под сомнение это утверждение, но видеть файлы - это одно, а работать с ними - совсем другое. В каждый момент времени разработчик взаимодействует с одним файлом из какого-то небольшого их множества. Для этого необходимо хорошо понимать структуру организации проекта и постоянно отслеживать тройку файлов - модель, представление и контроллёр, чтобы случайно не отредактировать сторонний код. Из-за ограничений редактора DroidScript такая группировка возможна только по именам файлов в корневой директории, например:

myproject_model.js - модель
myproject_view.js - представление
myproject_control.js - контроллёр

Ниже показан пример разделения кода предыдущего примера по файлам.

myproject_model.js - модель (function(){ var _obj = ; //+++ данные var _version = "Версия приложения 1.0"; //--- данные //+++ строковый ресурс var _titleShowVersion = "Показать версию"; //+++ строковый ресурс _obj.getVersion = function(){ return _version; } _obj.btnGetTitle = function(){ return _titleShowVersion; } window.model = _obj; })(); myproject_view.js - представление (function (){ var _lay = app.CreateLayout("linear", "VCenter,FillXY"); var _btnShowVersion = app.CreateButton(window.model.btnGetTitle(), 0.3, 0.1); _btnShowVersion.name = "_btnShowVersion"; _btnShowVersion.SetBackColor("#66778976"); _btnShowVersion.SetMargins(0, 0.05, 0, 0); _lay.AddChild(_btnShowVersion); app.AddLayout(_lay); })(); myproject_control.js - контроллёр app.LoadScript("myproject_model.js"); app.LoadScript("myproject_view.js"); (function(p_object){ var _obj = ; // метод поиска объекта _obj.findObjectById = function(p_name){ var _objectList = app.GetObjects(); for (var _i in _objectList){ if(_objectList[_i].name == p_name){ return _objectList[ _i]; } } return null; } window.control = _obj; })(); function OnStart(){ var _buttonShowVersion = window.control.findObjectById("_btnShowVersion"); //+++ действие _buttonShowVersion.SetOnTouch(function(){ this.SetText(window.model.getVersion()); }); // --- действие }

Такое простое разделение кода по файлам получилось не спроста. Для этого заранее была установлена связь с моделью через открытое свойство глобального корневого объекта - window.model , а связь с представлением через глобальный массив _map посредством метода app.GetObjects .

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

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

В JavaScript объекты передаются по ссылке. Изменение свойств виджета в контроллёре изменит свойства самого виджета. Теоретически, можно отделить объекты представлений от объектов кода, как это сделано в Java, где в качестве первых используются xml-структуры, но большого смысла в этом нет по двум причинам - отсутствия в DroidScript визуального редактора интерфейса и ограниченного набора доступных свойств API-объектов.

Многофайловая реализация MVC-модели

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



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: