Cele i zasady tworzenia klas pochodnych

From MorphOS Library

Grzegorz Kraszewski


Ten artykuł w innych językach: angielski

Wprowadzenie

Tworzenie klas pochodnych to jedna z podstawowych technik programowania obiektowego. W MUI najczęściej używamy klas pochodnych do następujących celów:

  • Umieszczenie całej funkcjonalności programu w zestawie metod, które mogą być potem wykonywane w notyfikajach. Do tego celu najczęściej służy klasa pochodna klasy Application.
  • Dostosowywanie klas standardowych do potrzeb programu przez przeciążanie specjalnie do tego przeznaczonych metod. Typowym przypadkiem są klasy pochodne od List, Numericbutton czy Popstring.
  • Tworzenie gadżetów i obszarów z własnymi procedurami rysowania się oraz obsługiwania zdarzeń zewnętrznych (mysz, klawiatura). W tym celu tworzy się klasy pochodne od klasy Area.

Niezależnie od powodu tworzenia klas pochodnych, robi się to zawsze w ten sam sposób. Programista musi napisać nowe metody lub przeciążyć istniejące, stworzyć funkcję dispatchera, zdefiniować strukturę danych obiektu (bywa, że jest ona pusta) i w końcu stworzyć klasę. Warto zauważyć, że klasy pochodne w MUI robi się tak samo jak w BOOPSI. Jedyną różnicą jest to, że MUI posiada własne funkcje do tworzenia i usuwania klas.


Dane obiektu

Dane każdego obiektu są przechowywane w obszarze automatycznie przydzielanym przez system. Obszaru tego używa się do przechowywania aktualnych wartości atrubutów, oraz do zmiennych i buforów wewnętrznych, repezentujących stan obiektu. Jest on zwykle definiowany jako struktura. Rozmiar danych obiektu jest podawany jako argument funkcji MUI_CreateCustomClass(). W hierarchii klas, każda klasa może dodać swoją część do obszaru danych obiektu. Jednakże w przeciwieństwie do C++, każda klasa ma dostęp jedynie do swojej części danych. Kod klasy nie może sięgnąć do danych klasy nadrzędnej (W C++ jest to możliwe, jeżeli pole danych ma deklarację protected, albo public). Dane klas nadrzędnych są dostępne wyłącznie poprzez atrybuty i metody zdefiniowane w tych klasach.

Z powodu wewnętrznych ograniczeń BOOPSI, rozmiar obszaru danych obiektu dla danej klasy nie może przekraczać 64 kB. Duże bufory i tablice powinny być w razie potrzeby alokowane dynamicznie w konstruktorze i zwalniane w destruktorze. Obszar danych obiektu jest zawsze kasowany przez BOOPSI zerami przed przekazaniem obiektowi do użytku. Jeżeli klasa nie potrzebuje danych obiektu, może jako rozmiar bloku danych podać zero funkcji MUI_CreateCustomClass().


Pisanie metod

Metoda MUI jest zwykłą funkcją w C, jednakże zestaw argumentów i ich typów jest częściowo stały.

IPTR NazwaMetody(Class *cl, Object *obj, TypStruktury *msg);

Rezultat metody może być albo liczbą całkowitą, albo wskaźnikiem na cokolwiek. Dlatego rezultat ten jest typu IPTR, który jest zdefiniowany jako "liczba całkowita wystarczająco duża, aby zmieścił się w niej wskaźnik". W aktualnej wersji MorphOS-a jest to po prostu liczba 32-bitowa (tak samo jak LONG). Jeżeli metoda nie ma nic sensownego do zwrócenia jako wynik, najlepiej po prostu zwrócić zero. Dwa pierwsze, stałe argumenty to: wskaźnik na klasę obiektu, oraz wskaźnik na sam obiekt. Ostatni argument to struktura parametrów metody. Jeżeli metoda jest przeciążeniem metody klasy nadrzędnej, to typ struktury parametrów określony jest przez klasę nadrzędną. Dla nowych metod typ tej struktury jest określany przez programistę. Niektóre metody mogą posiadać puste struktury parametrów (zawierające wyłącznie identyfikator metody). W tym przypadku trzeci argument może być pominięty.

Większość metod potrzebuje dostępu do danych obiektu. Do otrzymania wskaźnika na te dane używa się makra INST_DATA, zdefiniowanego w pliku <intuition/classes.h>. Oto przykład użycia tego makra:

struct ObjData
{
  LONG JakasWartosc;
  /* ... */
};

IPTR JakasMetoda(Class *cl, Object *obj)
{
  struct ObjData *d = (struct ObjData*)INST_DATA(cl, obj);

  d->JakasWartosc = 14;
  /* ... */
  return 0;
}

