ООП ради ООП ч. 2. Мучаем robotlegs
Запись от Rembrant размещена 21.07.2014 в 19:22
Некоторое время назад я пытался создать as3-приложение на основе библиотеки pureMVC, результат этих мучений можно лицезреть здесь. Среди прочего, было отмечено крайнее неудобство pureMVC как таковой, что сподвигло меня покопаться для сравнения ещё в одном MVC-фреймворке, robotlegs, знание которого также требуется довольно часто.
Итак, представляю на суд публики ту же самую галлерею, но уже на основании robotlegs. На оф. сайте доступны 2 версии - 1.5.2 и 2.2.1, соответственно у меня получилось 2 проекта, потому что реализация на этих двух версиях НУ ОЧЕНЬ отличается. Дальше попытаюсь разжевать (для себя в том числе) код проекта для версии 2.2.1 как самой прогрессивной, ну и в конце упомяну про отличия.
Часть 1. Программка на robotlegs 2.2.1
Дерево проекта:
Как и все каноничные MVC-фреймворки, robotlegs имеет модели, команды и медиаторы. Плюс - контекст. Контекст - это класс самонастройки, инициализирующий зависимости между компонентами программы. Он инициализируется в Main одной строкой:
GalleryContext - мой класс, который конфигурирует библиотечный Context следующим образом.
GalleryContext.as:
package app { import flash.display.DisplayObjectContainer; import robotlegs.bender.bundles.mvcs.MVCSBundle; import robotlegs.bender.extensions.contextView.ContextView; import robotlegs.bender.framework.api.IContext; import robotlegs.bender.framework.impl.Context; public class GalleryContext { public function GalleryContext(view:DisplayObjectContainer) { context = new Context() .install(MVCSBundle) .configure(GalleryConfig) .configure(new ContextView(view)); } private var context:IContext; } }
- конфигурируется ContextView, куда передаётся наш view, то есть Main;
- конфигурируется пользовательский GalleryConfig, в котором описаны все команды, медиаторы и модели.
GalleryConfig.as:
package app { import app.controller.ClosePreviewCommand; import app.controller.CreateMainPanelCommand; import app.controller.ImageClickCommand; import app.controller.LoadAllCommand; import app.events.GalleryEvent; import app.model.ImageModel; import app.view.MainPanelTile; import app.view.MainPanelTileMediator; import app.view.Preview; import app.view.PreviewMediator; import flash.events.Event; import flash.events.IEventDispatcher; import robotlegs.bender.extensions.contextView.ContextView; import robotlegs.bender.extensions.eventCommandMap.api.IEventCommandMap; import robotlegs.bender.extensions.mediatorMap.api.IMediatorMap; import robotlegs.bender.framework.api.IConfig; import robotlegs.bender.framework.api.IContext; import robotlegs.bender.framework.api.IInjector; public class GalleryConfig implements IConfig { public function GalleryConfig() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- [Inject] public var injector:IInjector; [Inject] public var mediatorMap:IMediatorMap; [Inject] public var commandMap:IEventCommandMap; [Inject] public var contextView:ContextView; [Inject] public var context:IContext; [Inject] public var dispatcher:IEventDispatcher; public function configure():void { injector.map(ImageModel).asSingleton(); commandMap.map(Event.INIT).toCommand(LoadAllCommand); commandMap.map(GalleryEvent.LOAD_COMPLETE).toCommand(CreateMainPanelCommand); commandMap.map(GalleryEvent.IMAGE_CLICK).toCommand(ImageClickCommand); commandMap.map(GalleryEvent.CLOSE_PREVIEW).toCommand(ClosePreviewCommand); mediatorMap.map(MainPanelTile).toMediator(MainPanelTileMediator); mediatorMap.map(Preview).toMediator(PreviewMediator); context.afterInitializing(init); } //-------------------------------------------------------------------------- // // PRIVATE SECTION // //-------------------------------------------------------------------------- private function init():void { dispatcher.dispatchEvent(new Event(Event.INIT)); } } }
После этого происходит магия, и переменная dispatcher становится доступной для использования во всём классе.
Чтобы наши команды, модели и медиаторы работали в проекте, они должны быть "замаплены" следующими товарищами:
[Inject] public var injector:IInjector; [Inject] public var mediatorMap:IMediatorMap; [Inject] public var commandMap:IEventCommandMap;
Команды обычно вызываются событиями, соответственно они связываются в пары:
commandMap.map(Event.INIT).toCommand(LoadAllCommand); commandMap.map(GalleryEvent.LOAD_COMPLETE).toCommand(CreateMainPanelCommand); commandMap.map(GalleryEvent.IMAGE_CLICK).toCommand(ImageClickCommand); commandMap.map(GalleryEvent.CLOSE_PREVIEW).toCommand(ClosePreviewCommand);
mediatorMap.map(MainPanelTile).toMediator(MainPanelTileMediator); mediatorMap.map(Preview).toMediator(PreviewMediator);
Первая команда (LoadAllCommand) запускается по событию Event.INIT, которое вызывается самим конфигуратором по завершению его работы. Она загружает все файлы и кладёт в модель.
Каждая команда выполняется через функцию execute.
LoadAllCommand.as:
package app.controller { import app.dicts.Constants; import app.events.GalleryEvent; import app.model.ImageModel; import flash.display.Loader; import flash.display.LoaderInfo; import flash.events.ErrorEvent; import flash.events.Event; import flash.events.IEventDispatcher; import flash.events.IOErrorEvent; import flash.net.URLRequest; import robotlegs.bender.bundles.mvcs.Command; public class LoadAllCommand extends Command { public function LoadAllCommand() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- [Inject] public var imageModel:ImageModel; [Inject] public var dispatcher:IEventDispatcher; override public function execute():void { _fileList = Constants.FILES_LIST; _total = _fileList.length; var loader:Loader; for (var i:int = 0; i < _total; i++) { loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoadHandler); loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, errorHandler); loader.load(new URLRequest(_fileList[i])); } } //-------------------------------------------------------------------------- // // PRIVATE SECTION // //-------------------------------------------------------------------------- private var _fileList:Vector.<String>; private var _total:uint; private var _loaded:uint = 0; private var _images:Array = []; private function imageLoadHandler(event:Event):void { var info:LoaderInfo = LoaderInfo(event.currentTarget); _images[Constants.FILES_LIST.indexOf(info.url)] = info.content; info.loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, imageLoadHandler); info.loader.contentLoaderInfo.removeEventListener(IOErrorEvent.IO_ERROR, errorHandler); _loaded++; if (_loaded >= _total) { imageModel.addImages(_images); dispatcher.dispatchEvent(new GalleryEvent(GalleryEvent.LOAD_COMPLETE)); } } private function errorHandler(event:ErrorEvent):void { throw new Error("bad link or internet disconnect"); } } }
if (_loaded >= _total) { imageModel.addImages(_images); dispatcher.dispatchEvent(new GalleryEvent(GalleryEvent.LOAD_COMPLETE)); }
CreateMainPanelCommand.as:
package app.controller { import app.model.ImageModel; import app.view.MainPanelTile; import robotlegs.bender.bundles.mvcs.Command; import robotlegs.bender.extensions.contextView.ContextView; public class CreateMainPanelCommand extends Command { public function CreateMainPanelCommand() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- [Inject] public var imageModel:ImageModel; [Inject] public var contextView:ContextView; override public function execute():void { var tilesList:Vector.<MainPanelTile> = new Vector.<MainPanelTile>(); var length:uint = imageModel.totalCount; for (var i:int = 0; i < length; i++) { tilesList[i] = new MainPanelTile(imageModel.cloneImage(i), i); tilesList[i].x = 10 + ((tilesList.length - 1) % COLUMN) * WIDTH; tilesList[i].y = 10 + int((tilesList.length - 1) / COLUMN) * HEIGHT; contextView.view.addChild(tilesList[i]); } } //-------------------------------------------------------------------------- // // PRIVATE SECTION // //-------------------------------------------------------------------------- private static const COLUMN:uint = 4; private static const WIDTH:uint = 200; private static const HEIGHT:uint = 160; } }
MainPanelTile.as:
package app.view { import flash.display.Bitmap; import flash.display.Sprite; public class MainPanelTile extends Sprite { public function MainPanelTile(image:Bitmap, index:int) { mouseChildren = false; buttonMode = true; _index = index; _template = new TileTemplate(); addChild(_template); var width:int = _template.maskMc.width; var height:int = _template.maskMc.height; var diff:Number = Math.max(_template.maskMc.width / image.width, _template.maskMc.height / image.height); image.width *= diff; image.height *= diff; image.x = - image.width / 2; image.y = - image.height / 2; image.smoothing = true; _template.iconSlot.addChild(image); _template.iconSlot.mask = _template.maskMc; } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- public function get index():int { return _index; } public function destroy():void { removeChild(_template); _template = null; } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- private var _template:TileTemplate; private var _index:int; } }
MainPanelTileMediator.as:
package app.view { import app.events.GalleryEvent; import app.model.ImageModel; import flash.events.MouseEvent; import robotlegs.bender.bundles.mvcs.Mediator; public class MainPanelTileMediator extends Mediator { public function MainPanelTileMediator() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- [Inject] public var view:MainPanelTile; [Inject] public var imageModel:ImageModel; override public function initialize():void { eventMap.mapListener(view, MouseEvent.CLICK, clickHandler); } //-------------------------------------------------------------------------- // // EVENT HANDLERS // //-------------------------------------------------------------------------- private function clickHandler(event:MouseEvent):void { imageModel.indexClick = view.index; eventDispatcher.dispatchEvent(new GalleryEvent(GalleryEvent.IMAGE_CLICK)); } } }
Обработчик clickHandler пишет в модель индекс нажатого тайла и генерирует следующее событие - GalleryEvent.IMAGE_CLICK, которое открывает превью изображения через команду ImageClickCommand.
ImageClickCommand.as:
package app.controller { import app.model.ImageModel; import app.view.Preview; import flash.display.Bitmap; import robotlegs.bender.bundles.mvcs.Command; import robotlegs.bender.extensions.contextView.ContextView; public class ImageClickCommand extends Command { public function ImageClickCommand() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- [Inject] public var imageModel:ImageModel; [Inject] public var contextView:ContextView; override public function execute():void { var bitmap:Bitmap = imageModel.getImage(); var preview:Preview; if (!imageModel.preview) preview = new Preview(); else preview = imageModel.preview; preview.update(bitmap, imageModel.indexClick, imageModel.totalCount, contextView.view.stage); if (!imageModel.preview) { contextView.view.addChild(preview); imageModel.preview = preview; } } } }
Как и MainPanel, Preview состоит из 2 классов - собственно вью и медиатора.
Preview должен обрабатывать клики на 3 элемента - 2 кнопки и фон. Чтобы медиатор мог это сделать, даю к ним доступ через аксессоры.
Preview.as:
package app.view { import flash.display.Bitmap; import flash.display.Sprite; import flash.display.Stage; public class Preview extends Sprite { public function Preview() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- public function update(bitmap:Bitmap, index:int, totalCount:int, stage:Stage):void { if (_image && _image.parent) _image.parent.removeChild(_image); _image = null; _image = bitmap; _index = index; if (!_template) { _template = new Sprite(); _template.graphics.beginFill(0x000000, 0.6); _template.graphics.drawRect(0, 0, stage.stageWidth, stage.stageHeight); _template.graphics.endFill(); addChild(_template); _btnBack = new BtnBackTemplate(); _btnBack.buttonMode = true; _btnBack.x = 0; _btnBack.y = (stage.stageHeight - _btnBack.height) / 2; addChild(_btnBack); _btnNext = new BtnNextTemplate(); _btnNext.buttonMode = true; _btnNext.x = stage.stageWidth - _btnNext.width; _btnNext.y = (stage.stageHeight - _btnNext.height) / 2; addChild(_btnNext); } _btnBack.alpha = _index > 0 ? 1 : 0.4; _btnNext.alpha = _index + 1 < totalCount ? 1 : 0.4; var diff:Number = Math.min(stage.stageWidth / _image.width, stage.stageHeight / _image.height); _image.width *= diff; _image.height *= diff; _image.smoothing = true; _image.x = (stage.stageWidth - _image.width) / 2; _image.y = (stage.stageHeight - _image.height) / 2; _template.addChild(_image); } public function get template():Sprite { return _template; } public function get btnBack():Sprite { return _btnBack; } public function get btnNext():Sprite { return _btnNext; } public function destroy():void { if (_btnBack) { _btnBack.parent.removeChild(_btnBack); _btnBack = null; } if (_btnNext) { _btnNext.parent.removeChild(_btnNext); _btnNext = null; } if (_image && _image.parent) _image.parent.removeChild(_image); _image = null; if (_template) { _template.parent.removeChild(_template); _template = null; } } //-------------------------------------------------------------------------- // // PRIVATE SECTION // //-------------------------------------------------------------------------- private var _image:Bitmap; private var _template:Sprite; private var _btnBack:BtnBackTemplate; private var _btnNext:BtnNextTemplate; private var _index:int; } }
PreviewMediator.as:
package app.view { import app.events.GalleryEvent; import app.model.ImageModel; import flash.events.MouseEvent; import robotlegs.bender.bundles.mvcs.Mediator; public class PreviewMediator extends Mediator { public function PreviewMediator() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- [Inject] public var view:Preview; [Inject] public var imageModel:ImageModel; override public function initialize():void { eventMap.mapListener(view.template, MouseEvent.CLICK, clickHandler); eventMap.mapListener(view.btnBack, MouseEvent.CLICK, backClickHandler); eventMap.mapListener(view.btnNext, MouseEvent.CLICK, nextClickHandler); } override public function destroy():void { view.destroy(); view = null; } //-------------------------------------------------------------------------- // // EVENT HANDLERS // //-------------------------------------------------------------------------- private function clickHandler(event:MouseEvent):void { eventDispatcher.dispatchEvent(new GalleryEvent(GalleryEvent.CLOSE_PREVIEW)); } private function backClickHandler(event:MouseEvent):void { if (event.currentTarget.alpha < 1) return; imageModel.indexClick--; eventDispatcher.dispatchEvent(new GalleryEvent(GalleryEvent.IMAGE_CLICK)); } private function nextClickHandler(event:MouseEvent):void { if (event.currentTarget.alpha < 1) return; imageModel.indexClick++; eventDispatcher.dispatchEvent(new GalleryEvent(GalleryEvent.IMAGE_CLICK)); } } }
ClosePreviewCommand.as:
package app.controller { import app.model.ImageModel; import robotlegs.bender.bundles.mvcs.Command; import robotlegs.bender.extensions.contextView.ContextView; public class ClosePreviewCommand extends Command { public function ClosePreviewCommand() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- [Inject] public var imageModel:ImageModel; [Inject] public var contextView:ContextView; override public function execute():void { contextView.view.removeChild(imageModel.preview); imageModel.preview = null; } } }
- позволяет записать загруженные битмапы изображений;
- отдаёт по индексу битмапу или её клон (поскольку битмапы могут выводиться на экран несколько раз);
- принимает и отдаёт индекс последнего нажатого тайла;
- хранит спрайт Preview, доступ к которому необходим из разных классов. Возможно, это неправильно, но не придумал, где ещё его хранить.
ImageModel.as:
package app.model { import app.view.Preview; import flash.display.Bitmap; import flash.display.BitmapData; public class ImageModel { public function ImageModel() { } //-------------------------------------------------------------------------- // // PUBLIC SECTION // //-------------------------------------------------------------------------- public function addImages(value:Array):void { if (!_images) _images = new Vector.<Bitmap>(); for (var i:int = 0; i < value.length; i++) { _images.push(value[i]); } } public function get indexClick():int { return _indexClick; } public function set indexClick(value:int):void { _indexClick = value; } public function cloneImage(i:int):Bitmap { var data:BitmapData = new BitmapData(_images[i].width, _images[i].height, false); data.draw(_images[i]); var cloneData:BitmapData = data.clone(); var cloneBitmap:Bitmap = new Bitmap(cloneData); return cloneBitmap; } public function getImage():Bitmap { return _images[_indexClick]; } public function get totalCount():int { return _images.length; } public function get preview():Preview { return _preview; } public function set preview(value:Preview):void { _preview = value; } //-------------------------------------------------------------------------- // // PRIVATE SECTION // //-------------------------------------------------------------------------- private var _images:Vector.<Bitmap>; private var _indexClick:int; private var _preview:Preview; } }
- библиотечный Context создаётся в пользовательском (ранее пользовательский Context наследовался от библиотечного);
- ранее все классы мапились в моём Context, теперь для этого есть Config, добавляющийся через функцию context.configure(). Синтаксис поменялся до неузнаваемости;
- ранее первая команда запускалась на библиотечное событие ContextEvent.STARTUP_COMPLETE, теперь событие нужно родить самому;
- ранее модель наследовалась от класса Actor, который тупо исчез;
- ранее contextView и eventDispatcher уже были доступны в командах, моделях и медиаторах, теперь их нужно подключать через [Inject].
Часть 3. Плюсы и минусы.
Плюсы по сравнению с pureMVC:
+ намного проще разобраться как применить либу вцелом (моё субъективное мнение);
+ намного проще доступ к stage и глобальным данным вообще;
+ общение через кошерные человеческие ивенты, а не странные обсерверы.
Минусы вообще:
- огромная разница между версиями либы, что может вылезти боком тому, кто захочет обновить проект;
- каждый медиатор состоит из 2 классов.
А выводы пусть каждый делает сам
Всего комментариев 15
Комментарии
22.07.2014 16:18 | |
Сигналов не хватает.
|
22.07.2014 16:34 | |
22.07.2014 17:33 | |
Начал читать сразу резануло глаза
Цитата:
После этого происходит магия, и переменная dispatcher становится доступной для использования во всём классе.
|
22.07.2014 17:35 | |
Кто отдаёт предпочтение командам вместо более толстых контроллеров-сервисов (которые живут подольше команд) – черкните пару строк, почему?
|
22.07.2014 18:10 | |
А может кто-нибудь объяснить, для каких целей предназначен RL2? Что на нем делать нельзя?
И расскажите пожалуйста, что происходит за кулисами, когда я обращаюсь из объекта к инжектируемому свойству ( в данном примере это модель ) ? |
|
Обновил(-а) LifeIsRhythm 22.07.2014 в 18:52
|
22.07.2014 23:32 | |
Я не навязываю свою точку зрения, отнюдь. Пишите как вам удобнее. Вам же с этим жить, а не мне.
|
23.07.2014 11:45 | |
dimarik, прочитал с утра на свежую голову - дошло.
|
02.09.2014 04:23 | |
Кстати, не повторяйте частую ошибку
http://knowledge.robotlegs.org/discu...in-a-wrong-way |
04.09.2014 20:30 | |
Цитата:
Кстати, не повторяйте частую ошибку
|
08.09.2014 17:18 | |
fljot
у меня туговато с английским... можно в двух словах, о чём там? |
Последние записи от Rembrant
- ООП ради ООП ч. 2. Мучаем robotlegs (21.07.2014)
- Программа на pureMVC. Оно или нет? (30.06.2014)