Tematy zaawansowane

Historia elementów programowania zorientowanego obiektowo w języku ActionScript

Ponieważ język ActionScript 3.0 jest oparty na poprzednich wersjach języka ActionScript, warto przyjrzeć się ewolucji modelu obiektowego w kolejnych wersjach języka. ActionScript był początkowo prostym mechanizmem skryptowym współpracującym z wczesnymi wersjami programu Flash Professional. Z czasem programiści zaczęli budować z jego pomocą coraz bardziej złożone aplikacje. W odpowiedzi na potrzeby takich programistów w kolejnych wersjach wprowadzano elementy języka usprawniające tworzenie skomplikowanych aplikacji.

ActionScript 1.0

ActionScript 1.0 to wersja języka używana w programie Flash Player 6 i wcześniejszych wersjach. Nawet na tym wczesnym etapie rozwoju model obiektowy języka ActionScript był oparty na koncepcji obiektu jako fundamentalnego typu danych. Obiekt w języku ActionScript jest złożonym typem danym składającym się z grupy właściwości . Przy omawianiu modelu obiektowego termin właściwość oznacza wszystkie elementy skojarzone z obiektem, takie jak zmienne, funkcje i metody.

Mimo że w pierwszej generacji języka ActionScript nie było możliwe definiowanie klas za pomocą słowa kluczowego class , można było definiować klasy przy użyciu specjalnego rodzaju obiektu, tzw. obiektu prototypowego. W językach opartych na klasach, takich jak Java i C++, programista za pomocą słowa kluczowego class tworzy abstrakcyjną definicję klasy, a następnie tworzy konkretne obiekty będące instancjami tej klasy. Natomiast w językach opartych na prototypach, takich jak ActionScript 1.0, używa się istniejącego obiektu jako modelu (prototypu) dla innych obiektów. Podczas gdy obiekty w języku opartym na klasach modą wskazywać klasę będącą ich szablonem, obiekty w języku opartym na prototypach wskazują na inny obiekt, prototyp, pełniący rolę takiego szablonu.

Aby utworzyć klasę w języku ActionScript 1.0 należy zdefiniować funkcję będącą konstruktorem tej klasy. W język ActionScript funkcje są faktycznie obiektami, a nie tylko abstrakcyjnymi definicjami. Utworzony konstruktor pełni rolę obiektu prototypowego dla instancji tej klasy. W poniższym kodzie utworzono klasę o nazwie Shape i zdefiniowano jedną właściwość o nazwie visible , która domyślnie przyjmuje wartość true :

// base class 
function Shape() {} 
// Create a property named visible. 
Shape.prototype.visible = true;

Ten konstruktor definiuje klasę Shape, której instancje można tworzyć za pomocą operatora new , w następujący sposób:

myShape = new Shape();

Obiekt funkcji-konstruktora Shape() jest prototypem instancji klasy Shape, ale może także pełnić rolę prototypu podklas klasy Shape — tj. innych klas rozszerzających klasę Shape.

Tworzenie podklasy klasy Shape przebiega dwuetapowo. Najpierw należy utworzyć klasę, definiując jej konstruktor, w następujący sposób:

// child class 
function Circle(id, radius) 
{ 
this.id = id; 
this.radius = radius; 
}

Następnie za pomocą operatora new należy zadeklarować, że klasa Shape jest prototypem klasy Circle. Domyślnie prototypem każdej tworzonej kasy jest klasa Object, co oznacza, że właściwość Circle.prototype zawiera obecnie obiekt ogólny (instancję klasy Object). Aby wskazać, że prototypem klasy Circle jest klasa Shape, a nie Object, należy za pomocą poniższego kodu zmienić wartość właściwości Circle.prototype , tak aby zawierała obiekt Shape, a nie obiekt ogólny:

// Make Circle a subclass of Shape. 
Circle.prototype = new Shape();

Klasa Shape jest teraz powiązana z klasą Circle relacja dziedziczenia nazywaną łańcuchem prototypów . Poniższy diagram ilustruje relacje w łańcuchu prototypów:

