Modular gedacht

Wie ja schon des öfteren erwähnt: Ich freue mich über Feedback und Anfragen, die mir dann auch gleich Anregung für einen neuen Beitrag liefern. Um so schöner, wenn’s denn mal ganz grundlegende Fragen betrifft.

So war dies dann auch kürzlich (nunja, einen Monat ist’s schon her), als mich ein Leser fragte, wie denn mit globalen Variablen umzugehen sei. Das ist eine Frage, die insbesondere bei Umsteigern bzw. Programmierern, die mit linearen Programmiersprachen begonnen haben, häufiger auftaucht. Saß man seinerzeit z.B. vor Turbo-Pascal, kam man schwerlich drum herum, globale Variablen zu definieren, also solche, die unter ihrem Namen von überall ansprechbar waren. Der Anfang eines solchen Programms bestand gerne mal aus ellenlangen Deklarationen von Variablen, auf die die später folgenden Methoden zugreifen konnten.

In objektorientierten Zeiten und Systemen, so wie bei Xojo, ist diese Frage nach globalen Variablen einfach zu beantworten: Möglichst gar nicht! Zum Prinzip von OOP gehört ja gerade die Verkapselung von Klassen. Was sehr praktisch ist: Habe ich eine Methode gebaut, die irgendeinen allgemeinen Zweck erfüllt, kann ich sie später in jedes andere Projekt kopieren, ohne dass ich sie anpassen müsste, weil sie eine bestimmte globale Variable, auf die sie baut, nicht mehr findet oder diese einen anderen Namen hat. Stattdessen übergibt man den Methoden eben Parameter und lässt sich ggf. Rückgabewerte zurückliefern.

Ganz ohne Properties, die allen (oder sehr vielen) Methoden zugleich zur Verfügung stehen, kommt man allerdings auch heute nicht immer aus. Anstatt aber von verschiedenen Stellen auf eine festcodierte Eigenschaft zuzugreifen, gibt es allerdings elegantere Möglichkeiten:

Byref

Nehmen wir mal an, Sie programmieren ein digitales Brettspiel, und für Berechnung der Punkte gibt es mehrere Kriterien, die in unterschiedlichen Methoden berechnet werden: Die Nähe zu anderen Spielfiguren, die Schlagkraft der verbliebenen Figuren, die vergangene Zeit …

Weiterhin nehmen wir an, dass es eine Klasse Spielfigur gibt und dass die Positionen dieser in einem Xojo.Core.Point repräsentiert werden.

Und nehmen wir außerdem an, Sie haben die Punktzahl als Integer-Property im App-Objekt geparkt:

Punkte As Integer

Dann könnte nun jede Berechnungsmethode auf App.Punkte zugreifen und diese Eigenschaft verändern – das wäre die lineare Methode, mit den beschriebenen Problemen bei Namensänderung etc.

Eine Möglichkeit, die Punkte modularer zu verwenden, wäre ein Aufruf der Methode BerechneSpielzug auf diese Art:

App.Punkte = BerechneSpielzug (Spielfigur, Position, App.Punkte)

Und es gäbe die Methode

Public Function BerechneSpielzug(Figur As Spielfigur, Position As Xojo.Core.Point, Punkte As Integer) as Integer
   Return Punkte * 2
End Function

(die jetzt nicht wirklich viel berechnet, sondern einfach den Eingabewert verdoppelt. Einfach mal die Phantasie anwerfen und imaginieren, dass hier wirklich ein Berechnung des Zugs stattgefunden hätte.)

Völlig legitim einerseits, so vorzugehen, aber nicht in jedem Fall der beste Stil. Was ist beispielsweise, wenn die Berechnungsmethode feststellt, dass der Spielzug gar nicht legitim war? Einen negativen Wert zurückgeben und in der aufrufenden Methode eine Extra-Auswertung dafür? Null zurückgeben? Stellt uns vor die gleiche Problematik. Das Programm wird weniger durchschaubar, weil ja diese Sonderbedingungen ausgewertet werden müssen – dabei hat BerechneSpielzug ja diese Aufgabe schon übernommen.

Klarer wäre doch ein Aufruf der Form

Dim LegitimerSpielzug As Boolean = BerechneSpielzug (Spielfigur, Position, Punkte)

Kommt ein True zurück, war also alles ok, und ansonsten gibt es einen freundlichen Hinweis an den Nutzer, sich gefälligst an die Regeln zu halten.

Das lässt sich dann auch in der Tat in einem Abwasch erledigen, durch das kleine Wörtchen ByRef:

Public Function BerechneSpielzug(Figur As Spielfigur, Position As Xojo.Core.Point, ByRef Punkte As Integer) as Boolean
  Dim Legitim As Boolean
  // Irgendeine Auswertung, die Legitim berechnet
  If Legitim Then Punkte = Punkte * 2
  Return Legitim
End Function

Durch ByRef werden die Punkte als Referenz übergeben, nicht als temporäre Variable. Im Fall eines legitimen Spielzugs also haben sie den doppelten Wert, und zugleich weiß die Hauptfunktion, ob sie im Spielverlauf einfach weitermachen kann.

Ortsbestimmung

Es gibt aber noch mehr zu verbessern. Ist das App-Objekt wirklich der beste Ort für Spielmechanik und Punkte? Ich bin ja auch noch gar nicht darauf eingegangen, wo sich BerechneSpielzug eigentlich befindet.