Jeżeli metoda jest dziedziczona z klasy nadrzędnej, może być w kodzie metody potrzebne wywołanie tej metody na klasie nadrzędnej. Takie wywołanie nigdy nie jest wykonywane niejawnie. Metoda klasy nadrzędnej musi być zawsze wywołana funkcją DoSuperMethodA():

result = DoSuperMethodA(cl, obj, msg);
result = DoSuperMethod(cl, obj, identyfikator, ...);

Druga forma tej funkcji pozwala na rekonstrukcję struktury parametrów metody z argumentów funkcji. Jest używana, jeżeli parametry metody są modyfikowane przed wywołaniem tej metody w klasie nadrzędnej. Metoda klasy nadrzędnej może być wywołana w dowolnym miejscu metody klasy podrzędnej, może też nie zostać wywołana wcale. Dla standardowych klas i metod MUI zasady wywoływania metody klasy nadrzędnej są opisane w dokumentacji a także innych rozdziałach tego przewodnika. Dla metod nowo zdefiniowanych przez programistę, kwestia wywoływania metod klas nadrzędnych jest całkowicie pozostawiona do jego uznania.


Dispatcher

Funkcja dispatchera klasy pełni rolę tablicy skoków do metod. Gdy jakakolwiek metoda zostanie wywołana na obiekcie (za pomocą DoMethod()), BOOPSI odszukuje dispatchera klasy obiektu i wywołuje go. Dispatcher sprawdza identyfikator metody (identyfikator jest zawsze pierwszym polem struktury parametrów metody) i wywołuje odpowiadającą mu metodę. Jeżeli metoda jest nieznana w danej klasie, dispatcher powinien przekazać ją klasie nadrzędnej poprzez funkcję DoSuperMethod().

Dispatcher jest szczególnym przypadkiem hooka. Pozwala to na wywoływanie go z dowolnego języka programowania, ponieważ sposób przekazania argumentów jest uniezależniony od C/C++. Wadą tego rozwiązania jest przekazywanie argumentów hooka poprzez wirtualne rejestry emulowanego procesora m68k. Z drugiej strony pozwala to na wykorzystanie natywnych klas skompilowanych dla PowerPC przez stare programy m68k, podobnie nowe natywne oprogramowanie może skorzystać ze starszych klas MUI pisanych jeszcze dla m68k. Jako że dispatcher jest hookiem, wymaga zdefiniowania odpowiadającej mu struktury EmulLibEntry zdefiniowanej w pliku nagłówkowym <emul/emulinterface.h>. Struktura ta funkcjonuje jako "bramka" dla agrumentów przekazywanych między natywnym kodem PowerPC, a emulatorem m68k.

const struct EmulLibEntry ClassGate = {TRAP_LIB, 0, (void(*)(void))ClassDispatcher};

Sam dispatcher zdefiniowany jest następująco:

