Rzut oka na BOOPSI

From MorphOS Library

Revision as of 09:36, 17 January 2011 by Krashan (talk | contribs) (Klasy: Translation in progress.)

Grzegorz Kraszewski



Ten artykuł w innych językach: angielski

Programowanie zorientowane obiektowo

Programowanie zorientowane obiektowo jest sposobem tworzenia programów, jaki powstał w odpowiedzi na dwie tendencje na rozwijającym się rynku oprogramowania. Po pierwsze zarządzanie tradycyjnie napisanym kodem staje się coraz trudniejsze w miarę zwiększania się jego rozmiarów. Po drugie, w latach osiemdziesiątych ubiegłego wieku nastąpił lawinowy rozwój graficznych interfejsów użytkownika, co oznaczało koniec programów wykonujących się w z góry zdefiniowanej sekwencji. Nowoczesne programy są sterowane zdarzeniami. Oznacza to, że tok wykonywania programu jest określony przez zdarzenia zewnętrzne (najczęściej wywoływane przez użytkownika) i nie jest znany w czasie pisania programu. Programowanie obiektowe dzieli program na zbiór obiektów oddziałujących na siebie poprzez dokładnie zdefiniowane zestawy metod i atrybutów. Wprowadzona w ten sposób modułowość ułatwia zapanowanie nad projektem i w naturalny sposób pasuje do koncepcji nowoczesnego interfejsu użytkownika. Elementy GUI (zwane w MUI "gadżetami") są po prostu obiektami wymieniającymi informacje z innymi obiektami reprezentującymi dane użytkownika.