Klasą bazową na końcu każdego łańcucha prototypów jest zawsze klasa Object. Klasa Object zawiera właściwość statyczną o nazwie Object.prototype , która wskazuje na bazowy obiekt prototypowy wszystkich obiektów tworzonych w języku ActionScript 1.0. Następnym obiektem w przykładowym łańcuchu prototypów jest obiekt Shape. Wynika to z faktu, że właściwości Shape.prototype nigdy w sposób jawny nie przypisano wartości, a więc nadal zawiera ona obiekt ogólny (instancję klasy Object). Ostatnim ogniwem tego łańcucha jest klasa Circle powiązana ze swoim prototypem, klasą Shape (właściwość Circle.prototype zawiera obiekt Shape).

Jeśli utworzymy instancję klasy Circle, tak jak w poniższym przykładzie, instancja odziedziczy łańcuch prototypów klasy Circle:

// Create an instance of the Circle class. 
myCircle = new Circle();

Przypominamy, że w przykładzie występowała właściwość o nazwie visible jako element klasy Shape. W tym przykładzie właściwość visible nie istnieje jako element obiektu myCircle , lecz jako element obiektu Shape, a mimo to następujący wiersz kodu daje w wyniku wartość true :

trace(myCircle.visible); // output: true

Środowisko wykonawcze, analizując łańcuch prototypów, jest w stanie ustalić, że obiekt myCircle dziedziczy właściwość visible . Podczas wykonywania tego kodu środowisko wykonawcze we właściwościach obiektu myCircle poszukuje właściwości o nazwie visible , ale takiej właściwości nie znajduje. Następnie szuka właściwości w obiekcie Circle.prototype , ale nadal nie znajduje tam właściwości o nazwie visible . Przechodząc coraz wyżej w łańcuchu prototypów, znajduje właściwość visible zdefiniowaną w obiekcie Shape.prototype i wyświetla wartość tej właściwości.

W celu zapewnienia prostoty wiele szczegółów i zawiłości łańcucha prototypów zostało pominiętych. Celem jest udostępnienie takiej ilości informacji, która umożliwia zrozumienie działania modelu obiektów ActionScript 3.0.

ActionScript 2.0

W języku ActionScript 2.0 wprowadzono nowe słowa kluczowe, takie jak class , extends , public i private , które pozwalały na definiowanie klas w sposób naturalny dla programistów posługujących się językami opartymi na klasach, takimi jak Java i C++. Należy jednak podkreślić, że zasadniczy mechanizm dziedziczenia w językach ActionScript 1.0 oraz ActionScript 2.0 nie uległ zmianie. W języku ActionScript 2.0 dodano jedynie nowe elementy składni służące do definiowania klas. Łańcuch prototypów działa tak samo w obu wersjach języka.

Nowe elementy składni wprowadzone w języku ActionScript 2.0, przedstawione w poniższym fragmencie kodu, umożliwiają definiowanie klas w sposób uważany przez wielu programistów za bardziej intuicyjny:

// base class 
class Shape 
{ 
var visible:Boolean = true; 
}

Ponadto w języku ActionScript 2.0 wprowadzono deklaracje typów pozwalające na sprawdzanie zgodności typów na etapie kompilacji. A zatem właściwość visible z poprzedniego przykładu można byłoby zadeklarować jako przyjmującą tylko wartości typu Boolean. Nowe słowo kluczowe extends upraszcza proces tworzenia podklasy. W poniższym przykładzie dwuetapowy proces wymagany w języku ActionScript 1.0 jest realizowany w jednym kroku przy użyciu słowa kluczowego extends :

// child class 
class Circle extends Shape 
{ 
    var id:Number; 
    var radius:Number; 
    function Circle(id, radius) 
    { 
        this.id = id; 
        this.radius = radius; 
        } 
}

Konstruktor jest teraz deklarowany jako część definicji klasy, a właściwości klasy id oraz radius muszą być zadeklarowane jawnie.

W języku ActionScript 2.0 umożliwiono definiowanie interfejsów. Interfejsy pozwalają zoptymalizować strukturę programów zorientowanych obiektowo, ponieważ definiują formalnie protokoły komunikacji między obiektami.

Obiekt klasy w języku ActionScript 3.0

W powszechnie spotykanym modelu programowania zorientowanego obiektowo, zwykle kojarzonym z językami Java i C++, do definiowania typów obiektów używa się klas. W językach programowania opartych na tym modelu z reguły klasy służą także do budowania instancji typów danych definiowanych przez te klasy. W języku ActionScript klasy mają oba te zastosowania, ale ponieważ język wywodzi się z modelu opartego na prototypach, ma pewne interesujące cechy. W języku ActionScript definicja każdej klasy jest specjalnym obiektem klasy, który może udostępniać zarówno zachowania, jak i określone stany. Jednak dla wielu programistów korzystających z języka ActionScript to rozróżnienie nie ma praktycznego znaczenia. Język ActionScript 3.0 zaprojektowano w taki sposób, aby umożliwiał tworzenie zaawansowanych aplikacji zorientowanych obiektowo bez konieczności używania tych specjalnych obiektów klas, a nawet bez świadomości ich istnienia.