IPTR ClassDispatcher(void)
{
  Class *cl = (Class*)REG_A0;
  Object *obj = (Object*)REG_A2;
  Msg msg = (Msg)REG_A1;

  /* ... */

Argumenty dispatchera są takie same jak argumenty metody, ale są przekazywane przez wirtualne rejestry procesora m68k, zamiast być po prostu argumentami funkcji. Jako wskaźnik na dispatcher funkcja MUI_CreateCustomClass() dostaje adres bramki danych (EmulLibEntry). Bramka danych jest używana nawet wtedy, kiedy natywna aplikacja PowerPC wywołuje również natywną klasę MUI. Wprowadzane przez bramkę danych opóźnienie jest jednak pomijalne. Wielu programistów używa prostych makr do "ukrycia" w kodzie powyższych detali, nie zostały one tu jednak użyte dla lepszego zrozumienia zagadnienia.

Typ Msg jest typem bazowym dla wszystkich struktur parametrów metod. Definiuje on strukturę zawierającą tylko identyfikator metody typu ULONG. Wszystkie pozostałe parametry metody muszą mieć adresy wyrównane zgodnie z wymaganiami stosu procesora, ponieważ funkcja DoMethod() buduje strukturę parametrów na stosie. Dlatego każdy parametr metody musi być zdefiniowany albo jako wskaźnik, albo IPTR, jeżeli jest liczbą.

Po otrzymaniu argumentów dispatcher sprawdza identyfikator metody i skacze do niej. Najczęściej robi się to w instrukcji switch. W przypadku małej ilości metod można też się posłużyć kaskadowym if/else if. Oto typowy przykład:

switch (msg->MethodID)
{
  case OM_NEW:            return MyClassNew(cl, obj, (struct opSet*)msg);
  case OM_DISPOSE:        return MyClassDispose(cl, obj, msg);
  case OM_SET:            return MyClassSet(cl, obj, (struct opSet*)msg);
  case OM_GET:            return MyClassGet(cl, obj, (struct opGet*)msg);
  case MUIM_Draw:         return MyClassDraw(cl, obj, (struct MUIP_Draw*)msg);
  case MUIM_AskMinMax:    return MyClassAskMinMax(cl, obj, (struct MUIP_AskMinMax*)msg);
  /* ... */
  default:                return DoSuperMethodA(cl, obj, msg);
}

Dla każdej metody wskaźnik na strukturę parametrów jest rzutowany na typ struktury parametrów tej konkretnej metody. Są programiści, którzy umieszczają kod metod bezpośrednio wewnątrz wyrażenia switch, zwłaszcza gdy metod jest mało, a ich kod jest krótki. W powyższym przykładzie przeciążono kilka metod klasy Area. Sposób nazywania funkcji implementujących metody jest wyłącznie przykładowy, nie ma tu żadnych narzuconych konwencji. Jednakże dodawanie nazwy klasy do nazw funkcji ma tę zaletę, że nie występują konflikty nazw między różnymi klasami jeżeli funkcje metod nie są zadeklarowane jako statyczne.


Tworzenie klasy

Jeżeli wszystkie niezbędne elementy (metody, dispatcher, bramka danych, struktura danych obiektu) są przygotowane, można stworzyć z nich kompletną klasę MUI.

struct MUI_CustomClass *MyClass;

MyClass = MUI_CreateCustomClass(NULL, MUIC_Area, NULL, sizeof(struct MyClassData),
 (APTR)&MyClassGate);

Pierwszym argumentem MUI_CreateCustomClass() jest baza biblioteki, o ile tworzona klasa będzie publiczna. Pisanie klas publicznych będzie omówione w jednym z dalszych artykułów. Na razie powiedzmy jedynie, że klasy publiczne są implementowane jako biblioteki współdzielone, więc każda taka klasa ma bazę biblioteki. Dla klas prywatnych ten argument powinien być zawsze NULL-em.

Dwa kolejne argumenty określają klasę nadrzędną tworzonej klasy. Są one używane zamiennie, jeżeli klasa nadrzędna jest prywatna, to odwołujemy się do niej przez wskaźnik (argument 3), jeżeli publiczna – to przez nazwę (argument 2). W każdym przypadku drugi, nieużywany argument tej pary powinien mieć wartość NULL. Przykład powyżej zakłada tworzenie klasy pochodnej od klasy publicznej, jest to przypadek najczęstszy. Większe programy mogą mieć bardziej rozbudowane hierarchie klas i wtedy zdarza się dziedziczenie po klasie prywatnej.

Czwarty argument określa rozmiar danych obiektu w bajtach dla tworzonej klasy. Ponieważ z reguły dane obiektu są zdefiniowane jako struktura, wystarczy podać jej rozmiar, korzystając z operatora sizeof(). Jeżeli klasa nie potrzebuje w ogóle żadnych danych obiektu, można podać tu 0.

Ostatni argument to adres struktury EmulLibEntry, czyli bramki danych. Programiści z doświadczeniem w AmigaOS na procesorach m68k na pewno zauważą tu różnicę, w tamtym systemie podawało się tu po prostu adres funkcji dispatchera. Jak wspomniano wyżej, bezproblemowa współpraca kodu dla PowerPC i m68k wymaga, aby program zawsze przechodził przez bramkę danych przechodząc z kodu systemu do dispatchera. Dlatego argumentem funkcji MUI_CreateCustomClass() jest adres bramki, a dopiero w niej znajduje się adres funkcji dispatchera.


Usuwanie klasy

Klasę MUI usuwa się za pomocą funkcji MUI_DeleteCustomClass().

if (MyClass) MUI_DeleteCustomClass(MyClass);

Przed usunięciem klasy muszą być spełnione dwa warunki:

  • Klasa powinna być usuwana tylko w przypadku, kiedy została stworzona. Wywołanie MUI_DeleteCustomClass() ze wskaźnikiem zerowym może skutkować zawieszeniem programu (stąd sprawdzenie wskaźnika w przykładzie).
  • Nie należy usuwać klasy, jeżeli istnieją w systemie jej klasy pochodne bądź obiekty. Najlepszą praktyką jest tworzenie klas przed stworzeniem interfejsu graficznego programu, a usuwanie ich po końcowym MUI_DisposeObject() usuwającym obiekt aplikacji. Funkcja MUI_DeleteCustomClass() sprawdza istnienie obiektów i klas podrzędnych i zwraca wartość logiczną FALSE jeżeli takowe istnieją i klasa nie może być usunięta.