MVC, часть 1: про дубовый стол и сиськи
[Что-то очень глючил редактор, где то возможны нестыковки - текст порой просто удалялся. Замечания пишите в комменты. Спасибо]
...и вот появился на свет гений. Истинный Разработчик. Луч света падает на него, а клавиатура сокрушается под ударами его пальцев... Сейчас он сядет и сделает шедевр, в ритмах Вивальди извлекая звуки от щелчков мышки и ритмичных ударов о клавиатуре...
Думайте это о Вас? Обо мне? Пфф... Это Вам не сага о Горце. Тут придется включить свою голову. Ну или хотя бы сделать вид. Как бы то ни было - купите виски и откиньтесь на спинку кожаного кресла, положив свои ноги в ботинках из крокодиловой кожи на стол из столетнего дуба. Да-да, вы не ослышались. MVC - это действительно круто. Соплякам тут не место )
Мне всегда вот в научных статьях, учебниках по квантовой физике и статьях на википедии не нравилось одно - сухость материала. Ну пришел Шредингер, сказал что нашел универсальное уравнение в материи в целом - и понеслись иероглифы. Ну вот как это следует вообще понимать? Тут тоньше надо. Не спугнуть читателя, объяснять всё простым языком... Тут тренд Дисковери уловил, конечно. Поэтому давайте-ка я расскажу вам о трёх буквах.
Готовы? Отлично, так рад видеть на Ваших лицах заинтересованность - вот оно, "эм вэ цэ" да ещё и языком художественным текстом, где возможно. Только сразу огорчу - про дубовые столы я погорячился. Точнее, не погорячился, а совсем соврал. Это полная чушь, что MVC что-то там даёт. Да-да, я вас сильно обманул, но обещаю, что больше так делать не буду, а вы мне взамен обещайте что будете слушать меня внимательно. Договорились? Договорились. Дак вот, MVC - это просто инструмент, он не сделает из Вас ни "тру-флешера", ни человека способного на что-то большее, чем он умел до этого. Скажу больше, удобство использования вы ощутите... ну после нескольких недель работы. Это как накачать пресс - жирок то родной, булочки вкусные, а тренажеры не манят. Зато потом сверкая прессом перед девочками на пляже... ну вы поняли. Не испугались? Тогда вперёд )
Начнем, пожалуй, с истории среднестатистического флеш-разработчика. Речь, конечно же, пойдёт о человеке растущим на флеше с 0, а не матёрым Java`ером, который решил переквалифицироваться.
Сперва разработчик начинает писать свой код в кадрах/в одном классе. Он не понимает, зачем создавать другие классы, ведь всё можно написать здесь - и оформить функцией - то есть замыканием. Некоторые о приёме замыкания не знают - и слава богу - и пишут в методах.
Наступает момент, когда кода становится слишком много и под давлением общественности человек начинает делать попытки создавать другие классы. И тут же чувствует себя скованным - как же получить глобальную переменную... В следствии чего в конструктор пихает всё что только может. А ещё лучше - ссылку на класс, который его и создал. В итоге Main порождает пятерку других классов. И все знают всё о Main.
Потом человек понимает, что так передавать - крайне неэффективно. И начинает использовать синглтон. Ну как синглтон... Он думает, что это синглтон. На самом деле - просто набор статический методов и переменных. Ну должен же быть глобальный контроль!
И вот наконец человек понимает. Черт возьми, как же неудобно когда всё жутко связанно. Как много кода копируется копипастом... Как неудобно эту полоску жизней делать отдельно и для врагов, и для игрока - выгладят по разному, а работа одна и та же... Как же хочется чтобы можно было просто изменять внешний вид, а логика работы так и оставалась где то далеко... И тут мы у человека происходит озарение. Да-да, мы думаем об одном и том же. Человек начинает использовать наследование.
И вот только потом, через много времени он чувствует себя готовым. Готовым к большим открытиям и большим наградам. И виднеются на горизонте три большие, заглавные буквы - M, символизирующие Молодец разработчику, V, символизирующее пятерку за архитектуру и C - символизирующее Сиськи, которых порой так не хватает рядом во время длительной разработки. Ну, это для меня. MVC - у каждого своё. Вроде бы очевидно, что от MVC со временем не так то просто отказаться.
...Затянулось начало. Перейдём к делу.
Чтобы было сразу понятней - сразу же введем пример. Предположим, у нас есть герой у которого есть 3 вида оружия. Вид игры - сверху.
Рассмотрим отдельно все части:
M - Model. Модель. Хранит данные, которые нужны триаде (MVC). Это - координаты игрока в мире, текущие оружие в руках, количество здоровья, количество боеприпасов. Остальные данные - например количество крови хлыщущее из его вены в модели не хранятся.
V - View. Вьюшка. Отображалка. Занимается всем тем, что видит и слышит пользователь. Хранит в себе как раз количество крови, которое из игрока хлыщет, само изображение игрока, создаёт звуки выстрелов... Ну вы поняли. Это всё, что вообще есть на экране.
C - Controller. Контроллер. Он занимается логикой всей триады. Именно он обрабатывает нажатие клавиш и меняет данные в модели, записывая туда новые координаты игрока. Именно он разрешает игроку стрелять и меняет у него оружие.
Всё понятно? Не пытайтесь проводить грань и понять взаимодействие между ними - просто определитесь, кто что делает.
Сейчас я дам вам картинку, нагло взятую где то из интернета.
Какая прелесть, правда? Давайте разберемся. Сплошными линиями показаны жесткие связи между трио - то есть у контроллера есть прямая ссылка на модель и на вьюшку, а у вьюшки на модель. Штрихованными же показано кто кого слушает - контроллер слушает вьюшку, а вьюшка модель. Что такое "слушает" - ниже, на примере каждого.
Пример с игрой и человечком сверху был удачным для объяснения идеи текстом, но мне кажется для практической реализации нужно взять пример попроще. Давайте сделаем совсем элементарный пример: несколько квадратиков, все разных цветов. При этом есть одна стрелочка, которая указывает на один из квадратов. По клику стрелочка указывает на случайный квадратик, а каждые 3 секунды цвета у квадратов меняются. Это игра будет для детей дошкольного возраста: они должны будут произносить вслух цвет, на который указывает стрелочка, а воспитатель скажет, правильно или нет.
Итак. Сперва мы должны создать Главный контроллер - он всегда самый главный в любом приложении. У нас будет всего один контроллер, одна модель и одна вьюшка - поэтому контроллер главный, и по совместительству единственный. Контроллеру нужно знать, куда добавлять всю прелестную графику? Нужно. Поэтому ему нужен иметь какую-то точку привязки для графики - DisplayObjectContainer. Где её взять? Это - ваш класс Main, который базовый. Как создать главный контроллер? В Main, в конструкторе или в обработчике события Event.ADDED_TO_STAGE нужно написать примерно следующее:
Мы просто создали этот контроллер и передали ссылку на себя - то есть на то место, где будет скапливаться вся графика. Нам даже не нужно сохранять ссылку на главный контроллер!
Код класса BaseController:
package { import flash.display.DisplayObjectContainer; public class BaseController { private var _host:DisplayObjectContainer; private var _model:Model; private var _view:View; public function BaseController(host:DisplayObjectContainer) { super(); _host = host; _model = new Model(); _view = new View(_model); } } }
Давайте напишем модель. Сперва нужно понять, что должно быть в модели. В модели должны быть данные, которые нужны всей триаде. Это - цвета всех квадратов и номер квадрата, на который указывает стрелочка. "А как же графика для стрелочки, размеры квадрата, расстояние между ними и всё остальное?" - воскликнет читатель. Нет, эти данные не должны хранится в модели. Вся графика, все звуки и всё остальное - это забота вьюшки, в этом и прелесть: сегодня у меня на экране человек, а завтра киборг. При этом я переписываю только отображение, не трогая контроллер и модель, тем самым не "ломая" логику. Размер прямоугольников - не нужен в контексте этой задачи - нам плевать на форму, хоть квадратная, хоть скругленная. Нам так же плевать на позиции - хоть синусоидально, хоть по прямой. На логику это не влияет. Нам просто нужно выводить на экран какие-то цветные элементы и помечать их. Отсюда всплывает очередная прелесть - мы можем не сильно заботится о том, что творится на экране, оставив это на "потом". Будь другой контекст - например, если бы нужно было отгадать размер в пикселах этой фигуры - то эти данные попали бы в модель. Аналогично и с формой, и с расположением. Проще говоря, в модели хранятся значимые данные в данной задаче. А на фоне гусей или океана это происходит - программу не колышет.
Итак, нам нужно хранить в модели массив цветов и номер прямоугольника, на который указывает стрелка. При этом при изменении информации в модели неплохо было бы заявить всем желающим, что данные изменились - неплохо бы их обновить. Это означает, что модель должна испустить событие о том, что она изменилась. Делается это GoF паттерном Observer, но не пугайтесь: обычная событийная флешевая событийная модель реализует именно его, поэтому нам совсем не стоит об этом беспокоится. При обновлении информации есть 2 подхода: push и pull. При обновлении информации мы генерируем событие о том, что что-то изменилось. Представим, что это событие кто-то поймал. В событии может содержаться вся информация о том, что же изменилось - такой подход называется push - мы проталкиваем с событием всё что нужно обновить. Другой подход - pull - значит "тяни". В таком случае события носят нотификационный характер - что-то вроде "Эй парень, число народу в игре обновилось.". А сколько теперь онлайн - это событие не скажет. И в таком случае эти данные приходится "тянуть" с модели явно. Я использую оба подхода, оба подхода хороши. Но конкретно сейчас я ограничусь pull-подходом. Кстати, событийное поведение добавляет ещё одно удобство, одно из главных - если у нас в 5 разных местах показывается, например, баланс игрока - то при изменении баланса в одном месте он должен поменяться во всех.
Но тут всплывает проблема: поменяв номер прямоугольника, на который указывает стрелочка вызовется сеттер, который испустит событие об изменении. Но в случае с массивом всё не так: чтобы вызвать сеттер у массива нужно будет переопределить на него ссылку, что недопустимо: нам нужно просто менять элементы массива, но не ссылку на него. Решений у проблемы несколько. Например, самый извращенный - это унаследовать модель от Proxy, определив там поведение для [], попутно реализовав ей IEventDispatcher. Можно, например, сделать метод setElement, в теле которых и генерировать событие об изменении. Можно в модели добавить метод "сделай событие пожалуйста, что ты изменилась" и дёргать его. Но немного подумав вспоминаем, что модель мы меняем в контроллере. А в контроллере мы имеем полный контроль над вьюшкой. Поэтому в этом случае на мой взгляд самым лучшим решением будет явно вызвать метод у вьюшки после того как закончится изменение модели.
Код Model:
package { import flash.events.Event; import flash.events.EventDispatcher; [Event(name="change", type="flash.events.Event")] public class Model extends EventDispatcher implements IReadableModel { private var _pointer:int = 0; public var colors:Array = []; public function Model() { super(); } public function get pointer():int { return _pointer; } public function set pointer(value:int):void { if (_pointer == value) return; _pointer = value; super.dispatchEvent(new Event(Event.CHANGE)); } } }
Итак, теперь View. Как мы поняли ранее, у View есть прямая ссылка на Model, в итоге нам нужно всего лишь визуализировать эти данные.
package { import flash.display.Shape; import flash.display.Sprite; import flash.events.Event; public class View extends Sprite { private var _model:Model; private var _squares:Array; private var _pointer:Shape; public function View(model:Model) { super(); _model = model; //здесь будем хранить ссылки на созданные графические объекты _squares = []; //это типа стрелочка _pointer = new Shape(); _pointer.graphics.beginFill(0x00FF00); //круглая такая стрелочка _pointer.graphics.drawCircle(0, 0, 30); _pointer.y = 100; _pointer.graphics.endFill(); super.addChild(_pointer); //вызываем обработчик как будто-бы модель изменилась. Это нужно чтобы указатель //сразу встал на нужный квадрат onPointerChange(null); //подписались на изменение модели _model.addEventListener(Event.CHANGE, onPointerChange); } public function updateSquares():void { //поменялись цвета - удаляем все старые прямоугольники var i:int = _squares.length; while (i--) super.removeChild(_squares.pop()); //рисуем новые i = _model.colors.length; while (i--) { var shape:Shape = new Shape(); shape.graphics.beginFill(_model.colors[i]); shape.graphics.drawRect(0, 0, 50, 50); shape.graphics.endFill(); shape.x = 100 * i; _squares.push(shape); super.addChild(shape); } } private function onPointerChange(event:Event):void { _pointer.x = 100 * _model.pointer; } } }
Абстрагируемся. Нам сообщают событиями о том, что модель изменилась. Если мы получили такое сообщение - меняем изображение, чтобы быть актуальным каким-то данным, которые есть в модели. Попутно имеем паблик метод updateSquares, который будет вызван кем-то сверху, когда нужно обновить квадраты. Если не думать о трио в целом, а только об одном элементе - всё очень просто.
Резюме. Мы имеем что-то, что предоставляет нам данные и говорит, когда данные поменялись. На основе этого мы строим изображение. Всё просто? Отлично. Теперь связываем в голове модель и вьюшку. Связываем, думаем.
Готовы продолжать? Теперь контроллер.
Задача контроллера - менять модель. Просто писать в неё данные. Абстрагируемся вновь. Мы просто делаем какую-то логику, и пишем её в объект, не задумываясь о том что происходит дальше. Но ещё нам нужно вот что сделать в контроллере: добавить вьюшку в дисплай лист - т.е. просто сделать addChild к тому, что мы передали в конструктор главному контроллеру. По условию задачи менять каждые 3 секунды цвета. Что-ж, ничего сложного! Делаем:
package { import flash.display.DisplayObjectContainer; import flash.events.TimerEvent; import flash.utils.Timer; public class BaseController { private var _host:DisplayObjectContainer; private var _model:Model; private var _view:View; public function BaseController(host:DisplayObjectContainer) { super(); _host = host; _model = new Model(); _view = new View(_model); //создали таймер и запустили var _timer:Timer = new Timer(3000); _timer.addEventListener(TimerEvent.TIMER, onTimer); _timer.start(); //сразу же вызвали обработчик, чтобы сразу при запуске забить массив //случайными квадратами onTimer(null); //добавили в дисплай лист - чтобы мы смогли увидеть работу вьюшки host.addChild(_view); } private function onTimer(event:TimerEvent):void { //забиваем модель случайными цветами var i:int = 5; while (i--) { _model.colors[i] = int(Math.random() * 0xFFFFFF); } //говорим, что мы изменили цвета //это как раз тот момент из за невозможности вызова геттера _view.updateSquares(); } } }
Как это сделать? Наверное вам в голову лезет мысль - в контроллере отлавливать клик и менять модель. Нет, это не совсем правильно: это лепка программы "под конкретный вариант", а это как раз то, против чего создано MVC. Пользователь с чем взаимодействует? Только с вьюшкой. Он не может кликнуть по модели или по контроллеру - он кликает по экрану, что по сути является вьюшкой. Значит клик должна обрабатывать вьюшка. При этом это слишком связано, что именно по клику: сегодня по клику, завтра по нажатию клавиши, а послезавтра вообще по голосовой команде. Вьюшка должна сообщить о том, что надо бы поменять стрелочку - а уж отчего вьюшка так решила знает только она сама. В итоге во View дописываем функционал, что по клику диспатчим какое-то событие, которое слушает контроллер. Я опять буду использовать событие Event.CHANGE - поначалу могут возникнуть сложности - как же так, мы ведь его уже используем в модели - но это здорово развивает абстракцию. В реальных проектах типов событий у меня очень часто больше 30-40, тут, конечно, одним Event.CHANGE не обойтись и я делаю свой класс, расширяющий Event. Однако, какой случай - такой и инструмент, поэтому здесь Event.CHANGE.
package { import flash.display.Shape; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; [Event(name="change", type="flash.events.Event")] public class View extends Sprite { private var _model:Model; private var _squares:Array; private var _pointer:Shape; public function View(model:Model) { super(); _model = model; //здесь будем хранить ссылки на созданные графические объекты _squares = []; //это типа стрелочка _pointer = new Shape(); _pointer.graphics.beginFill(0x00FF00); //круглая такая стрелочка _pointer.graphics.drawCircle(0, 0, 30); _pointer.y = 100; _pointer.graphics.endFill(); super.addChild(_pointer); //вызываем обработчик как будто-бы модель изменилась. Это нужно чтобы указатель //сразу встал на нужный квадрат onPointerChange(null); //подписались на изменение модели _model.addEventListener(Event.CHANGE, onPointerChange); //нам должен быть доступен stage super.addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); } private function onAddedToStage(event:Event):void { super.removeEventListener(Event.ADDED_TO_STAGE, onAddedToStage); //подписались на заветный click super.stage.addEventListener(MouseEvent.CLICK, onClick); } private function onClick(event:MouseEvent):void { //и выпустили событие super.dispatchEvent(new Event(Event.CHANGE)); } public function updateSquares():void { //поменялись цвета - удаляем все старые прямоугольники var i:int = _squares.length; while (i--) super.removeChild(_squares.pop()); //рисуем новые i = _model.colors.length; while (i--) { var shape:Shape = new Shape(); shape.graphics.beginFill(_model.colors[i]); shape.graphics.drawRect(0, 0, 50, 50); shape.graphics.endFill(); shape.x = 100 * i; _squares.push(shape); super.addChild(shape); } } private function onPointerChange(event:Event):void { _pointer.x = 100 * _model.pointer; } } }
Теперь немного перепишем контроллер:
package { import flash.display.DisplayObjectContainer; import flash.events.Event; import flash.events.TimerEvent; import flash.utils.Timer; public class BaseController { private var _host:DisplayObjectContainer; private var _model:Model; private var _view:View; public function BaseController(host:DisplayObjectContainer) { super(); _host = host; _model = new Model(); _view = new View(_model); //создали таймер и запустили var _timer:Timer = new Timer(3000); _timer.addEventListener(TimerEvent.TIMER, onTimer); _timer.start(); //сразу же вызвали обработчик, чтобы сразу при запуске забить массив //случайными квадратами onTimer(null); //добавили в дисплай лист - чтобы мы смогли увидеть работу вьюшки host.addChild(_view); //подписались на событие, по которому нужно изменить модель _view.addEventListener(Event.CHANGE, onViewChange); } private function onViewChange(event:Event):void { //просто случайная позиция для стрелочки _model.pointer = Math.random() * 5; } private function onTimer(event:TimerEvent):void { //забиваем модель случайными цветами var i:int = 5; while (i--) { _model.colors[i] = int(Math.random() * 0xFFFFFF); } //говорим, что мы изменили цвета //это как раз тот момент из за невозможности вызова геттера _view.updateSquares(); } } }
Теперь пролистните вверх и посмотрите снова на картинку и вновь перечитайте часть, где я рассказывал про связи между M, V и C (Model, View и Controller). Теперь всё куда понятней, не правда ли?
Но и это не всё.
View не может изменять Model, как вы уже поняли, а у нас может. Как это исправить? Вью нужно передавать не саму модель, а только интерфейс, в которым "порезаны" все set методы. Например, в нашем случае такой:
package { public interface IReadableModel { function get pointer():int; function get colors:Array; } }
Однако, мы всё равно можем изменить элемента массива colors. Можно, конечно, придумать метод getElementAt, который бы лез в массив и возвращал элемент на заданной позиции, тем самым запретив изменять элементы массива... однако запрет на изменение модели не имеет другого характера, кроме как "защиты от дурака/запарки", чтобы случайно вы сами не поменяли модель. Мне не было бы стыдно оставить интерфейс в виде, который я привёл чуть выше - если вы не доверяете себе или людям, с которыми работаете - пишите метод. Это - инструмент, и он должен подстраиваться под вас, а не вы под него. Не стесняйтесь смотреть на картинку - она сильно помогает первое время. Если она даже сейчас кажется вам сложной - не смотрите на неё, а читайте текст и рисуйте эту картинку у себя на бумажке. Не ленитесь, это действительно помогает.
На этом я закончу первую лекцию. В следующих мы будем ветвить контроллеры, делать иерархические модели, общаться с сервером. В общем, в следующие разы будет действительно интересно. Stay tuned
Вторая часть.
Всего комментариев 231
Комментарии
17.02.2011 19:11 | |
BlooDHounD, а куда он у вас делся? Или вы переход на второй кадр делаете?
|
17.02.2011 20:04 | |
эээ ... никуда не делся. удалился. а как без перехода на второй кадр инитироваться? и причём тут это?
|
18.02.2011 02:15 | |
Даже если сделать переход на второй кадр это ничего не изменит.
|
18.02.2011 03:05 | |
не нужен? тогда как инитиализуются все остальные классы? вы считаете что моя религия запрещает мне удалить прелоадер после того как он отработал?
|
18.02.2011 11:39 | |
alatar, можно забрать stage у factoryClass, а потом удалить его. Добавлять всё не в factoryClass, а прямо на сцену. (Если я правильно понял о чем Вы)
|
18.02.2011 12:27 | |
Да, действительно.
|
18.02.2011 16:10 | |
alatar, getDefinitionByName - это конечно замечательно, но откуда он возьмётся без перехода на второй кадр? я тоже подразумевал factoryClass. он какой-то особенный?
|
18.02.2011 18:08 | |
Мда... пора отдохнуть, что-то я торможу и несу ахинею.
|
28.11.2011 11:13 | |
картинки грузит въюшка, так как другая въюшка может быть и без картинки. Допусти есть такой класс flash.display.Loader, указываете координаты и говорите грузить какой ни будь файл. Остальное он сам сделает. Правда я бы не советывал использовать именно Loader, лучше посмотрите вот это
|
28.11.2011 15:11 | |
Но в моделе не м.б. картинки, т.к. завтра картинка может поменяться же или я опять все напутал? Как я понял, в моделе даже не стоит хранить ширину и высоту картинки.
|
28.11.2011 17:16 | |
Цитата:
Так вот то что будет меняться в зависимости от реализации - это вьюер, а то, что не будет - это модель.
|
28.11.2011 19:22 | |
+1 к тому что сказал gloomyBrain
|
29.11.2011 12:27 | |
@fish_r
Цитата:
Надо вьюеру - пусть он меняет. Не суть. Просто вьюеру это делать, как правило, не с руки, ибо "один класс - одна задача", поэтому это сваливают контроллеру.
@crazyone Цитата:
типа вот так у нас выглядит кнопка, то в модели они не должны быть.
Нет, я согласен, его можно назвать как угодно, но BitmapData, как меня ни убеждайте, для меня все равно останется данными, а не отображением. А то что хранит данные для меня останется моделью. ЗЫ Я вообще хотел сказать одну простую вещь: то ЧТО мы показываем - мы храним в модели. То КАК мы показываем - мы храним во View |
|
Обновил(-а) gloomyBrain 29.11.2011 в 13:15
|
29.11.2011 13:24 | |
Цитата:
Sprite тоже данные хранит x, y, width, height, а рендерит его фп. Sprite модель?
|
29.11.2011 14:15 | |
Цитата:
И вряд ли у Вас возникнет задача сделать консольную галерею картинок =)
|
29.11.2011 14:18 | |
Цитата:
и данные начинают смешиваться с отображением
Цитата:
View, в свою очередь, может сам реализовывать MVC.
|
29.11.2011 14:58 | |
Я считаю BitmapData данными, которые нужно сугубо вьюхе, посему не выношу их в модели триады.
|
29.11.2011 16:11 | |
Последние записи от Psycho Tiger
- Тонкости и трюки ActionScript`а, которые... бесполезны (10.05.2011)
- Vkontakte: как пользоваться wall.post, нужен ли теперь wall.savePost? (05.03.2011)
- А пятый контер-страйк хорош. (19.01.2011)
- Пацаны, гоу Вконтакте? (21.12.2010)
- Давайте начистоту (18.12.2010)