Poniższy schemat ilustruje strukturę obiektu klasy reprezentującego prostą klasę A zdefiniowaną za pomocą instrukcji class A {} :

Każdy prostokąt na diagramie reprezentuje jeden obiekt. Każdy obiekt na diagramie ma indeks A oznaczający, że należy on do klasy A. Obiekt klasy (CA) zawiera odwołania do szeregu innych ważnych obiektów. Obiekt cech instancji (TA) zawiera właściwości instancji zdefiniowane w definicji klasy. Obiekt cech klasy (TCA) reprezentuje wewnętrzny typ klasy i zawiera właściwości statyczne definiowane przez klasę (indeks C to skrót od słowa „class”). Obiekt prototypowy (PA) zawsze oznacza obiekt klasy, z którym był pierwotnie skojarzony, za pośrednictwem właściwości constructor .

Obiekt cech

Obiekt cech, który jest nowym elementem środowiska ActionScript 3.0, to rozwiązanie wprowadzone z myślą o wydajności działania kodu. W poprzednich wersjach środowiska ActionScript wyszukiwanie nazw było potencjalnie czasochłonne, ponieważ program Flash Player sprawdzał kolejne ogniwa łańcucha prototypów. W środowisku ActionScript 3.0 wyszukiwanie nazw przebiega znacznie wydajniej i szybciej, ponieważ właściwości dziedziczone są kopiowane z nadklas do obiektów cech podklas.

Obiekt cech nie jest bezpośrednio dostępny dla kodu pisanego przez programistę, ale jego obecność ujawnia się pośrednio poprzez większą szybkość działania kodu i optymalizację zużycia pamięci. Obiekt cech dostarcza maszynie AVM2 szczegółowych informacji o strukturze i zawartości klasy. Na podstawie tych informacji maszyna AVM2 jest w stanie znacznie przyspieszyć wykonywanie kodu, ponieważ często możliwe jest wygenerowanie instrukcji maszynowych uzyskujących dostęp do właściwości lub bezpośrednie wywoływanie metod bez czasochłonnego wyszukiwania nazw.

Dzięki obiektom cech obiekty aplikacji zajmują znacznie mniej miejsca w pamięci niż ich odpowiedniki w starszych wersjach środowiska ActionScript. Na przykład, jeśli klasa jest zapieczętowana (tj. nie jest zadeklarowana jako dynamiczna), to instancja tej klasy nie musi zawierać tabeli mieszania dla dynamicznie dodawanych właściwości, a obiekt tej klasy zajmuje niewiele więcej miejsca niż wskaźnik do obiektu cech i pola na właściwości ustalone zdefiniowane w klasie. W rezultacie obiekt, który w środowisku ActionScript 2.0 zajmował 100 bajtów pamięci, w środowisku ActionScript 3.0 zajmuje zaledwie 20 bajtów.

Uwaga: Obiekt cech jest wewnętrznym rozwiązaniem implementacyjnym i nie ma gwarancji, że w przyszłych wersjach środowiska ActionScript nie zostanie zmieniony lub nawet całkowicie wyeliminowany.

Obiekt prototypowy

Każdy obiekt klasy w języku ActionScript ma właściwość prototype , która zawiera odwołanie do obiektu prototypowego klasy. Obiekt prototypowy jest zaszłością wywodzącą się z korzeni ActionScript jako języka opartego na prototypach. Więcej informacji zawiera dokumentacja Historia elementów programowania zorientowanego obiektowo w języku ActionScript .

Właściwość prototype jest przeznaczona tylko do odczytu, co oznacza, że nie można jej zmodyfikować tak, aby wskazywała na inne obiekty. Zachowuje się zatem inaczej niż właściwość prototype klas w poprzednich wersjach języka ActionScript, które umożliwiały zmianę przypisania prototypu tak, aby właściwość ta wskazywała na inna klasę. Mimo że właściwość prototype jest przeznaczona tylko do odczytu, obiekt prototypowy, do którego ta właściwość się odwołuje, może być modyfikowany. Innymi słowy, do obiektu prototypowego można dodawać nowe właściwości. Właściwości dodawane do obiektu prototypowego są wspólne dla wszystkich instancji klasy.

