Kapitel 5: Grundlagen von Vererbung

5.1 Rueckblick
Bis hierhin sollten die Funktionsweisen von Funktionen bekannt sein. Es sollte
bekannt sein, wie Funktionen deklariert und aufgerufen werden. Zusaetzlich
ist bekannt, was Definitionen von Funktionen und Variablen sind und was sie
bewirken. Eine Funktion beinhaltet einen Block, in dem die Anweisungen etc
eingefuegt werden. void-Funktionen haben keine return-Wert.

Bevor wir in die Vererbung gehen, noch ein paar Punkte zu den
Preprozessor-Direktiven:
Der Preprozessor ist ein Programm, dass den Quelltext, der uebersetzt werden
soll, vorbereitet, damit der uebersetzt werden kann.
Beispielsweise entfernt er Leerzeilen, Einrueckungen, Kommentare etc.
Ausserdem fuehrt er verschiedene Befehle, die ganz oben in jedem Objekt
stehen, aus.

Alle Preprozessordirektiven beginnen mit einem '#'.
Die wichtigste Direktive ist:
#pragma
Der Quelltext wird nach bestimmten vorgegebenen Richtlinien ueberprueft.
Das wichtigste (und restriktivste) #pragma ist das
#pragma strong_types
Damit wird ueberprueft, ob (wie in Kapitel 4 beschrieben) die Funktionen
richtig deklariert wurden und auch logische return-Werte vergeben wurden.
Im Tamedhon ist es festgelegt, dass jedes Objekt, dass in die Spielwelt
eingehen soll, so definiert ist.

Die Direktive:
#include
wird verwendet, um Headerdateien, also Dateien, die Definitionen enthalten,
die das Programmieren erleichtern (Beispiele: Properties), einzubinden.
Diese Bibliotheksdateien (heissen Headerdateien, da sie in den Headern der
Objekte aufgerufen werden) stehen dann bei der Compilierung zur Verfuegung.
Es gibt zwei Moeglichkeiten:
1) absolute Pfade:
#include 
Damit wird die Headerdatei properties.h, die an einem fest definierten Ort,
wo die meisten allgemeinen Headerdateien liegen (das ist normalerweise /sys/
im MUD), liegt, eingebunden.
2) relative Pfade:
#include "../def.h"
Damit wird die (selbsterstellte) Headerdatei def.h eingebunden, die eine
Ebene hoeher als das aufrufende Objekt (normalerweise das Projektverzeichnis)
liegt, eingebunden.

Als letzte wichtige Direktive ist die Direktive:
#define
zu nennen.
Damit werden konstante Ersetzungen fuer den spaeter folgenden Quellcode
definiert.
So kann man zB solche auf den ersten Blick aufwaendige Konstruktionen wie
capitalize(this_player()->name()) einfach ersetzen, indem man schreibt:
#define CTP capitalize(this_player()->name())
Das macht den Code zwar schwerer lesbar, ist aber, wenn man sich an bestimmte
Regelungen haelt, das Programmieren einfacher.
Ich persoenlich halte davon meist wenig, man sollte es nur einsetzen, um
absolute Pfadangaben zu uebersetzen und die #define's dann auch in eine
Extra-Datei, da das die Wartung erleichtert.
Denn wenn ein LPC-Projekt, das ja oft eine Subregion umfasst (also eine Stadt,
einen Weg etc), aus hunderten Dateien besteht, muesste man, so man dieses
Projekt verschieben, wollte, alle diese Dateien aendern.
Durch solche Defines, die meist so aussehen:
#define ROOM "/d/steppe/gralkor/ergoch/room/"
wird die Wartung erleichtert. Damit muss naemlich nur die def.h geaendert
werden.
Und wenn man fremden Code analysiert ist man dem Coder dankbar, wenn er
moeglichst wenige #define's verwendet hat.
Frage:
Was macht die obere Konstruktion eigentlich (rein technisch).
Hinweis: -> ist ein Ausfuehrungsoperator. Damit wird in dem Objekt, das im
return-Wert (und das geht nur mit Objekten!, dazu spaeter) der aufrufenden
Funktion steht, die Funktion nach dem Operator ausgefuehrt.