Gerne werden solche Geschichten dann in das Fenster ausgelagert, das die grafische Oberfläche zeichnet. Wenn man später aber Überlegungen anstellt, ob das fiktive Spielprojekt vielleicht einen iOS-Ableger bekommen sollte … iOS kennt keine Fenster, dafür Views. Also wieder Umbauarbeiten!

Viel sinnvoller ist es, Berechnungen und Ausgabe zu trennen. Und damit auch endlich zum Titel dieses Beitrags: Nehmen Sie doch ein Modul! In dieses Modul gehört alles, was die Spielmechanik ausmacht: Properties für die Punkte, die Spielfiguren, die Spiellogik inkl. Auswertung. Das Fenster, iosView oder die WebPage hat dann nur die Aufgabe, den aktuellen Inhalt darzustellen und die Spielmechanik anzustupsen, indem z.B. ein Button für „Neues Spiel“ angeklickt wird oder ein Spielzug zur Auswertung freigegeben wird.

Das lässt sich noch weiter auf die Spitze treiben, indem man ein weiteres Objekt dazwischenschaltet, das die Auswertung der Fensterveränderungen übernimmt und nach Berechnungen wiederum das Fenster aktualisiert. Damit wäre man in der höchsten Abstraktionsstufe angekommen, nämlich einem Model-View-Controller-System: Das Fenster (View im Modell) teilt dem Controller, dem Vermittler zwischen Spiellogik und Grafik, eine Veränderung mit, und der Controller teilt dann wiederum dem Fenster mit, wenn die Spiellogik (das Model) die Auswertung abgeschlossen hat und optische Aktualisierungen nötig werden.

Das ist jetzt, zugegeben, ein bisschen akademisch und soll an dieser Stelle auch nicht Thema sein, nur Anregung, sich möglichst frühzeitig im Projekt mit der Modularisierung auseinanderzusetzen, um sich selbst spätere Erweiterungen so einfach wie möglich zu machen. Ein bisschen Anschauungsmaterial zum MVC-Modell liefert dieser Gastbeitrag von Gavin Smith auf dem Xojo-Blog.

Bleiben wir stattdessen bei den Modulen als solchen und schauen ein bisschen in praktische Anwendungen:

Ein Modul kann also benutzt werden, um thematisch zusammenpassende Properties und Methoden, die nicht unbedingt in eine Klasse gehören, unterzubringen.

Debug-Hilfe

Auch wenn es wünschenswert wäre, dass jedes Programm auf Anhieb so funktioniert wie vorgestellt: Die Realität stellt sich oft anders dar. Gerne greift man dann auf die Konsole zurück und lässt sich Stände des laufenden Programms mittels

System.DebugLog (DebugText As String)

ausgeben. Praktische Angelegenheit. Bis alles zufriedenstellend läuft. Dann durchforstet man seinen Code und wirft diese ganzen Zeilen wieder raus und kommentiert sie aus. Umständlich, oder?

Wie wäre es stattdessen mit einem Modul DebugModul mit der Methode

Public Sub Debug(DebugText As Text)
 If DebugEnabled Then System.DebugLog DebugText
End Sub

und der Property

Protected Property DebugEnabled as Boolean

Ist man auf der Fehlersuche, setzt man diese Property auf True. Läuft alles, geht es zurück auf den Standardwert False, und die Konsole wird nicht vollgeschrieben. Hat auch den Vorteil, dass man statt System.DebugLog nur noch Debug schreiben muss, und der Datentyp Text macht das Ganze auch iOS- bzw. Xojo-Framework-kompatibel.

Statt einer manuell zu setzenden Property kann die Methode auch einfach

Public Sub Debug(DebugText As Text)
 #If DebugBuild 
   System.DebugLog DebugText
 #EndIf
End Sub

heißen, und die Debug-Meldungen erscheinen nur noch während des Programmtests. Mit behagt allerdings die erste Methode mehr. Auf die Art lässt sich ggf. auch beim Anwender das Protokollieren einschalten, um eine Ferndiagnose erstellen zu können. Als Protected Property kann mein Code von überall auch beliebig das Protokollieren mittels

DebugModul.DebugEnabled

ein- und ausschalten.

Darf’s eine Funktion mehr sein?

Ganz ähnlich sind Module äußerst praktische Sammelstellen für Funktionserweiterungen, die nicht zum Normalumfang von Xojo gehören. Wie gerade im Forum diskutiert:

Benötige ich z.B. regelmäßig mal den morgigen Tag als Datum, muss ich diesen nicht an jeder Stelle explizit erzeugen. Stattdessen ein Modul DateModul mit der Using Clause

Using Xojo.Core

und der Methode

Public Function Morgen(extends d as Date) as Date
 Return d + New DateInterval (0,0,1)
End Function

Und fortan kann ich überall, wo ein Xojo.core.Date auftaucht, den morgigen Tag durch

Using Xojo.Core
Dim Heute As Date = DateNow
Dim Morgen As Date = Heute.Morgen

aufrufen.

Auf diese Art basteln Sie sich in Kürze sinnvolle Erweiterungen zusammen, die jede Menge Tipparbeit sparen und überall wiederverwendet werden können.

Ein Mini-Spielprojekt mit beiden Modulen finden Sie hier.

Ein Gedanke zu “Modular gedacht

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s