Łańcuch prototypów, który w poprzednich wersjach języka ActionScript był jedynym mechanizmem dziedziczenia, pełni w języku ActionScript 3.0 jedynie drugorzędną rolę. Podstawowy mechanizm dziedziczenia, tj. dziedziczenie właściwości ustalonych, jest realizowany wewnętrzne przez obiekty cech. Właściwość ustalona do zmienna lub metoda zdefiniowana jako element definicji klasy. Dziedziczenie właściwości ustalonych nazywane jest także dziedziczeniem klas, ponieważ ten właśnie mechanizm dziedziczenia jest powiązany z takimi słowami kluczowymi, jak class , extends i override .

Łańcuch prototypów stanowi alternatywny mechanizm dziedziczenia — bardziej dynamiczny niż dziedziczenie właściwości ustalonych. Możliwe jest dodawanie właściwości do obiektu prototypowego klasy nie tylko w definicji klasy, lecz również za pośrednictwem właściwości prototype obiektu klasy. Należy jednak zwrócić uwagę, że w wypadku wybrania trybu ścisłego kompilatora nie będzie możliwe uzyskanie dostępu do właściwości dodanych do obiektu prototypowego, jeśli klasa nie będzie zadeklarowana ze słowem kluczowy dynamic .

Za przykład klasy z wieloma właściwościami powiązanymi z obiektem prototypowym jest klasa Object. Metody toString() i valueOf() klasy Object są w istocie funkcjami przypisanymi właściwościom obiektu prototypowego tej klasy. Poniżej przedstawiono teoretyczną deklarację tych metod (w praktyce deklaracja ta jest nieco inna z uwagi na szczegóły implementacyjne):

public dynamic class Object 
{ 
    prototype.toString = function() 
    { 
        // statements 
    }; 
    prototype.valueOf = function() 
    { 
        // statements 
    }; 
}

Jak już wcześniej wspomniano, możliwe jest powiązanie właściwości z obiektem prototypowym klasy poza definicją klasy. Na przykład metoda toString() mogłaby być zdefiniowana poza definicją klasy Object, w następujący sposób:

Object.prototype.toString = function() 
{ 
    // statements 
};

Jednak w przeciwieństwie do dziedziczenia właściwości ustalonych dziedziczenie oparte na prototypach nie wymaga użycia słowa override przy przesłanianiu metody w podklasie. Na przykład. jeśli chcemy przedefiniować metodą valueOf() w podklasie obiektu Object, mamy do wyboru trzy możliwości. Po pierwsze, możemy zdefiniować metodę valueOf() w obiekcie prototypowym podklasy, wewnątrz definicji tej podklasy. Poniższy kod tworzy podklasę klasy Object o nazwie Foo i zmienia definicję metody valueOf() w obiekcie prototypowym Foo w ramach definicji klasy. Ponieważ każda klasa dziedziczy z klasy Object, nie jest konieczne użycie słowa kluczowego extends .

dynamic class Foo 
{ 
    prototype.valueOf = function() 
    { 
        return "Instance of Foo"; 
    }; 
}

Po drugie, możemy zdefiniować metodę valueOf() w obiekcie prototypowym Foo poza definicją klasy, co ilustruje poniższy kod:

Foo.prototype.valueOf = function() 
{ 
    return "Instance of Foo"; 
};

Po trzecie, możemy zdefiniować ustaloną właściwość o nazwie valueOf() jako element klasy Foo. Ta technika polega na jednoczesnym zastosowaniu dziedziczenia właściwości ustalonych i dziedziczenia opartego na prototypach. W każdej podklasie klasy Foo, w której chcielibyśmy zmienić definicję właściwości valueOf() , musimy użyć słowa kluczowego override . Poniższy kod przedstawia definicję valueOf() jako właściwości ustalonej w klasie Foo:

class Foo 
{ 
    function valueOf():String 
    { 
        return "Instance of Foo"; 
    } 
}

Przestrzeń nazw AS3