Dieser so vorbereitete Quellcode wird dann vom Compiler uebersetzt.

Aber zurueck zum Hauptthema:

Wenn Du Dir Quelldateien ansiehst, wirst Du normalerweise auf Code wie DIESEN
stossen:

inherit "/std/room";

#pragma strong_types
#include 

void create()
{
 ::create();

 SetProp(P_LIGHT,1);
 SetProp(P_INT_SHORT,"Ein Testraum");
 SetProp(P_INT_LONG,"Du stehst in einem Testraum.");
 ...
 AddExit("norden","/gilden/abenteurer");
}

Falls Du wirklich jeden Punkt bis hierher verstanden haben solltest, sind
folgende Punkte klar:
1) create() ist die Definition einer Funktion
2) Sie ruft die Funktionen SetProp() und AddExit() auf
3) Der Preprozessor soll die Headerdatei /sys/properties.h zur Vorbereitung
heranziehen
4) Da sind zwei Zeilen, die voellig unlogisch sind:
4.1) inherit "/std/room";
4.2) ::create();

Dieses Kapitel will die Antwort auf folgende Fragen finden:
1) Wieso wurde create() nicht deklariert?
2) Wo sind die Funktionen SetProp() und AddExit() deklariert und definiert?
3) Was macht dieses inherit?

5.2 Objektorientiertes Programmieren (OOP)
Vererbung ist eine der Faehigkeiten, die objektorientiertes Programmieren (OOP)
ausmachen. Denn dadurch kannst Du generischen (also Standard) Code produzieren,
der in vielen verschiedenen Arten in vielen verschiedenen Programmen verwendet
werden kann.
Der Sinn der MUDlib ist, dass dort viele dieser generischen Objekte existieren,
die von anderen Objekten verwendet werden, um spezialisierte Objekte zu werden.

Muesstest Du den Code, der die Funktionalitaet, die der oben zitierte Raum
besitzt, nachprogrammieren wolltest, muesstest Du zwischen 2000 und 3000 Zeilen
schreiben. Und das ist natuerlich eine voellig sinnlose Platzverschwendung.
Zusaetzlich wuerde solch Code nicht gut mit anderen Raeumen und Spielern
interagieren, da jeder Programmierer einen anderen Stil hat, und verschiedene
Funktionalitaeten anders implementiert. Ein Programmierer nennt die
Funktion, die die Langbeschreibung beinhaltet, long().
Ein Anderer nennt sie lieber langbeschreibung(). Dann gibt es noch den
kreativen Magier, der sie langbeschreibung_von_raum_a24() nennt.
Das ist ein Grund, warum MUDLIBS nicht kompatibel sind, da sie unterschiedliche
Protokolle der Objektinteraktion haben.

Mit OOP umgeht man dieses Problem. In dem oben zitierten Raum erbt das Objekt
die Funktionen, die bereits in dem Objekt "/std/room.c" definiert sind.
Es hat alle Faehigkeiten, die ein Raum haben muss.
Dort sind Funktionen, wie man den Raum betritt, was dort passieren kann etc.
bereits definiert und initialisiert.
Da jeder Raum diesen Raum als Vorlage nimmt, verhalten sich alle Raeume gleich.
Du brauchst lediglich, um Deinen Raum anders als das Original zu machen,
weil Du zB gern eine andere Langbeschreibung haettest, lediglich eine eigene
Funktion. Diese Funktion heisst create().

5.3 So funktioniert Vererbung (inheritance)
Wie Du bereits vermutet hast, liegt das Geheimnis in folgender Zeile:

inherit "/std/room";

