Инструкция, как последовательность методов

в 9:00, , рубрики: Action Script, говнокод от юниора, упорядочивание кода, метки: ,

Привет!

Пишу впервые, немного нервничаю, поэтому уже в четвертый раз набираю первое предложение.
Я занимаюсь разработкой игр. Преимущественно на флеше. Больше года работал с HaXe NME — круто. Но много своих «выкрутасов», которые, если сравнивать с AS3 при таргете во флеш, можно уместить в отдельную статью.
За более чем два года опыта выделил одну насущную для меня проблему при создании флеш игр.
Это первичная инициализация. Проблема заключается в том, что пока пользователь смотрит на прогресс-бар прелоадера, в бэкграунде выполняется множество операций — от загрузки данных и ассетов до их подготовки. И, зачастую, количество этих операций зашкаливает даже в самом начале разработки, не говоря уже об обновлениях.

Очень много смотрел чужие исходники игр и видел ужасные нагромождения конструкций вида:

/**
 * calling server (pseudo-code)
 */
private function startInitialization():void {
	ServerConnection.instance.call('method', params, onCallMethodComplete);
}

private function onCallMethodComplete():void {
	ServerConnection.instance.call('methodTwo', params, onCallMethodTwoComplete);
}

private function onCallMethodTwoComplete():void {
	AssetsLoader.loadAssetZipByName('assetName', onAssetLoaded);
}

private function onAssetLoaded():void {
	// etc
}

При этом, не всегда пользуются какими-то вспомогательными синглтон-классами в методах, и зачастую в методах класса загрузки появляется куча loader-ов для каждой операции со всеми вытекающими слушателями. Мешанина не читаемая абсолютно. Сам так же делал когда-то.
На вкус и цвет, как говорится, но когда количество методов для инициализации вырастает хотя бы до десятка — читать такой код становится трудно. А если впоследствии внезапно понадобится вставить дополнительную промежуточную функцию, скажем, для дополнительной обработки полученных данных, — это превращается в головную боль и уйму времени, потраченного на попытки разобраться что за чем идёт.
Такая проблема актуальна не только для загрузки данных. А так же, например, последовательной анимации элементов интерфейса. Кто-то возразит, что анимировать можно и во Flash IDE — спору нет. Однако, зачастую при анимации появления окна требуется не только показывать элементы по порядку, но ещё и активировать/деактивировать их, чтобы не возникало случайных закрытий окна по пользовательскому клику до завершения анимаций.
Задача вроде бы понятная и достаточно тривиальная. Вижу цель. Иду к цели.
Утилитка, которую я написал, — весьма небольшая и вполне себе простая. Однако, она очень помогла мне перейти от конструкций, приведенных выше, к вполне лаконичному:

private function init():void {
	Instruction.create()
	.add(configureViewTree)
	.add(initializeManagers)
	.add(SocialData.instance.init, flashVars)
	.add(GameData.instance.init, 'http://server.ru/', 'http://static.server.ru/assets/')
	.add(Assets.getZip, 'music/music.zip')
	.add(MetaData.instance.init)
	.add(World.instance.create, worldContainer)
	.execute(startGame);
}

Все методы вызовутся в той же последовательности, в которой они добавлены.
Последовательность действий плюс относительно удобная читаемость очереди. Казалось бы, что ещё нужно?!

Полный код класса Instruction
package {	
	/**
	 * ...
	 * @author Frost
	 */
	public class Instruction {
		
		public static function create():Instruction {
			return new Instruction();
		}
		
		private var functionsQueue:Vector.<Function>;
		private var argumentsQueue:Vector.<Array>;
		
		public function Instruction() {
			functionsQueue = new Vector.<Function>();
			argumentsQueue = new Vector.<Array>();
		}
		
		public function add(calledFunction:Function, ...params):Instruction {
			functionsQueue.push(calledFunction);
			argumentsQueue.push(params);
			return this;
		}
		
		public function execute(completeHandler:Function = null, ...params):void {
			add(completeHandler, params).checkQueue();
		}
		
		private function doTask():void {
			var calledFunctionArguments:Array = argumentsQueue.shift();
			calledFunctionArguments.unshift(checkQueue);
			functionsQueue.shift().apply(null, calledFunctionArguments);
			calledFunctionArguments = null;
		}
		
		private function checkQueue():void {
			if (functionsQueue.length > 1) {
				doTask();
				return;
			}
			completeInstruction();
		}
		
		private function completeInstruction():void {
			if (functionsQueue[0] != null) {
				functionsQueue.shift().apply(argumentsQueue.shift());
			}
			functionsQueue = null;
			argumentsQueue = null;
		}
	}
}

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

function weWantToAddToInstruction(completeHandler:Function, paramOne:Object, paramTwo:String, paramThree:Function, ...params):void {
	// do some crazy stuff
	completeHandler();
}

Так же, к минусу можно отнести и то, что обработка ошибок в выполняемой инструкции лежит на плечах выполняемых методов. Я пока не придумал решение, которое позволит вынести отлов ошибок под ответственность самой инструкции. В вариантах, что я пробовал — приходилось сильно менять структуру функций => не удобно. Да и во многих ситуациях всё заходило в тупик. Так-то было бы здорово.

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

Положительным аспектом стало то, что в инструкцию можно запихивать методы, аргументы для которых — абсолютно произвольные, как по содержанию, так и по количеству. Это весьма удобно. Но лишь в том случае, если твёрдо уверен, что аргументы — именно те, которые задуманы. Хотя, это правило применимо ко всему, наверное.

В общем-то это всё. Благодарю за внимание, читатели.
Очень здорово, если то, что я написал, будет кому-то полезно.

Автор: Frost47rus

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js