To krótkie wprowadzenie nie jest oczywiście kompletnym wykładem z programowania obiektowego. Z drugiej strony do zaznajomienia się z BOOPSI nie jest wymagana znajomość jakiegokolwiek obiektowo zorientowanego języka programowania. Zazwyczaj to język programowania daje programiście do ręki podstawowe konstrukcje obiektowe. Taki język jest albo obiektowy od podstaw (np. C++, C#, Java) albo ma obiektowość doklejoną w mniej lub bardziej logiczny sposób (Objective C, PHP). Inaczej jest w przypadku BOOPSI i MUI. Tutaj konstrukcje wspierające obiektowość dostajemy od systemu operacyjnego. Dzięki temu możemy używać MUI w dowolnym języku programowania, również tradycyjnym, jak np. stare dobre C, a nawet asembler.

Moduł BOOPSI znajduje się w bibliotece intuition.library, niektóre ważne funkcje znajdziemy w statycznej bibliotece libabox. Pierwotnym zadaniem BOOPSI było "zapakowanie w obiekty" elementów GUI tworzonych przez Intuition. Takie podejście okazało się jednak za mało elastyczne, dlatego MUI używa BOOPSI wyłącznie jako podstawę programowania obiektowego, tworząc własne elementy GUI i wykorzystując Intuition jedynie do podstawowych rzeczy (np. tworzenie okien). BOOPSI tworzy cztery podstawowe pojęcia programowania obiektowego: klasy, obiekty, metody i atrybuty. Obsługuje również dziedziczenie klas. Dzięki swojej prostocie BOOPSI jest łatwe w użyciu, szczególnie w porównaniu do bardziej zaawansowanych platform obiektówych, takich jak np. język C++.

Klasy

Klasa jest podstawowym pojęciem programowania obiektowego. Jest ona kompletnym opisem swoich obiektów, definiuje ich metody i atrybuty. W BOOPSI klasa składa się z:

  • Struktury IClass. Do klasy odwołujemy się zawsze poprzez wskaźnik na tę strukturę. Struktura IClass jest zdefiniowana w systemowym pliku nagłówkowym <intuition/classes.h>. Znajduje się tam również definicja typu Class, który jest tożsamy z struct IClass.
  • Funkcji, tzw. dispatchera. Gdy program wykonuje metodę na obiekcie, wywoływany jest dispatcher klasy obiektu. Na podstawie identyfikatora metody dispatcher wykonuje skok do metody. Jest on zwykle implementowany w postaci dużego bloku switch. W przypadku małych klas zawierających jedynie kilka krótkich metod, kod metod jest często umieszczany bezpośrednio w poszczególnych wyrażeniach case. Większe klasy mają metody w oddzielnych funkcjach umieszczonych poza dispatcherem. Ponieważ każda metoda przechodzi przez dispatcher, wszystkie metody BOOPSI są wirtualne w sensie C++. Z tego powodu wywołanie metody w BOOPSI jest zwykle wolniejsze niż w C++.

Zestaw metod klasy (metod, jakie można wykonywać na jej obiektach) jest określony przez zestaw wyrażeń case dispatchera. Atrybuty obiektu są ustawiane przy użyciu metody OM_SET(), a odczytywane metodą OM_GET(). Atrybuty mogą być również przekazane bezpośrednio do konstruktora obiektu. Zestaw atrybutów klasy, oraz możliwość ich odczytu, zapisu i użycia przy inicjacji obiektu są zatem zdefiniowane przez kod metody OM_GET() (odczyt), OM_SET() (zapis) i OM_NEW() (konstruktor, a więc inicjacja). W BOOPSI nie występuje formalna deklaracja klasy, nie ma też podziału między metody i atrybuty publiczne i prywatne. Pewną imitację deklaracji i poziomy dostępu można wprowadzić umieszczając każdą klasę w odrębnym pliku źródłowym. Towarzyszący mu plik nagłówkowy zawiera wtedy definicje identyfikatorów metod i atrybutów i struktur parametrów metod, ale tylko te uważane za "publiczne". Identyfikatory metod prywatnych umieszcza się wewnątrz pliku z kodem klasy, nie są więc formalnie dostępne na zewnątrz.

Klasa BOOPSI może być współdzielona między programami. Wszystkie klasy wbudowane w MUI są klasami współdzielonymi. BOOPSI tworzy systemową listę klas publicznych (listę tę można przeglądać np. za pomocą monitora systemu Scout). Klasy publiczne są identyfikowane przez nazwę. Część klas standardowych MUI znajduje się w jego głównej bibliotece – muimaster.library. Biblioteka dodaje te klasy do listy klas publicznych, kiedy zostaje załadowana do pamięci przy pierwszym otwarciu. Pozostałe standardowe klasy MUI są umieszczone na partycji systemowej dysku w katalogu MOSSYS:Classes/MUI/. Dodatkowe klasy, pisane przez niezależnych programistów, powinny być instalowane w katalogu SYS:Classes/MUI/.

Shared classes use the MorphOS shared library framework, in other words a shared BOOPSI class is just a kind of shared library. The class adds itself to the public list of classes, when it is opened from disk. As such, a BOOPSI shared class should be opened with OpenLibrary() before use (see details), especially as BOOPSI classes are usually not included into the list of libraries opened automatically. This is not the case for MUI classes however. MUI shared classes can be used without opening them. It is explained below, in the MUI Extensions to BOOPSI section.

Metody i atrybuty

Metody

Methods are just actions, which can be performed on an object. A set of available methods is defined by the object's class. Technically speaking, a method is a function called with an object as its parameter in order to change the object's state. In BOOPSI, methods are called using the DoMethod() call from libabox:

result = DoMethod(object, method_id, ... /* method parameters */);
result = DoMethodA(object, method_struct);

The first, more popular form of the call just builds the method structure on the fly, from arguments passed to it. Any method structure always has the method identifier as the first field. The DoMethodA() call gets a pointer to the method structure, the structure is built by the application. The second form is rarely used. The number and meaning of parameters, as well as the meaning of the result are method specific. Comparison of executing a method with both forms of the call is given below:

struct MUIP_SomeMethod
{
  ULONG MethodID;
  LONG ParameterA;
  LONG ParameterB;
};

DoMethod(object, MUIM_SomeMethod, 3, 7);

struct MUIP_SomeMethod mparams = { MUIM_SomeMethod, 3, 7 };
DoMethodA(object, &mparams);

The DoMethod() form is more convenient, so it is commonly used. MUI uses specific prefixes for all its structures and constants:

  • MUIM_ for method identifiers.
  • MUIP_ for method parameter structures.
  • MUIA_ for attribute identifiers.
  • MUIV_ for special, predefined attribute values.


The C types used in the method structure above may need some explanation. LONG is a 32-bit signed integer, ULONG is an unsigned one. Because the structure is usually built on the processor stack, all parameters are extended and aligned to 32 bits. Then every parameter in the structure must be defined either as a 32-bit integer or a pointer. Any parameter larger than 32 bits must be passed via pointer (for example double precision floats or strings).

Ustawianie wartości atrybutu

An object's attributes represent its properties. They are written and read using special methods, OM_SET() and OM_GET() respectively. This differs from most object oriented programming languages, where attributes (being implemented as an object's fields) are accessed directly. Manipulating attributes in BOOPSI is slower then, as it implies performing a method.