Sie besagt, dass der Preprozessor (weil sie so wichtig ist, wurde ihr ein
eigenes Kapitel gewidmet), vor dem Compilieren die Datei /std/room.c als
Vorlage nehmen soll, und den Code, der folgt, an diese Datei fuer den Compiler
anhaengen soll.
Funktionen, die bereits existieren, wuerden dadurch wieder ueberschrieben, wenn
die Namen gleich lauten. Durch einen kleinen Kniff wird das verhindert.
Mit ::create(); ruft die selbstgeschriebene Create-Funktion die originale
Funktion auf, und schreibt sich dort an das Ende dieser Funktion.
Damit kann eine sehr lange create() entstehen, wenn man ueber viele Instanzen,
also solche Vorlagen erbt.
Ein schoenes Beispiel ist zB ein Schwert.
Der Magier hat ein neues Schwert geschrieben, dass die Schwertvorlage
in /p/weapon/schwerter/breitschwert.c erbt.
Dieses Breitschwert erbt /p/MI/weapon.c, da es eine Chance hat, ein magisches
Schwert zu werden.
Die standardisierte magische Waffe inheritet die standardisierte Waffe
(/std/weapon.c), die wiederum ein Objekt ist, dass /std/thing.c als
Grundlage fuer alle Objekte nimmt.
Damit ist der Vererbungsbaum:
 /std/thing.c
      |
       ---> /std/weapon.c
                 |
  ---> /p/MI/weapon.c
             |
      ---> /p/weapon/schwerter/breitschwert.c
                               |
        ---> neues Schwert
Jede neue Instanz hat dieselbe Funktionalitaet wie die weiter oben
liegende Instanz, erweitert sie jedoch um eigene Funktionen.
Das ist der Grund, warum es einfach ist, als normaler Programmierer im MUD
Objekte zu erstellen.
SetProp() zB wird in /std/thing.c definiert.
Waehrend die Kampfeigenschaften, die ein normales Objekt nicht braucht,
in /std/weapon.c definiert sind.
Welche Funktionen wo verwendet werden, ist mudlibspezifisch.

5.4 Zusammenfassung
Das ist natuerlich nicht alles, was man zur Vererbung sagen kann.
Diese Kurzeinfuehrung soll ja nur die Grundlagen erklaeren (und mit Hilfe der
man-Pages im MUD vertiefen, die ja dichter an der eigentlichen
MUD-Programmierung sind).
Es sollte nun bekannt sein, wie man die Vererbung nutzt, um eigene Objekte
zu erstellen. Damit zusammenhaengend sollte inzwischen bekannt sein:
1) Jede MUDLIB ist eine Zusammenstellung von generischen Objekten mit ihren
eigenen allgemeinen Funktionen, die durch Vererbung von den Programmierern
benutzt werden koennen, um eigene Objekte zu programmieren. Durch die
Vererbung ist es einfacher, solche Objekte zu schreiben, und die Interaktion
zwischen den Objekten ist fluessiger und standardisierter.
2) Die Funktionen, die die Objekte mitbringen, variieren und bauen
normalerweise aufeinander auf. Genaueres ist in den entsprechenden man-Pages
zu erfahren. Wenn nicht bekannt ist, welche Funktionen mit welcher Struktur
vorhanden sind, koennen sie nicht benutzt werden.
3) Es wird mit folgender Anweisung geerbt:

inherit "filename";

Dabei ist filename der Name des Objekte, von dem geerbt wird.

Mit ::function(); wird die originale Funktion aufgerufen, damit nicht der
Inhalt dieser Funktion durch die eigene Funktion ueberschrieben wird.
Was es genau macht, ist fuer das Verstaendnis des Handbuches relativ unwichtig,
das Einzige, was wichtig ist, ist, dass Du weisst, dass ohne diesen Aufruf
die originale Funktion nicht mehr zur Verfuegung steht.
mud.tamedhon.de:4711 ON
mud.tamedhon.de:4712 (TLS) ON
mud.tamedhon.de:4713 (Webclient) ON