Опубликован: 23.12.2005 | Уровень: специалист | Доступ: платный | ВУЗ: Московский физико-технический институт
Лекция 7:

Наследование во Flash MX

Приватные поля и наследование

Перегенерация функций для доступа к частным полям

Вот попытка использовать все-таки странное содержимое этого параграфа. В прошлой лекции мы рассказывали о том, как эмулировать во Флэше приватные поля. Если вы пишете класс с нуля, - вы без всяких проблем сможете использовать этот метод. Но предположим, что вам достался уже готовый класс, а вы хотите сделать его наследника более защищенным. Сможете ли вы это сделать? В голову приходит идея пройтись по прототипу и перегененировать функции, которые мы там найдем, таким образом, чтобы потом вызывать исходные функции при помощи apply и передавать им в качестве объекта, с которым они работают, контекст вызова. (Попробуйте написать код, который делает это. А затем посмотрите, какой есть еще способ решить эту задачу - он описан в следующем подпараграфе.)

Включение в цепочку __proto__ контекста вызова конструктора

Перегенерация функций - это интересная идея. Но как быть, если вы хотите сделать приватными всего одно-два особенно важных поля, и обращаются к этим полям опять-таки один-два метода? Перегенерация всех методов класса будет в таком случае явно избыточной. С другой стороны, как обеспечить этим методам (в процессе перегенерации) доступ к остальному содержимому класса? Ведь вызывать мы будем эти методы при помощи apply, передавая им контекст вызова конструктора. Но на этот раз мы не собираемся в контекст конструктора копировать все нужные нам ссылки. Выход из этого положения как раз может подсказать найденный нами недавно фокус с заданием ссылки __proto__ для контекста вызова! Установим __proto__ контекста вызова ссылающимся на наш объект. Тогда все значения переменных из объекта будут доступны для рассматриваемой функции. Правда - и здесь нужно быть аккуратным - эта функция не сможет их изменить (точнее, измененные значения останутся в контексте вызова конструкт ора, а не в самом объекте). Так что если менять значение публичного поля все-таки надо, следует или воспользоваться функцией- setter 'ом этого поля (если нужно, то создать его), или же сделать это поле приватным и перегенерировать еще и те функции, которые к нему обращаются. Первый способ, конечно, предпочтительнее.

Сейчас мы приведем пример использования предлагаемого метода. Воспользуемся уже знакомым нам замечательным лифтом. Предположим, мы добавили возможность внешнему контроллеру управлять этим лифтом. Контроллер сможет открывать и закрывать двери, а также останавливать лифт, если надо. Условимся, что изменение соответствующей переменной будет означать изменение состояния лифта. То есть, если мы установили переменную doorsAreOpen в true, то это означает, что двери открылись. Посмотрите на новый вариант кода лифта:

// ================ Базовый класс лифта - модифицирован =================
	// Базовый конструктор - добавлено поле moving
_global.lift = function (minFloor, maxFloor){
	// Проверяем, не перепутал ли пользователь максимальный и 
	// минимальный этажи
	if (minFloor < maxFloor){
		this.minFloor = Math.round(minFloor);
		this.maxFloor = Math.round(maxFloor);
	}
	else{  // Если перепутал, корректируем
		this.minFloor = Math.round(maxFloor);
		this.maxFloor = Math.round(minFloor);
	}
	   // Устанавливаем лифт на 1 этаж
		 // (но если лифт туда не должен ездить, корректируем 
		 // начальный этаж в нужную сторону).
	this.currentFloor = Math.min(Math.max(1, this.minFloor), 
		this.maxFloor);
	this.doorsAreOpen = true;
		// Новое поле	
	this.moving = false;
}
	// Функция goto - без изменений
_global.lift.prototype.goto = function(where){
	trace("-----------");
		// Oкругляем и ограничиваем этаж назначения
	this.floorToGo = Math.min(Math.max(Math.round(where), this.minFloor), this.maxFloor);
	if (this.floorToGo != this.currentFloor) this.go();  
// Поехали!
	else trace("Ничего не делаем." );
	trace("-----------");
}
	// Уделяем повышенное внимание безопасности
_global.lift.prototype.setDoorsOpen = function(open){
		// Если мы едем, двери трогать нельзя
	if (this.moving) return;
	if (this.doorsAreOpen != open){
		trace("Осторожно!");
		trace(open ? "Открываем двери." : "Закрываем двери.");	
	}
	this.doorsAreOpen = open;
}
	// Добавлена регистрация состояний движения/неподвижности,
	// а также добавлено взаимодействие с внешним контроллером
_global.lift.prototype.go = function(){
		// Осторожно, двери закрываются
	this.setDoorsOpen(false);
	this.moving = true;
	
		// Поехали!
	var distance = this.floorToGo - this.currentFloor;
	var signOfDistance = (distance >= 0 ? 1 : -1);
	for(  // Это работает независимо от того, едем вверх или вниз
		var i = this.currentFloor; 
		signOfDistance*i <= signOfDistance*this.floorToGo; 
		i += signOfDistance
	){
			// Едем и выводим об этом сообщение
		trace("Этаж " + i);
		this.currentFloor = i;
			// Проехали один этаж, сообщаем об этом контроллеру
		externalController.work(this);
			// А вдруг контроллер остановил лифт?
		if (!this.moving) break;
	}
		// Приехали
	this.moving = false;
	this.setDoorsOpen(true);
}