Istnienie dwóch odrębnych mechanizmów dziedziczenia, tj. dziedziczenia właściwości ustalonych i dziedziczenia opartego na prototypach, stwarza pewne potencjalne problemy ze zgodnością właściwości i metod klas podstawowych języka. Robocza specyfikacja ECMAScript, na której oparty jest język ActionScript, zakłada stosowanie dziedziczenia w oparciu o prototypy, co oznacza że właściwości i metody klas podstawowych języka są zdefiniowane w swoich obiektach prototypowych. Z drugiej strony, zgodność z wywołaniami języka ActionScript 3.0, w których stosowane jest dziedziczenie właściwości ustalonych, wymaga zdefiniowania właściwości i metod klas podstawowych za pomocą słów kluczowych const , var i function . Co więcej, użycie klas ustalonych zamiast ich wersji prototypowych może prowadzić do znacznego przyspieszenia wykonywania kodu.

W języku ActionScript 3.0 problem ten rozwiązano, stosując w klasach podstawowych języka zarówno dziedziczenie oparte na prototypach, jak i dziedziczenie właściwości ustalonych. Każda klasa podstawowa języka zawiera dwa zestawy właściwości i metod. Jeden zestaw jest zdefiniowany w obiekcie prototypowym w celu zapewnienia zgodności ze specyfikacją ECMAScript, a drugi zestaw jest zdefiniowany przy użyciu właściwości ustalonych w przestrzeni nazw AS3.0 w celu zapewnienia zgodności z językiem ActionScript 3.0.

Przestrzeń nazw AS3 udostępnia wygodny mechanizm wyboru jednego z tych zestawów właściwości i metod. Jeśli w programie nie jest używana przestrzeń nazw AS3, instancja klasy podstawowej języka dziedziczy właściwości i metody zdefiniowane w obiekcie prototypowym klasy podstawowej. Jeśli przestrzeń nazw AS3 jest używana, instancja klasy podstawowej dziedziczy wersje z tej przestrzeni nazw (AS3), ponieważ właściwości ustalone mają zawsze pierwszeństwo przed prototypowymi. Innymi słowy, gdy tylko dostępna jest właściwość ustalona, jest używana w miejsce właściwości prototypowej o tej samej nazwie.

Możliwe jest selektywne stosowanie właściwości i metod z przestrzeni AS3 poprzez kwalifikowanie nazw przestrzenią AS3. Na przykład w poniższym kodzie zastosowano wersję metody Array.pop() z przestrzeni AS3:

var nums:Array = new Array(1, 2, 3); 
nums.AS3::pop(); 
trace(nums); // output: 1,2

Alternatywnym rozwiązaniem byłoby użycie dyrektywy use namespace w celu otwarcia przestrzeni nazw AS3 dla wszystkich definicji w bloku kodu. Na przykład w poniższym kodzie zastosowano dyrektywę use namespace w celu otwarcia przestrzeni nazw AS3 zarówno dla metody pop() , jak i metody push() :

use namespace AS3; 
 
var nums:Array = new Array(1, 2, 3); 
nums.pop(); 
nums.push(5); 
trace(nums) // output: 1,2,5

Środowisko ActionScript 3.0 udostępnia opcje kompilatora właściwe dla każdego zestawu właściwości, pozwalając na zastosowanie przestrzeni nazw AS3 do całego programu. Opcja kompilatora -as3 oznacza przestrzeń nazw AS3, a opcja kompilatora -es oznacza dziedziczenie w oparciu o prototypów (skrót es pochodzi od nazwy ECMAScript). Aby otworzyć przestrzeń nazw AS3 dla całego programu, należy ustawić opcję kompilatora -as3 na true , a opcję kompilatora -es na false . Aby stosować wersje prototypowe, należy ustawić opcje kompilatora na przeciwne wartości. Domyślna konfiguracja kompilatora w programach Flash Builder i Flash Professional to: -as3 = true oraz -es = false .

Jeśli planowane jest rozszerzanie klas podstawowych języka i przesłanianie metod, należy wziąć pod uwagę wpływ przestrzeni nazw AS3 na wymagany sposób deklarowania metod przesłoniętych. Jeśli używana jest przestrzeń nazw AS3, każda metoda przesłaniająca metodę klasy podstawowej języka musi również deklarować przestrzeni nazw AS3 wraz z atrybutem override . Jeśli przestrzeń nazw AS3 nie jest używana i planowane jest przedefiniowanie metody klasy podstawowej w podklasie, nie należy deklarować przestrzeni nazw AS3 ani słowa kluczowego override .