Март 2011
Руководство по actionscript. часть 2, стр. 001
ГЛАВА 9
Интерфейс — это конструкция языка ActionScript, которая описывает новый тип данных подобно описанию типа данных с помощью класса. Однако, тогда как класс не только описывает тип данных, но и предоставляет для него реализацию, интерфейс только описывает тип данных в абстрактных терминах и не предоставляет реализацию для этого типа данных. Иными словами, класс не только объявляет группу методов и переменных, но и реализует определенное поведение; тела методов и значения переменных управляют поведением класса. Вместо того чтобы предоставлять собственную реализацию, интерфейс принимается одним или несколькими классами, которые согласны предоставить для него реализацию. Экземпляры класса, предоставляющего реализацию для интерфейса, принадлежат как типу данных класса, так и типу данных, описанному интерфейсом. Являясь одновременно членом нескольких типов данных, экземпляры могут выполнять в приложении различные функции.
I Не путайте термин «интерфейс», обсуждаемый в данной главе, с другими применени-мй а * ями этого слова. В этой главе «интерфейс» обозначает конструкцию языка ActionScript, а не графический интерфейс пользователя (GUI) или открытый API класса, которые в общей теории объектно-ориентированного программирования иногда именуются интерфейсами.
Если это ваше первое знакомство с интерфейсами, то их теоретические описания могут оказаться сложными для восприятия, поэтому сразу же рассмотрим пример.
Аргумент в пользу интерфейсов
Предположим, мы создаем протоколирующий класс Logger, который рапортует о статусных сообщениях («записях журнала») программы в процессе ее выполнения. Многие классы получают статусные сообщения от класса Logger и по-разному реагируют на них. Например, один класс — LogUI — отображает журнальные сообщения на экране, другой класс — LiveLog — отправляет предупреждение специалисту службы поддержки с помощью сетевого инструмента администрирования, а еще один класс — LogTracker — добавляет журнальные сообщения в базу для ведения статистики. Для получения журнальных сообщений каждый класс определяет метод update ( ). Чтобы отправить сообщение объектам всех заинтересованных классов, класс Logger вызывает метод update ( ).
Пока все это кажется логичным, но что произойдет, если мы забудем определить метод update ( ) в классе LogUI? Статусное сообщение будет отправлено, однако объекты класса LogUI не получат его. Нам необходим такой подход, который
гарантирует, что каждый получатель журнальных сообщений определяет метод update ( ).
Чтобы предоставить такую гарантию, предположим, что мы ввели новое требование в нашу программу: любой объект, желающий получать журнальные сообщения от класса Logger, должен быть экземпляром базового класса LogRecipient (мы предоставим его описание) или экземпляром одного из подклассов класса LogRecipient. Реализация метода update ( ) в классе LogRecipient будет включать базовую функциональность, попросту отображая журнальное сообщение с помощью функции trace ( ):
public class LogRecipient { public function update (msg:String)-.void { trace(msg);
}
}
Теперь любой класс, который желает получать журнальные сообщения от класса Logger, просто расширяет класс LogRecipient и, если требуется специфическое поведение, перекрывает метод update ( ) класса LogRecipient, реализуя желаемое поведение. Например, следующий класс LogTracker расширяет класс LogRecipient и перекрывает метод update ( ), реализуя функциональность сохранения информации в базе данных:
public class LogTracker extends LogRecipient { // Перекрытый метод update( ) класса LogRecipient override public function update (msg:String):void { // Сохранение сообщения об ошибке в базе данных. Код не показан…
Продолжение:
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,
77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,
109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,
135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158
Руководство по actionscript. часть 2, стр. 002
}
}
Возвращаясь к классу Logger, мы определим метод addRecipient ( ), который выполняет регистрацию объекта, желающего получать журнальные сообщения. Базовое описание метода addRecipient ( ) представлено ниже. Обратите внимание, что в метод addRecipient ( ) могут передаваться только экземпляры класса LogRecipient и его подклассов:
public class Logger { public function addRecipient (1r:LogRecipient):Boolean { // Размещаемый здесь код должен выполнять регистрацию объекта 1г // для получения статусных сообщений и возвращать значение типа Boolean. // которое отражает результат выполненной операции (код не показан).
}
}
Если передаваемый в метод addRecipient ( ) объект не принадлежит типу LogRecipient, компилятор сгенерирует ошибку несоответствия типов. Если этот объект является экземпляром подкласса класса LogRecipient, в котором не реализован метод update ( ), то по крайней мере будет выполнена базовая версия метода update ( ) (определенная в классе LogRecipient).
Приемлемый вариант, не правда ли? Почти. Однако есть проблема. Что делать в том случае, если класс, желающий получать события от класса LogRecipient,
уже расширяет другой класс? Например, предположим, что класс LogUI расширяет класс flash. display. Sprite:
public class LogUI extends Sprite { public function update (msg:String):void { // Отображение статусного сообщения на экране, код не показан…
}
}
В языке ActionScript класс не может расширять несколько классов. Класс LogUI уже расширяет класс Sprite, поэтому он не может расширять еще и класс LogRecipient. Следовательно, экземпляры класса LogUI не могут регистрироваться в классе Logger, чтобы получать от него статусные сообщения. В данной ситуации нам крайне необходим способ, позволяющий указать, что на самом деле экземпляры класса LogUI принадлежат двум типам данных: LogUI и LogRecipient.
Руководство по actionscript. часть 2, стр. 003
В игру вступают… интерфейсы!
Интерфейсы и классы с несколькими типами данных
В предыдущем разделе мы создали тип данных LogRecipient, определив класс LogRecipient. Ограничение данного подхода заключается в том, что каждый получатель сообщений от класса Logger должен быть экземпляром либо класса LogRecipient, либо одного из его подклассов. Чтобы избавиться от этого ограничения, мы можем описать тип данных LogRecipient путем создания интерфейса LogRecipient, а не путем создания одноименного класса. В этом случае экземпляры любого класса, который формально согласен предоставить реализацию для метода update ( ), могут регистрироваться для получения журнальных сообщений. Посмотрим, как это работает.
Синтаксически интерфейс представляет собой просто список методов. Например, следующий код создает интерфейс LogRecipient, содержащий один-единственный метод update ( ) (обратите внимание, что интерфейсы, как и классы, могут быть описаны с помощью модификаторов управления доступом public и internal).
public interface LogRecipient { function update(msg:String):void;
}
Как только интерфейс будет описан, любое количество классов может использовать ключевое слово implements, чтобы вступить в соглашение с этим интерфейсом, пообещав определить содержащиеся в нем методы. Как только класс даст такое обещание, его экземпляры будут считаться членами не только типа данных класса, но и типа данных интерфейса.
Например, чтобы указать, что класс LogUI согласен определить метод update ( ) (описанный в интерфейсе LogRecipient), мы используем следующий код:
class LogUI extends Sprite implements LogRecipient { public function update (msg:String):void { // Отображение статусного сообщения на экране, код не показан…
}
}
Вместо того чтобы расширять класс LogRecipient, LogUI расширяет класс Sprite и реализует интерфейс LogRecipient. Поскольку класс LogUI реализует интерфейс LogRecipient, он должен определить метод update ( ). В противном случае компилятор сгенерирует следующую ошибку:
Interface method update in namespace LogRecipient not implemented by class LogUI.
Руководство по actionscript. часть 2, стр. 004
На русском языке она будет выглядеть следующим образом: Метод интерфейса update из пространства имен LogRecipient не реализован классом LogUI.
Поскольку класс LogUI обещает реализовать методы интерфейса LogRecipient, экземпляры этого класса могут быть использованы везде, где требуется тип данных LogRecipient. Экземпляры класса LogUI фактически принадлежат двум типам данных: LogUI и LogRecipient. Таким образом, даже несмотря на то, что класс LogUI расширяет класс Sprite, экземпляры класса LogUI принадлежат типу LogRecipient и могут благополучно передаваться в метод addRecipient ( ) класса Logger.
Ошибки компилятора — это ключ ко всей системе интерфейсов. Они гарантируют, что класс сдержит свои обязательства по реализации методов интерфейса, тем самым позволив внешнему коду использовать этот класс с уверенностью в его правильном поведении. Эта уверенность особенно важна при разработке приложения, возможности которого будут расширяться другим разработчиком или использоваться третьими лицами.
Теперь, когда мы получили общее представление об интерфейсах и о том, как они используются, рассмотрим детали синтаксиса.
Синтаксис и использование интерфейсов
Как уже известно, интерфейс описывает новый тип данных, не предоставляя реализацию ни для одного из методов этого типа. Таким образом, для создания интерфейса используется следующий синтаксис:
interface НекоеИмя {
function д/етол! (параметр1:типданных… параметрп-.типданных): типВозвращаемогоЗначения;
function метод2 (параметр1:типданных. . . параметрп-.типданных): типВозвращаемогоЗначения;
function метода (параметр^, типданных… параметрп-.типданных): типВозвращаемогоЗначения;
}
Здесь НекоеИмя — это имя интерфейса, метод1. . . методп — методы данного интерфейса, параметр1: типданных. . . параметрп: типданных — параметры методов, а типВоз — вращаемогоЗначения — тип данных возвращаемого значения каждого из методов.
Объявления методов в интерфейсах не включают (и не должны включать) фигурные скобки. Следующее объявление метода вызовет ошибку в интерфейсе на этапе компиляции, поскольку в объявление включены фигурные скобки:
function метод1 (параметр-.типданных)-.типВозвращаемогоЗначения { }
Возникшая ошибка:
Methods defined in an interface must not have a body.
Руководство по actionscript. часть 2, стр. 005
На русском языке ошибка будет выглядеть так: Описываемые в интерфейсе методы не должны иметь тела.
Объявления всех методов в интерфейсе не должны включать модификаторы управления доступом. Интерфейсы в языке ActionScript не могут содержать определения переменных; описания интерфейсов не могут быть вложенными. Тем не менее интерфейсы могут включать get — и set-методы, которые могут применяться для имитации переменных (с позиции кода, использующего эти методы). Описания интерфейсов, как и описания классов, могут размещаться либо непосредственно внутри оператора package, либо за его пределами, но нигде больше.
Как было сказано в предыдущем разделе, класс, который желает принять тип данных интерфейса, должен согласиться реализовать методы этого интерфейса. Чтобы сформировать данное соглашение, класс использует ключевое слово implements, имеющее следующий синтаксис:
class НекоеИмя implements НекийИнтерфейс { }
В данном коде НекоеИмя — это имя класса, который обещает реализовать методы интерфейса НекийИнтерфейс, а НекийИнтерфейс — имя интерфейса. Говорят, что класс НекийКласс «реализует интерфейс НекийИнтерфейс». Обратите внимание, что, если в описании класса используется раздел extends, ключевое слово implements должно идти после него. Более того, если после ключевого слова implement s вместо имени интерфейса вы укажете имя класса, компилятор сгенерирует следующую ошибку:
An interface can only extend other interfaces, but ИмяКласса is a class.
По-русски она будет выглядеть так: Интерфейс может только расширять другие интерфейсы, а ИмяКласса является классом.
Класс НекоеИмя должен реализовать все методы, описанные в интерфейсе НекийИнтерфейс, иначе на этапе компиляции возникнет следующая ошибка:
Interface method имяМетода in namespace ИмяИнтерфейса not implemented by class ИмяКласса.
На русском языке ошибка означает следующее: Метод интерфейса имяМетода в пространстве имен ИмяИнтерфейса не реализован классом ИмяКласса.
Определения методов в классе, реализующем интерфейс, должны быть открытыми и в точности соответствовать определениям методов интерфейса, включая количество и типы параметров, а также тип возвращаемого значения. Если между интерфейсом и реализующим его классом существует различие по любому из перечисленных аспектов, компилятор сгенерирует следующую ошибку:
Interface method имяМетода in namespace ИмяИнтерфейса is implemented with an incompatible signature in class ИмяКласса.
По-русски она будет звучать так: Метод интерфейса имяМетода в пространстве имен ИмяИнтерфейса реализован с несовместимой сигнатурой в классе ИмяКласса.
Руководство по actionscript. часть 2, стр. 006
Класс может реализовать несколько интерфейсов, разделив их имена запятыми, как показано в следующем коде:
class НекоеИмя implements НекийИнтерфейс, НекийДругойИнтерфейс { }
В данном случае экземпляры класса НекоеИмя принадлежат всем трем указанным типам данных: НекоеИмя, НекийИнтерфейс и НекийДругойИнтерфейс. Если класс реализует два интерфейса, в каждом из которых описан метод с одним и тем же именем, но с отличающейся сигнатурой (то есть имя метода, список параметров и тип возвращаемого значения), то компилятор сгенерирует ошибку, указывающую на то, что один из методов реализован неправильно.
С другой стороны, если класс реализует два интерфейса, в каждом из которых описан метод с одним и тем же именем и совпадающей сигнатурой, ошибки не будет. Однако в связи с этим возникает вопрос: может ли класс предоставить функциональность, необходимую обоим интерфейсам, в одном определении метода? В большинстве случаев ответ на этот вопрос отрицателен.
Добавление новых методов в интерфейс, который уже реализован одним или несколькими классами, приведет к появлению в этих классах ошибок на этапе компиляции (поскольку в классах определение новых методов отсутствует)! Таким образом, перед внесением изменений в код вы должны тщательно подумать, какие методы будут включены в интерфейс, и быть уверенными в архитектуре своего приложения.
Если класс объявит, что он реализует некий интерфейс, но компилятор не сможет найти этот интерфейс, то появится следующая ошибка:
Interface ИмяИнтерфейса was not found.
На русском языке ошибка будет выглядеть следующим образом: Интерфейс ИмяИнтерфейса не найден.
Соглашения по именованию интерфейсов
Имена интерфейсов, как и имена классов, должны начинаться с прописной буквы, давая понять, что они представляют типы данных. Большинству интерфейсов имена даются исходя из дополнительной возможности, описываемой этими интерфейсами.
Предположим, что приложение содержит несколько классов, представляющих визуальные объекты. Некоторые объекты можно перемещать, другие — нет. В нашем проекте объекты, которые могут быть перемещены, должны реализовать интерфейс Moveable. Рассмотрим пример теоретического класса Product Icon, реализующего интерфейс Moveable:
public class Productlcon implements Moveable { public function getPosition ( ):Point {
}
public function setPosition (pos:Point):void { }
}
Интерфейс с именем Moveable обозначает конкретную возможность, которую он добавляет в класс. Объект может быть фрагментом изображения или блоком текста, но, если он реализует интерфейс Moveable, его можно перемещать. Примерами похожих имен являются Storable, Kill able или Serializable. Некоторые разработчики перед именем интерфейса дополнительно указывают букву I, например IMoveable, IKillable или ISerializable.
Руководство по actionscript. часть 2, стр. 007
Наследование интерфейсов
Как и в случае с классами, для наследования одного интерфейса от другого может применяться ключевое слово extends. Например, следующий код демонстрирует интерфейс IntA, который расширяет другой интерфейс — IntB. В данной схеме интерфейс IntB называется подинтерфейсом, а интерфейс IntA — суперинтерфейсом.
public interface IntA { function methodA ( ):void;
}
public interface IntB extends IntA { function methodB ( ):void;
}
Классы, реализующие интерфейс IntB, должны определять не только метод methodB ( ), но и метод methodA ( ). Наследование интерфейсов позволяет описывать иерархию типов, во многом напоминающую иерархию, которая образуется при использовании наследования классов, но без предоставления реализаций методов.
Интерфейсы языка ActionScript также поддерживают множественное наследование, то есть один интерфейс может расширять несколько. Например, рассмотрим следующие три описания интерфейсов:
public interface IntC { function methodC ( ):void;
}
public interface IntD { function methodD ( ):void;
}
public interface IntE extends IntC. IntD { function methodE ( ):void;
}
Поскольку интерфейс IntE расширяет оба интерфейса IntC и IntD, классы, реализующие интерфейс IntE, должны предоставить определения для методов methodC( ), methodD ( ) и methodE ( ).
Руководство по actionscript. часть 2, стр. 008
Интерфейсы-маркеры
Чтобы быть полезными, интерфейсы могут вообще не содержать никаких методов. Иногда пустые интерфейсы, называемые интерфейсами-маркерами, применяются для «отметки» (обозначения) класса, обладающего некоторой возможностью. Требования, предъявляемые к отмеченным классам (классам, реализующим интерфейс-маркер), описываются в документации по каждому конкретному интерфейсу-маркеру. Например, API среды выполнения Flash включает интерфейс-маркер IBitmapDrawable, обозначающий класс, который может быть отображен объектом BitmapData. Класс BitmapData будет отображать только те классы, которые реализуют интерфейс IBitmapDrawable (хотя на самом деле этот интерфейс не определяет никаких методов). Интерфейс IBitmapDrawable используется просто для того, чтобы «показать», что данный класс пригоден для работы с растровым изображением. Вот исходный код интерфейса IBitmapDrawable:
package flash. display { interface IBitmapDrawable { }
}
Другой пример использования составных типов
Как уже известно из предыдущего примера с протоколирующим классом, класс может не только наследоваться от другого класса, но и реализовывать интерфейс. Экземпляры подкласса одновременно принадлежат типу данных суперкласса и типу данных интерфейса. Например, экземпляры класса LogUI из рассмотренного примера принадлежали типам данных Sprite и LogRecipient, поскольку класс LogUI был унаследован от класса Sprite и реализовывал интерфейс LogRecipient. Рассмотрим эту важную архитектурную структуру на новом примере.
**4
Для понимания материала, изложенного в этом разделе, требуется предварительное знание массивов (упорядоченных списков значений), которые еще не рассматривались Эа1 в этой книге. Если вы незнакомы с массивами, то пока пропустите этот раздел и вернитесь к нему после изучения гл. 11.
Предположим, мы создаем приложение, которое сохраняет объекты на сервере с помощью серверного сценария. Класс каждого сохраняемого объекта обязан предоставить метод serialize ( ), возвращающий строковое представление экземпляров данного класса. Строковое представление используется для воссоздания определенного объекта с нуля.
Одним из классов нашего приложения является простой класс Rectangle с переменными экземпляра width, height, fillColor и lineColor. Для представления объектов Rectangle в виде строк класс Rectangle реализует метод serialize( ), который возвращает строку следующего формата:
«wi <№\=значение | hei ф^значение | f i 11 col ог=значение \ 1 i necol ог=значение"
Чтобы сохранить объект Rectangle на сервере, мы вызываем метод serialize ( ) над данным объектом и передаем полученную строку в наш серверный сценарий. В дальнейшем мы сможем получить эту строку и создать новый экземпляр класса Rectangle, размеры и цвет которого будут соответствовать значениям исходного экземпляра.
Для упрощения данного примера мы будем полагать, что каждый сохраняемый объект в приложении должен сохранять только имена переменных и их значения. Мы также будем полагать, что никакие значения переменных сами по себе не являются объектами, для которых требуется сериализация (то есть преобразован в строку).
Руководство по actionscript. часть 2, стр. 009
Когда наступит время сохранить состояние нашего приложения, экземпляр пользовательского класса StorageManager выполнит следующие задачи.
1. Соберет объекты для сохранения.
2. Преобразует каждый объект в строку (вызвав метод serialize( )).
3. Перенесет объекты на диск.
Чтобы гарантировать тот факт, что каждый сохраняемый объект может быть се-риализован, класс StorageManager отклонит любые экземпляры классов, которые не принадлежат типу данных Serializable. Вот фрагмент кода класса StorageManager, демонстрирующий метод addOb j ееt ( ), который используется объектами для регистрации в списке сохраняемых объектов (обратите внимание, что в этот метод могут быть переданы только экземпляры, принадлежащие типу Serializable):
package { public class StorageManager { public function addObject (o:Serializable):void { }
}
}
Тип данных Serializable описывается одноименным интерфейсом, который содержит один-единственный метод serialize ( ), как показано в следующем коде:
package { public interface Serializable { function serialize( ) .-String;
}
}
Для выполнения сериализации создадим класс Serialize г, реализующий интерфейс Serializable. Этот класс предоставляет следующие базовые методы для сериализации любого объекта:
? setSerializationObj ( )— указывает объект для сериализации;
? setSerializationVars ( ) — задает, какие переменные объекта должны быть сериализованы;
? setRecordSeparator ( ) — указывает строку, используемую в качестве разделителя между переменными;
? serialize( ) — возвращает строку, представляющую объект.
Руководство по actionscript. часть 2, стр. 010
Рассмотрим исходный код класса Serializer: package {
public class Serializer implements Serializable { private var serializationVars:Array; private var serializationObj:Serializable; private var recordSeparator:String;
public function Serializer ( ) { setSeri alizati onObj(thi s);
}
public function setSerializationVars (vars:Array):void { serializationVars = vars;
}
public function setSerializationObj (obj:Serializable):void { serializationObj = obj;
}
public function setRecordSeparator (rs:String):void { recordSeparator = rs;
}
public function serialize ( ):String { var s:String = «»;
// Обратите внимание, что счетчик цикла // уменьшается до нуля.
// а его обновление (декремент i) происходит внутри // условного выражения цикла
for (var i:int = serializationVars. length; —i >= 0; ) { s += serializationVars[i]
+ «=» + String(serializationObj[serializationVars[i]]); if (i > 0) { s += recordSeparator;
}
}
return s; } ‘
}
}
Если какой-либо класс желает воспользоваться возможностями сериализации класса Serializer, то может просто расширить его. Класс, непосредственно расширивший класс Serializer, унаследует и интерфейс Serializable, и реализацию этого интерфейса классом Serializer.