Conveyor для AS3/Flex
Новый язык, новые возможности новый конвейер. Предыдущая статья о конвейере ставила цель убедить читателя в необходимости использования конвейера при разработке flash-приложений. Конвейер предлагает концепцию простого и понятного workflow, «заточенного под flash», как средство отказаться от практики использования кадров, тиков фрэймов (onEnterFrame), а иногда даже от использования событийной модели. Конвейер был предоставлен общественности после нескольких лет его использования и развития, а три года после его публикации и написания статьи показало большую популярность конвейера среди flash-разработчиков, и есть масса попыток сделать, «что-то подобное, но не так» - свое и это хорошо. Важно, что идея нужна и делает разработку приложений на flash проще.
Сейчас новое время для flash, пришел flex framework, MXML и Action Script 3. Конечно же, нужно переделывать и конвейер. А нужно ли? Нужен ли конвейер в AS3 и во Flex? В проектах классических – fla+as конечно же нужен. Здесь ничего не изменилось. Нужен ли он во flex? Во flex есть мощный механизм состояний (states), замечательная событийная модель. Могут ли они заменить собой конвейер? Я специально, в целях «погружения в среду» сделал три небольших проекта без использования конвейера, чтобы убедиться в обратном - конвейер во flex просто необходим. States – отличное средство для создания переходов между состояниями интерфейса. Однако механизм States не совсем подходит для описания состояний логики приложения и управления ими. Декларативное описание эффектов в MXML – удобное средство, но когда нужно выстроить эффекты в цепочки динамически, выстраивая, таким образом, логику выполнения эффектов и работу приложения – начинается привычная мешанина и ненужная сложность. Особенно это становится очевидным, если знать, как можно было сделать все это красиво и просто с конвейером.
Конечно же, конвейер нужен, но он нуждается и в обновлении, помочь использовать новые возможности языка и фрэймворка в полную силу.
Что такое конвейер?
Конвейер – это обычная очередь (queue). Очередь исполнения. И это очень простая в использовании очередь. Команда - это ссылка на метод или анонимную функции (closure). Метод добавления команды имеет свой «синтаксис», можно вызвать метод, а можно поместить анонимную функцию, задержку можно создать в секундах и что очень важно во фреймах. Команда еще не выполнилась, а имеется ссылка на значение, которое метод может вернуть, эту ссылку вы можете передать другой команде конвейера.
Работа конвейера похожа на обычное исполнение кода. Как сказано в предыдущей статье о конвейере – это способ во flash описывает работу приложения во времени. Каждый элемент очереди – это массив параметров – исполняемый код (метод) и аргументы либо команда изменения параметра объекта. Команда возвращает значение. Команда очереди конвейера может иметь поведение call stack, когда одна команда создает другие команды конвейера и выполняются в той же точке исполнения.
Flash – это презентация данных для пользователей. Поэтому в конвейеры есть встроенные механизмы для работы с анимацией.
Конвейер простой способ сначала разделить приложение на части – на компоненты, а потом «собрать» их в одно целое. Например, разработчик создает компоненты, реализацию работы которых используется собственный конвейер компонента. При работе приложения компоненты связываются в общую очередь исполнения передачей общего для компонентов приложения конвейера – компоненты получают общую «цепную передачу» и без, часто громоздкого, использования событийной модели, компоненты работают сообща.
О формулировке. Больше мне нравится формулировка – способ описывать поведение flash/flex приложения во времени. Если попытаться сформулировать более конкретно, я бы сказал, что конвейер – это иерархическая очередь для создания workflow во flash/flex. Размыто и не совсем понятно, но во всяком хоть какая-то формулировка. Важно, что он прост в использовании и экономит массу времени. Кто пробовал – знает.
Что нового?
Во-первых, нет больше метода «put» :) Вернее он поменял название. Я привел имена методов к общему виду. Изначально напрашивалось, что метод добавления команды в очередь должен называться «add». В старом action script это ключевое слово было занято устаревшим оператором сложения. Потому пришлось искать что-то другое. К тому же, в AS3 нельзя использовать include как название метода. Напрашивались перемены.
Теперь методы добавления в очередь выглядят единообразно: add(), addInclude(), addEffect(), addPause(). Да я знаю, что у многих conv.put() вошел в подсознание через пальцы (как и у меня), но что поделать, нужно бороться со стереотипами :)
Далее. Конвейер подружился с эффектами flex framework. Теперь можно декларативно описать твиннинги, звуки и так далее в MXML. И поместить их в очередь. Причем метод addEffect() поместит в конвейер эффект так, что остановив конвейер остановится и эффект, и при последующем старте конвейера эффект доиграет и следующая команда выполнятся по окончанию времени которое вы задали на проигрывание эффекта.
Например:
var m:Move = new Move(mySprite);
m.xBy = 20;
m.yBy = 30;
m.duration = 500;
// или
// <mx:Move id=”m” target=”{mySprite}” xBy=”20” yBy=”30” duration=”500” />
conv.addEffect(m); |
И все. Магия начинается, когда добавляются более сложные эффекты созданные через Sequence или Parallel. Или же звуки (SoundEffect). Если добавить лирики, то конвейер не ревнивый сноб, он не считает, что все нужно делать с помощью него, и анимация во flex отстой, хорош только он, он лишь помогает делать все проще и удобнее ;)
Далее. Концепция возвращения значения командой, была предложена в рассылке «ruFlash». Кто-то сказал, извините, не помню кто, что было бы замечательно иметь возможность связывать команды конвейера через возвращаемые значения. Этот функционал был добавлен еще в старую версию, но документируется только сейчас.
Например (уже новый синтаксис):
var conv:Conveyor = Conv.create(true);
conv.addPause(1000);
var o:* = conv.add(function(){
return “time = “ + getTime();
}, 0);
conv.add(trace, 0, o); |
На консоли выведется что-то вроде «time = 1234».
То есть, третья команда, где выводится текст на консоль, хранит ссылку на объект, которого еще «нет в природе». Он будет получен через одну секунду. Просто фокус какой-то. Но все просто. Конвейер при создании команды возвращает заглушку, которая может быть принята как аргумент команды. Эту заглушку нужно передавать другой команде конвейера. Заглушка при выполнении метода, запоминает возвращаемое значение, а когда обрабатываются аргументы команды (где trace) из заглушки данные достает сам конвейер и помещает уже реальные данные как аргумент функции.
Далее. Метод «anim» накачал мускулы. Теперь можно одной строкой сказать, что нужно объект переместить в этом вот место, плавно увеличивая его ширину и высоту, а еще бы хотелось и альфу анимационно изменить.
Одна строка:
conv.anim(myCanvas, {x:200, y:300, width:200, height:200, alpha:0.5}, 500); |
Мне кажется – красота. Вторым параметром можно указать не объект, а другой спрайт, но в этом случае будет анимация только движения. Но мощь появляется, когда вы указываете вторым параметром именно объект с полями и значениями, которые нужно анимировать. Причем вы можете анимировать и стили. Конвейер автоматически определить поле ли это или же стиль компонента. Одно ограничение, значение должно быть числом. Анимация добавляется как набор команд конвейера в конец очереди со всеми вытекающими удобствами.
Идем дальше. Новая возможность AS3, когда метод «знает» свой объект, позволила отказаться от передачи имени метода в виде строки.
Теперь можно делать так:
var a:Array = new Array();
conv.add(a.push, “1”, “first element”);
conv.add(trace, 0, a);
conv.add(a.push, “1”, “second element”);
conv.add(trace, 0, a);
А не как раньше – conv.put(a, “push”, “1”, “new element”); |
Aвтокомплит методов и проверка при компиляции – замечательное средство борьбы с ошибками. Старый синтаксис также остался, может, кому он и пригодится.
Плюс к этому, как и раньше вы можете и указать «чужое пространство», чтобы сделать метода родным для объекта на время.
conv.add(myObject.method, targetObject, 0,); |
Далее. Как я уже поминал выше, добавился функционал для работы с множеством конвейеров и организации workflow. Например, нужно подождать пока выполнятся все команды списка конвейеров, и только после этого выполнять команды «основного» конвейера.
var rootConv: Conveyor = Conveyor.create(true);
var c1:Conveyor = Conveyor.create(true);
c1.addPause(20);
c1.add(trace, “1”, “hello, I’m c1”);
var c2:Conveyor = Conveyor.create(true);
c2.addPause(120);
c2.add(trace, “1”, “hello, I’m c2”);
var c3:Conveyor = Conveyor.create(true);
c3.addPause(220);
c3.add(trace, “1”, “hello, I’m c3”);
rootConv.addFlow(c1, c3, c2);
rootConv.add(function():void
{
c1.close();
c2.close();
c3.close();
trace(“flow execution complete”);
}, 0); |
Как видно из примера, главный конвейер выполнит команду закрытия трех конвейеров только если они выполнят команды. Если нужно сделать «цепочку» выполнения, создать очередь, используется метод addQueue().
Например:
Очередь и параллельное исполнение относится к advanced практике, и лично я буду пользоваться ими не часто, но коль мы взрослые то нужно соответствовать.
Что не появилось
Уже не помню сколько раз мне предлагали добавить в механизм конвейера очень нужный, по мнению предлагавших, метод addFirst(). То есть, метод, который добавляет команду не в конец очереди, а в его начало. Типа «это очень полезно и я добавил в код конвейера этот метод и был счастлив». Так вот, я его не добавил. Почему? Потому что, такой метод не нужен. Отвечу на такую просьбу так – «вы просто не поняли».
Почему, нужен такой метод? Такой метод был бы нужен, если бы не было команды addInclude (include в старом синтаксисе). Представим ситуацию, человек не знает о существовании инклудов, и видит конвейер как обычную плоскую очередь, список команд. Ага, я добавил с десяток команд, и вот конвейер выполняет команду. Условия в приложении изменились и нужно добавить десяток команд конвейера, и выполнить их именно после текущей команды. А выполнить сейчас эти новые команды я не могу, ведь я уже забил конвейер новыми командами, и добавив их в хвост конвейера мне нужно будет ждать выполнения текущих команд. Что нужно? Нужен метод, который позволит добавлять в начало списка. Тогда я создам список команд и помещу их в начало очереди. Конвейер начнет обрабатывать команды в начале списка и вот удача – мои новые команды выполнятся именно сейчас. Ну очень нужный функционал.
Однако, инклуд как раз для таких случает и создан, инклуд это команда, которая на момент ее выполнения переводит конвейер в режим «добавляю в тоже место - то есть первую добавленную в начало, вторую, если она будет, на второе место, и так далее». Это и называется иерархическим списком.
Вот пример:
conv.add(trace, 0, “начинаю работать”);
conv.add(trace, 0, “гадаю на кофейной гуще в следующей команде”)
conv.addInclude(function():void
{
conv.add(trace, 0, “поглядим четное ли время”);
If(getTimer()%2 == 0)
{
conv.add(trace, 0, “время четное, я встал с правильной ноги”);
}else{
conv.add(trace, 0, “время не четное, нужно быть осторожнее”);
conv.add(trace, 0, “хотя, попробую еще раз);
conv.addInclude(function():void
{
conv.add(“поглядим еще раз четное ли время”);
if(getTimer()%2 == 0)
{
conv.add(trace, 0, “время четное”);
conv.add(trace, 0, “все в полном порядке”);
}else{
conv.add(trace, 0, “что-то со мной не так”);
}
}, 0);
}
}, 0);
conv.add(trace, 0, “завершение сеанса гадания”);
|
С вероятностью один к четырем в консоли выведется текст:
начинаю работать
гадаю на кофейной гуще в следующей команде
поглядим четное ли время
время не четное, нужно быть осторожнее
хотя, попробую еще раз
поглядим еще раз четное ли время
время четное
все в полном порядке
завершение сеанса гадания
Посмотрите на код, и поглядите, как он выполнятся во времени. Теперь посмотрите, как выводится текст на консоль. Изначально в конвейер помещается только три команды. Но команды, которые помещаются позже в инклуде, «ведут» себя так, как будто их поместили в конвейер в первый момент времени. И последовательность вывода на консоль повторяет последовательность команд в коде. Вы должны научиться думать инклудами – «я поместил инклуд, будет вызван функция, которая поместит команды в эту же точку, а можно поместить другие инклуды, которые поместят команды потом, между новыми командами». Логика похожа на вызов метода методом и выполнение обычного кода. С помощью инклудов можно даже реализовывать поведение сродни рекурсии, но распределенную во времени и во фреймах.
Так зачем тогда нужен метод addFirst(), то есть метод, заставляющий программиста опускаться ниже в абстракции, задумываться, куда точно нужно поместить команды, если конвейер уже имеет встроенные механизмы, позволяющий просто не думать об этом?
Конвейер не был бы конвейером без инклудов. Это его ядреная бомба. Без инклудов он был бы обычным списком команд.
Обратите на инклуды внимание, поймите их, и вы получите совсем другой уровень комфорта при описании работы вашего приложения во времени.
На что нужно обратить внимание
AS3 – более строгий язык чем старый добрый AS. Возможно не раз вы получите ArgumentsError при выполнении вашей команды, так как вы не правильно передали список аргументов для вашего метода. Конвейер, конечно, мог бы перехватить это исключение, но не я не стал так делать, это могло бы стать постоянным источником ошибок.
Далее. Garbage Collector ну просто зверь. Удаляет из памяти работающие анонимные конвейеры с большой охотой.
Например, вы создаете временный конвейер в переменной.
var c:Conveyor = Conveyor.create();
c.addPause(1000);
c.add(trace, 0, “GC – lamer”); |
с большой долей вероятности сообщение «GC – lamer» вы не увидите.
Для решения этой проблемы, нужно создавать ссылку на конвейер как свойство класса, или же, указать методу create сохранить ссылку на конвейер:
var c:Conveyor = Conveyor.create(true);// true указывает что ссылка нужна |
c.addPause(1000);
c.add(trace, 0, “GC – best friend”);
c.add(c.close, 0); // конвейер закрыт и отдан на растерзание GC |
В этом случае вы получите и сообщение на консоли, и конвейер будет корректно закрыт и удален из памяти после того, как он выполнит свою работу. Если вы использовать конвейер как свойство класса, то указывать параметр не нужно.
private var lConv:Conveyor = Conveyor.create(); |
В этом случае конвейер будет удален из памяти только при удалении экземпляра вашего класса.
Заключение
Конвейер можно скачать здесь. Исходный код доступен.
|