The OM_SET() method does not take a single attribute and its value, but a taglist of them, so one can set multiple attributes at once. The setting of two attributes to an object may be done as follows:

struct TagItem attributes[] = {
  { MUIA_SomeAttr1, 756 },
  { MUIA_SomeAttr2, 926 },
  { TAG_END, 0 }
};

DoMethod(object, OM_SET, (ULONG)attributes);

However, this is cumbersome and the code is not easily readable. The intuition.library makes it easier by providing the SetAttrsA() function, which is a wrapper for the OM_SET() method. Using this function and the array defined above, one can write:

SetAttrsA(object, attributes);

It still requires definition of a temporary taglist, but the function also has a variadic (meaning it can take a variable number of arguments) form SetAttrs(), which allows for building the taglist on-the-fly:

SetAttrs(object,
  MUIA_SomeAttr1, 756,
  MUIA_SomeAttr2, 926,
TAG_END);

This is not all however. Programmers are lazy and decided that in the common case of setting a single attribute, SetAttrs() is still too much typing. A common practice found in sources using MUI was to define an xset() or set() macro, which is now defined in the system headers, in the <libraries/mui.h> file.

#define set(object, attribute, value) SetAttrs(object, attribute, value, TAG_END)

Then, setting a single attribute can be coded as follows:

set(object, MUIA_SomeAttr1, 756);

The OM_SET() method returns the number of attributes applied to the object. If some attributes are not known to the object's class (and superclasses), they are not counted. This return value is usually ignored, it may be used for testing an attribute applicability.

MUI provides a few additional methods for setting attributes, namely MUIM_Set(), MUIM_NoNotifySet() and MUIM_MultiSet(). They are mainly used in notifications.

Odczytywanie wartości atrybutu

The OM_GET() method gets a single attribute from an object. There is no multiple attributes getting method. Its first, obvious parameter is the attribute identifier. The attribute value is not returned as the result of the method however. Instead the second parameter is a pointer to a memory area, where the value is to be stored. This allows for passing attributes larger than 32 bits, they are just copied to the pointed memory area. This only works for fixed size attributes. Text strings cannot be passed this way, so they are passed as pointers (a pointer to the string is stored at a place in memory pointed to by the second parameter of OM_GET()). The three examples below demonstrate all three cases:

LONG value1;
QUAD value2;    /* 64-bit signed integer */
STRPTR *value3;

DoMethod(object, OM_GET, MUIA_Attribute1, (ULONG)&value1);  /* integer attr */
DoMethod(object, OM_GET, MUIA_Attribute2, (ULONG)&value2);  /* fixed size big attr */
DoMethod(object, OM_GET, MUIA_Attribute3, (ULONG)&value3);  /* string attr */

In cases when an attribute is returned by pointer, the data pointed to should be treated as read-only unless documented otherwise.

Similarly as for OM_SET(), there is a wrapper function for OM_GET() in the intuition.library, named GetAttr(). This function unexpectedly changes the order of arguments: attribute identifier is the first, object pointer is the second. The three examples above may be written with GetAttr() as follows:

GetAttr(MUIA_Attribute1, object, &value1);
GetAttr(MUIA_Attribute2, object, (ULONG*)&value2);
GetAttr(MUIA_Attribute3, object, (ULONG*)&value3);

The third parameter, a storage pointer is prototyped as pointer to ULONG, so in the first example type casting is not needed.

The <libraries/mui.h> system header file defines a macro get(), which reverses the order of the two first arguments of GetAttr() and adds the typecasting to ULONG*. The order of arguments of get() is the same as for set(), which helps to avoid mistakes. The third line of the above example may be rewritten with get() this way:

get(object, MUIA_Attribute3, &value3);

The most often used attributes are integers (32-bit or shorter) and strings. Both of them fit into a 32-bit variable, as strings have to be passed via pointers. Taking this into account, MUI programmers invented a function (sometimes defined as a macro), which just returns the attribute value instead of storing it at a specified address. The function is named xget() and works as shown below:

value1 = xget(object, MUIA_Attribute1);
/* MUIA_Attribute2 can't be retrieved with xget() */
value3 = (STRPTR)xget(object, MUIA_Attribute3);

The xget() function may be defined in the following way:

inline ULONG xget(Object *obj, ULONG attribute)
{
  ULONG value;

  GetAttr(attribute, object, &value);
  return value;
}