Сразу видно, что с точки зрения безопасности движения у нас возникают проблемы. Контроллер может открыть двери в обход функции setDoorsOpen (которая теперь проверяет, движется ли лифт). Вот код контроллера со всевозможными вариантами потенциально опасных действий:

//================ Контроллер лифта ===========================

externalController = {};
externalController.work = function(liftToControl){
	this.liftToControl = liftToControl;
	if (liftToControl.currentFloor == 3) this.doEvilThing();	
}
externalController.openLiftDoors1 = function(){
	this.liftToControl.doorsAreOpen = true;
}
externalController.openLiftDoors2 = function(){
	this.liftToControl.setDoorsOpen(true);
}
externalController.openLiftDoors3 = function(){
	this.liftToControl.moving = false;
	this.liftToControl.setDoorsOpen(true);
}
externalController.doEvilThing = externalController.openLiftDoors1;

Давайте теперь напишем класс safeLift, который будет наследником класса lift, избавленным от этих проблем.

//================== Безопасный лифт =================================

_global.safeLift = function(minFloor, maxFloor){
	super(minFloor, maxFloor);

	var doorsAreOpen = true;
	var getActivationObject = function(){
		return this;	
	}
	var activationObject = getActivationObject();
	activationObject.__proto__ = this;
	// Или var __proto__ = this;
	var setDoorsOpen = function(){
		return this.__proto__.setDoorsOpen.apply(activationObject, 
			arguments);
	}
		// Переопределяем старую функцию
	this.setDoorsOpen = setDoorsOpen;
}
_global.safeLift.prototype = new _global.lift(0, 1);

Тестировочный код будет у нас выглядеть следующим образом:

//=========================== Тестовая часть =============================

l1 = new lift(1, 5);
l2 = new safeLift(1, 5);
  
  // Тестируем 
trace("l1:");
l1.goto(10);
trace("l2:")
l2.goto(10);

externalController.doEvilThing = externalController.openLiftDoors2;

trace("l1:");
l1.goto(0);
trace("l2:")
l2.goto(0);

externalController.doEvilThing = externalController.openLiftDoors3;

trace("l1:");
l1.goto(10);
trace("l2:")
l2.goto(10);

Поместив в кадр _level0 все вышеприведенные кусочки кода и запустив эту программку, мы получим:

l1:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Этаж 4
Этаж 5
-----------
l2:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Этаж 4
Этаж 5
Осторожно!
Открываем двери.
-----------
l1:
-----------
Осторожно!
Закрываем двери.
Этаж 5
Этаж 4
Этаж 3
Этаж 2
Этаж 1
Осторожно!
Открываем двери.
-----------
l2:
-----------
Осторожно!
Закрываем двери.
Этаж 5
Этаж 4
Этаж 3
Этаж 2
Этаж 1
Осторожно!
Открываем двери.
-----------
l1:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Осторожно!
Открываем двери.
-----------
l2:
-----------
Осторожно!
Закрываем двери.
Этаж 1
Этаж 2
Этаж 3
Осторожно!
Открываем двери.
-----------

Посмотрим, что у нас произошло. При первоначальном движении вверх у лифта номер 1 возникли проблемы. Контроллер открыл двери на 3-м этаже, при этом лифт не выдал никакого предупреждения и не остановился (о том, что двери открылись, можно судить по отсутствию сообщений об открывании дверей, когда лифт приехал на 5-й этаж). При движении вниз оба лифта отвергли попытку контроллера открыть двери с помощью setDoorsOpen, поскольку переменная moving была установлена в true. Наконец, при обратном движении наверх оба лифта остановились на 3-м этаже, поскольку контроллер установил moving в false. Итак, safeLift оказался полностью защищенным от некорректных действий контроллера.

Отметим, что строчка activationObject.__proto__ = this; в конструкторе безопасного лифта является обязательной. Попробуем закомментировать ее, и при движении лифта вниз (когда контроллер открывает двери при помощи setDoorsOpen ) мы получим:

l2:
-----------
Осторожно!
Закрываем двери.
Этаж 5
Этаж 4
Этаж 3
Осторожно!
Открываем двери.
Этаж 2
Этаж 1
-----------

То есть в этом случае функция setDoorsOpen проигнорировала переменную moving, что неудивительно - она просто не смогла ее найти в контексте вызова конструктора, который мы передали ей в качестве this. И только запись activationObject.__proto__ = this; спасает положение.

Заметим, что вместо activationObject.__proto__ = this, мы могли также написать var __proto__ = this. Действительно, var как раз позволяет добавлять нам ссылки в контекст вызова, так что и ссылку __proto__ можно добавить эдаким образом. Хотя рассудок протестует против идеи составлять выражение из трех ключевых слов разного вида и оператора присваивания. Но, тем не менее, такая запись тоже работает.

Является ли приведенный здесь код образцом хорошего стиля? Вряд ли. Но в особых случаях (когда при наследовании вам очень хочется сделать некоторые поля приватными) можно применять подобные приемы. Главное, чтобы вы точно знали, что делаете и зачем. А если вы часто испытываете нужду в более продвинутых объектных средствах разработки - переходите на Flash MX 2004.

Зарина Каримова
Зарина Каримова
Казахстан, Алматы, Гимназия им. Ахмета Байтурсынова №139, 2008
Akiyev Begench
Akiyev Begench
Беларусь, Полоцк, полоцкий государственный университет