MVC, часть 2. Лирика и теория.
Первая часть.
Жил был контроллер. Умный был... шибко умный. И хотел он чтобы его кто нибудь слушал... Хотел он холить и лелеять. И тут появилась она. Модель. Ничего лишнего, никаких порочных связей. Она была сама по себе, худа - из веса её кости, да обсервер - и красива - куда ни глянь, там аксессоры. Контроллер знал что делать.
Ловким движением он выцепил ссылку на модель и вонзил в себя. Теперь она его. Он вошел в неё и увидел много разных аксессоров к разным данным... Да, модель интересная личность.
Время летело незаметно. Не заметила модель, как появился в доме малыш вьювер, который беспрекословно слушал каждое слово своего папы и частенько думал о том, что стоит исполнять просьбы мамы. И был бы конец этой истории, но...
Контроллер всегда был единомышлеником. Он не знал как оправдаться перед своей моделью о непонятно откуда взявшейся вьюшке. Похоже, не одна модель у контроллера...
MVC, часть 2.
Итак, сразу скажу. Общепринятые понятия были даны в первой статье. Всё что здесь - является осколками моего разума по этой теме и кусочками того, что мне подсказали другие люди. Здесь нет правды, здесь нет лжи. Я так вижу моё MVC. Какое оно будет у вас - я не знаю.
Связь с внешним миром.
Здесь есть несколько вариантов. Все основаны на следующем: главный контроллер создаёт некий коннектор к серверу, который предоставляет данные, полученные с него, и имеет функционал который способен эти данные отправить.
Вариантов реализации - масса.
GoF паттерн Command: коннектор формирует команду и отправляет её контроллеру.
Что я имею ввиду: например, с сервера пришел xml
Код:
<response><action>chat</action><message>Hello!</message></response>
var c:Command=new Command(); c.action=xml.action.toString(); c.info=xml.message.toString();
Client: заводим в контроллере метод chat, а когда нам приходит команда от сервера - пример xml`ки сверху - делаем что-то вроде
try{ _controller[xml.action.toString()].apply(this,xml.message.toString); } catch (error:Error){ trace("Method not founded"); }
Такое управление, кстати говоря, носит имя RPC.
Можно выдумать много разных вариантов как обрабатывать информацию с сервера - и все будут правильными. Далее я подробно опишу тот, который использую я.
Для начала заведем класс для работы с сервером. Сервера у нас нету, поэтому будем использовать следующий:
package { import flash.utils.setTimeout; public class ServerConnector { private var _controller:BaseController; public function ServerConnector(controller:BaseController) { super(); _controller = controller; } public function sendSomeCommand():void { setTimeout(randomCommand, 300); } private function randomCommand():void { var xml:XML =<xml/>; switch (Math.floor(Math.random() * 3)) { case 0: xml.action = "chat"; xml.data = "Hello world!"; break; case 1: xml.action = "add"; xml.data = "monster"; break; case 2: xml.action = "add"; xml.data = "human"; break; default: break; } _controller.onServerData(xml); } } }
Вот код главного контроллера:
package { import flash.display.DisplayObjectContainer; import flash.events.MouseEvent; public class BaseController { private var _host:DisplayObjectContainer; private var _connector:ServerConnector ; public function BaseController(host:DisplayObjectContainer) { super(); _host = host; _host.stage.addEventListener(MouseEvent.CLICK, onStageClick); _connector = new ServerConnector(this); } private function onStageClick(event:MouseEvent):void { _connector.sendSomeCommand(); } public function onServerData(xml:XML):void { var command:String = xml.action.toString(); switch (command) { case "chat": trace("Chat message: " + xml.data.toString()); break; case "add": switch (xml.data.toString()) { case "human": trace("Adding a human"); break; case "monster": trace("Oh no, a monster!"); break; } break; } } } }
Но веселее становится если начать ветвить контроллеры. Эта вещь весьма спорная, но рассказать о ней стоит.
Ветвим контроллеры.
Зачем это нужно? Ну, например есть курилка, где можно найти себе соперника и есть основное поле боя. Очевидно, что между ними нет почти ничего общего, а значит, наверное, и логику было бы логично держать в разных контроллерах, верно?
Немного об их иерархии. Нижний контроллер может просить о чем - то верхнего, что понятно. Но может и... приказать! Например, выход из курилки в меню выбора персонажей - главный контроллер не может принять решение не удовлетворить такой переход. Переход обязан состояться, если контроллер курилки решит, что перейти нужно. Поэтому контроллеры общаются как событиями, так и напрямую.
Тут важно понять следующее: контроллер является дочерним, но это не значит что модель с которой этот контроллер работает тоже будет дочерней моделью. Дочерний контроллер может работать с любой моделью или с несколькими, даже только с главной моделью. Даже изменяя её. Если он её меняет - тогда дочернему контроллеру ставится статус "старшенького" сыночка по отношению к модели, которому можно. Бывают ситуации, когда контроллеру нужно считать что то с главной модели, но изменять её он право не имеет. Тогда он "младшенький". Ну это так, на пальцах. Порой и 140 кг гиганта бокса зовут малышкой, так что как кого называть - это ваше дело )
Не пугайтесь, если это показалось запутанным, это просто лирика.
Как реализовывать?
В главном контроллере когда нужно происходит нечто вроде
А в дочернем:
package { import flash.display.DisplayObjectContainer; import flash.events.EventDispatcher; import flash.utils.setTimeout; public class SmallController extends EventDispatcher { private var _baseController:BaseController; private var _view:DisplayObjectContainer; public function SmallController(baseController:BaseController, view:DisplayObjectContainer) { super(); _view = view; _baseController = baseController; setTimeout(_baseController.getOut, 200); } } }
Обычно дочерних контроллеров бывает несколько. Наверное, у вас появились такие мысли: наверное, разумно будет завести интерфейс ISmallController и реализовать его всем дочерним контроллером. В этом интерфейсе будет метод destroy. И интерфейс IBigController. В нём будет метод getOut и, возможно, get model. Потому что достаточно часто мелкому контроллеру нужно знать модель большого - в ней, например, хранится имя игрока.
Зачем? Всё очень просто. Зачем нужен большой интерфейс, я думаю, всем понятно - это инкапсуляция. С мелким интерфейсом интереснее: мелких контроллеров может быть много, и бывает так, что надо при их удалении просто дёрнуть метод destroy. Если сигнатуру метода getOut изменить вот так:
то можно один метод дёргать многими малышами.
Мысли, конечно, хорошие. Момент в том, что вы дёргаете метод, который дёрнет ваш метод. То есть вместо вызова getOut можно сразу вызвать destory, и эффект будет 1 в 1. Да и не бывает так, что контроллер удалился бесследно для других - в старшем контроллере надо что-нибудь да наколдовать. С подходом как есть сейчас, наверное, самым лучшим будет банальная проверка через is. А ещё лучше через switch:
switch (controller){ case _smallController1: break; case _verySmallController: break; case _angelicaBoobsController: break; }
Доставка сообщений в нужный контроллер.
Под сообщениями подразумеваются сообщения с сервера.
Если мы ветвим контроллеры - хотелось бы чтобы каждый контроллер мог пощупать сервер. Все щупки осуществляются через BaseController, у которого метод "отправить на сервер" помечен как public. А все дочерние контроллеры имеют к нему доступ. То есть мы просто дёргаем этот метод и говорим, что отправить. Всё просто.
Интереснее с доставкой сообщения от сервера клиенту. Давайте взглянем по логике: первым его получает ServerConnector (экземпляр класса, работающий с сервером), вторым его должен получить BaseController. Потом его младшие, потом младшие младших и так далее. Так и поступим. Ранее был рассмотрен пример получения сообщений от сервера, там был метод onServerData. Сообщение, собственно, попадает первым делом в этот метод. Главный контроллер парсит его и решает что делать. Он может в любом момент прекратить распространение сообщения, сделав return в методе. Он может в это сообщение написать матерное слово. Он как бы его хозяин.
В конце этого метода следует сделать dispatchEvent, а событие должно содержать сообщение от сервера. Все дочерние контроллеры имеют ссылку на базовый контроллер? Имеют. Они подписываются на событие отправки события у главного контроллера и выполняют какие-либо действия, если посчитают нужным. Более старшие контроллеры подписываются в коде раньше, чем более младшие, тем самым при одинаковых приоритетах прослушивания событий сообщение сперва попадёт в старший контроллер, а потом в младший. Как мы и планировали!
Но часто бывает так, что сообщение прошло через главный контроллер, но ему на это сообщение наплевать. Например, это сообщение чата. А сообщение чата очень хочет получить контроллер чата. С методом выше, конечно, всё получится. Но давайте представим, что контроллер чата вдруг умер. Отвалился. Сгорел. Значит сообщение чата будет существовать, но обработки не получит. Другими словами, это сообщение мы вдруг проворонили. И кто виноват? А сообщение пришло с сервера? А нижний контроллер его поймал? А может случилось так, что всё прошло хорошо, но это сообщение просто не вывелось на экран?
Столько вопросов... Знакомая ситуация? Её серьезно облегчает MVC.
Вы знаете, что есть у объекта Event такое свойство как cancelable? Слыхали, наверно. В общем, в 2 словах - наверняка вы замечали, что делая dispatchEvent этот самый метод возвращает какое-то там булево значение. Ах да, удалось ли событие отправить или нет. Не задумывались - как это так, удалось ли отправить событие? Возможно, конечно, тут вольности моего перевода. Но смысл такой - был ли вызван preventDefault для этого события. А вызвать его можно только в обработчиках. Другими словами:
trace( dispatchEvent(event) ); private function handler(event:Event):void{ event.preventDefault(); }
К чему я клоню? А вот к чему. Если событие вдруг нашло адресата в лице младшего контроллера - событию зовётся preventDefault.
Вот так рассылаем событие всем желающим:
Например вот так написан код у желающих (внутри обработчика события)
switch (command) { case "getFriendsInfo": ... break case "questGetInvite": ... break; default: return; } event.preventDefault();
Мысли о ветвлении моделей.
Слышали о GoF Composite? DisplayList во флеше реализует именно его. Реализация заключается в том, что одних детей можно вкладывать в другие. У нас - методом addChild(At). Чем прелестно? Тем, что наблюдается строгая иерархия. Зачем это надо? Например, есть у вас модель дома. В доме - квартира. В квартира - шкаф. В шкафу - надувная женщина. Матрешка, в общем. Но пока главный герой ходил в театр его дом взорвали. Совсем взорвали. Модель дома должна удалится. А вместе с тем должны удалится квартиры в нём, шкафы... ну и элементы личной жизни. Стрёмной, но личной.
Вообще такая иерархия реализуется обычными полями класса. Если в модели House завести поле Cabinet, а в нём Woman - то удаляя House удалятся и его поля, по сути говоря. То есть то, что мы и хотели. Но такое ветвление моделей... не очень. Женщина всего одна в шкафу. И делая дом Казановы - надо бы трёх женщин в дом, пятерых в шкаф. А у Моисеева и не женщин вовсе.
И вот сталкиваемся с проблемкой - казалось бы, модели похожие. Все дома имеют количество этажей и объем, к примеру. А с другой стороны - дома разных людей отличаются наполнением.
Нет, можно конечно создавать массив Women, давая неограниченное количество женщин в дом (не истекайте слюной, по сюжету они резиновые), но ведь не предусмотришь всего-всего. Наверное, можно завести один большой массив. Нетипизированный. И туда пихать всё что хотим! Отличная идея. Теперь в домах есть всё что угодно, неплохо. Причем где-то объект может содержать в себе ещё что-то (Cabinet), а что-то не может (Woman. Нет-нет, вы можете запихать в неё что-нибудь, мы не о том). Было бы здорово разделять одиночную модель и композитную. В которую нельзя класть и в которую можно...
И вот мудрим-мудрим-мудрим и приходим к тому, что идеально это всё реализовано в дисплай листе. Однако я считаю это плохим тоном - использовать в качестве моделей DisplayObject. Расходы памяти больше, по автокомплиту много чуши... да и ощущение кривизны накатывает )
Лучше реализовывать такой самостоятельно. Но есть у DisplayObject`ов плюс - event bubbling. Это 2 параметр в конструкторе Event, если кто не понял. Можно отправить событие бабблясь... Зачем такое может пригодится?
Например, есть у вас игра клёвая. В игре периодически появляются здания, на здании появляются пушки и вертолеты и всякое такое. И иногда это неудобно - создавать модель, потом создавать вьюшку, кормить вьюшку моделью. Куда удобней было бы просто создать модель и сказать ей - "сядь на крышу здания" А вьюшка сама взяла бы - и подцепилась к вьюшке здания. Создали нового солдата - его вьюшка подцепилась к вьюшке заднего двора, где они респаются. Фантастика?
Да нет же. Как только модель добавляется - т.е. вызывается addChild в любой модели - должно диспатчиться событие Event.ADDED. Оно бабблится до самой главной модели игры. И где то там его ловит главная вьюшка. Она спрашивает у модели - что приключилось? А модель ей - мне поступили сведения что новая модель присоединилась ко мне! И вьюшка цепляет новую вьюшку. Туда куда надо. Повторяя иерархию прям в моделях. Вот и бабблинг пригодился.
Конечно, можно делать оптимизации. Например, бабблинг необязателен: модель говорит о том, что в неё кого-то добавили. И вьюшка этой модели сразу добавляет нужную вьюшку. Я просто хотел показать вам, что бабблинг в моделях иногда нужен. Например, в контексте этой задачи для подсчета прицепленных моделей. Нотификация "пересчитай сколько у тебя моделей" или "я присоединился и присоединил с собой 3 модели!" отправляется наверх именно бабблищимся событием.
Нависает вопрос. Как же реализовать бабблинг, не пользуясь DisplayObject`ами? Есть у меня несколько идей. И у вас должны быть.
На этом я закругляюсь. Информации здесь я изложил даже более, чем обычно использую на практике. MVC у каждого свой, не бойтесь экспериментировать. Изложенная выше теория - набор моих мыслей, которые я более-менее использую на практике. Они служат пищей, но если вы придумали, например, новый способ доставки сообщений по контроллерам - не стесняйтесь его использовать. Делайте как вам удобно. MVC создан помогать, а не угнетать.
Кстати, взгляните на картинку:
http://www.flasher.ru/forum/showpost...54&postcount=2
Она всё ещё вызывает у вас страх и ужас?
От автора:
Если соберусь с силами - напишу 3 часть. Часть будет целиком состоять из примера с объяснением - как же реально работает большой проект на MVC. Но думать что в ней всё прояснится ненужно - в неё не будет ничего нового. Только скомпонованные знания из первой и второй "в реальной битве".
Всего комментариев 37
Комментарии
![]() ![]() |
|
Ох. Ну вот не люблю я кэйсы.. Для Messages и Actions лучше всё-же стратегия наверное..
public function onServerData(xml:XML):void { // создаём команду нужного класса (наследника BaseCommand ) и заполняем поля var command:BaseCommand = BaseCommand.serializeFromXML(xml); // выполняем действие над моделью. command.execute(this /* контроллер*/, model/*Model*/); } Экшены можно повторно использовать ( с определёнными условиями конечно) и нет чёртовых кэйсов) |
|
Обновил(-а) Котяра 03.12.2010 в 00:20
|
![]() ![]() |
|
Даешь часть 3! =) Отличная статья, читал и получал удовольствие. Правда с женщинами во множественном и единственном числе есть проблемы (в плане написания)
|
![]() ![]() |
|
Спасибо.
Покажи пожалуйста где. Можно в личку/аську) |
![]() ![]() |
|
а регистрацию классов
хорошо делать с помощью registrerClassAlias("chat", ChatMessage); либо просто заносить в хэш какой-нибудь а потом в классе BaseCommand |
![]() ![]() |
|
Цитата:
А мне приходит под 40 команд для одного контроллера и одной модели
кэйс из 40 элементов - это намного хуже. При добавлении 41-го сообщения:
И MVC тут вроде не причём вообще. Стратегия применима и к другим архитектурам. |
|
Обновил(-а) Котяра 06.12.2010 в 02:31
|
![]() ![]() |
|
Ну хозяин барин. По мне - 80 классов завести самое то.
они очень хорошо выглядят в папке project. захотел - исправил метод execute в классе GalaktekoOpasnosteMessage чем искать какието кэйсы и свичи. |
|
Обновил(-а) Котяра 06.12.2010 в 02:35
|
![]() ![]() |
|
Я тоже использую AMF формат для общения с сервером (Zend AMF и Red5) Маппинг работает нормально. Для мелких проектов маппинг использовать не рационально. Мелкий, это 10-30 команд
|
![]() ![]() |
|
Я не отрицаю рациональность. Я говорю что нет понятия "нерационально". Более рационально, менее рационально - есть. Конечная инстанция в виде "нерационально" - нету.
|
![]() ![]() |
|
Огромное спасибо за обе статьи по MVC!
Планируешь ли ты обещанную третью часть? |
![]() ![]() |
|
Цитата:
Переделываю проект на новый лад, ощутил прелесть композиции модели. Хочется всё-таки выяснить более конкретно по этому поводу.
Мне пока в голову приходит только следующий способ: создать классы одиночной модели и модели-контейнера. примерно Model: AnyDataItem -> DataContainer -> Data -> EventDispatcher -> ... View: AnyViewItem -> DisplayObjectContainer -> DisplayObject -> ... Data - при изменении парента, по идее может (хотя может быть должна) диспатчить Added\Removed, но у меня такой необходимости, пока, не было. я знаю, что у Вlooddy есть реализация: Цитата:
Нависает вопрос. Как же реализовать бабблинг, не пользуясь DisplayObject`ами?
|
|
Обновил(-а) СлаваRa 13.01.2012 в 12:50
|
![]() ![]() |
|
Да, схема известная. В теории вполне понятно. Пытаюсь на практике реализовать
|
![]() ![]() |
|
Фишка в зеркальности данных, понятно.
|
![]() ![]() |
|
Да, это очень сильно упрощает и ускоряет разработку.
|
![]() ![]() |
|
![]() ![]() |
|
Цитата:
|
![]() ![]() |
|
И проверь ЛС, если еще не видел нового сообщения
![]() |
![]() ![]() |
|
Цитата:
Просто гениально! Гениально просто! Короче, лошадиный восторг!) |
![]() ![]() |
|
![]() ![]() |
|
Цитата:
Интересненько Psycho Tiger придумал с
|
![]() ![]() |
|
Та я-то не сам придумал. Все было до нас. Это GoF chain of responsibility
|
Последние записи от Psycho Tiger
- Тонкости и трюки ActionScript`а, которые... бесполезны (10.05.2011)
- Vkontakte: как пользоваться wall.post, нужен ли теперь wall.savePost? (05.03.2011)
- А пятый контер-страйк хорош. (19.01.2011)
- Пацаны, гоу Вконтакте? (21.12.2010)
- Давайте начистоту (18.12.2010)