The function is very simple and is compiled to a few processor instructions. That is why it is declared as inline, which causes the compiler to insert the function's code in-place instead of generating a jump. This makes the code faster, albeit a bit bigger. Except for working only with 32-bit attributes, xget() also has the disadvantage of loosing the OM_GET() return value. The value is boolean and is TRUE if the object's class (or any of its superclasses) recognizes the attribute, FALSE otherwise. This value is usually ignored, but may be useful for scanning objects for supported attributes.

The xget() function is not defined in the system headers. It has been described here because of its common use in MUI applications. Its counterparts for bigger sized arguments may be defined if needed.


Konstrukcja obiektu

Having a class, the programmer can create an unlimited number of objects (instances) of this class. Every object has its own instance data area, which is allocated and cleared automatically by the BOOPSI. Of course only object data are allocated for each instance. The code is not duplicated, so it must be reentrant (static variables and code self-modification must not be used).

Objects are created and disposed with two special methods: the constructor, OM_NEW() and the destructor, OM_DISPOSE(). Of course the constructor method cannot be called on an object, because it creates a new one. It needs a pointer to the object's class instead, so it cannot be invoked with DoMethod(). The intuition.library provides NewObjectA() and NewObject() functions for calling the constructor. The difference between them is that NewObjectA() takes a pointer to a taglist specifying initial values for objects. NewObject() allows the programmer to build this taglist from a variable number of function arguments.

NewObject[A]() has two alternative ways of specifying the created object's class. Private classes are specified by pointers of Class type. Shared classes are specified by name, which is a null-terminated string. If the pointer is used for class specification, the name should be NULL, if a name is used, the pointer should be NULL. Four examples below show creating instances of private and public class with both NewObjectA() and NewObject():

Object *obj;
Class *private;

struct TagItem initial = {
  { MUIA_Attribute1, 4 },
  { MUIA_Attribute2, 46 },
  { TAG_END, 0 }
};

A private class, NewObjectA():

obj = NewObjectA(private, NULL, initial);

A private class, NewObject():

obj = NewObject(private, NULL,
  MUIA_Attribute1, 4,
  MUIA_Attribute2, 46,
TAG_END);

A public class, NewObjectA():

obj = NewObjectA(NULL, "some.class", initial);

A public class, NewObject():

obj = NewObject(NULL, "some.class",
  MUIA_Attribute1, 4,
  MUIA_Attribute2, 46,
TAG_END);

NewObject[A]() returns NULL in case of an object creation failure. Usual reasons are: wrong class pointer/name, lack of free memory, wrong/missing initial values of attributes. The return value of NewObject[A]() should always be checked in the code.


Destrukcja obiektu

The OM_DISPOSE() method is used to destroy an object. Unlike OM_NEW() the destructor may be invoked with DoMethod():

DoMethod(object, OM_DISPOSE);

The intuition.library has a wrapper for this however, named DisposeObject():

DisposeObject(object);


Jak MUI rozszerza BOOPSI

The Magic User Interface not only builds on BOOPSI but also extends it. Other than providing a broad set of classes, MUI also modifies the BOOPSI mode of operation a bit. Two modifications are discussed in this chapter: extension of the IClass structure and MUI's own functions for object construction and destruction.

MUI uses the MUI_CustomClass structure for its internal class representation. This structure contains the standard Class structure inside. It is important when creating objects from MUI private classes with NewObject(), that the Class structure must be extracted from the MUI_CustomClass structure:

struct MUI_CustomClass *priv_class;
Object *obj;

obj = NewObject(priv_class->mcc_Class, NULL, /* ... */ TAG_END);

MUI's second modification of BOOPSI is using its own functions for object construction and destruction, MUI_NewObject[A]() and MUI_DisposeObject() respectively. These two functions are used only for objects of MUI shared (public) classes. Objects of private classes are created with NewObject() as shown above. The main advantage of MUI_NewObject() is automatic opening and closing of disk based shared classes. Here is an example:

Object *text;

text = MUI_NewObject(MUIC_Text, MUIA_Text_Contents, "foobar", TAG_END);

'MUIC_Text is a macro defined in <libraries/mui.h> and it expands to "Text.mui" string. All MUI public classes should be referenced by their MUIC_ macros rather than by direct string literals. It helps to detect mistyped class names, as a typo in a macro will be detected during compilation. The MUI checks if a class named Text.mui has been added to the public list of classes. If not, the class is found on disk, opened and used for creating the requested object. Closing the class when no longer in use is handled automatically too. All MUI objects should be disposed using MUI_DisposeObject(), which takes the object to be disposed as its only argument, the same as DisposeObject().

MUI_DisposeObject(text);