April 2008

Plattformunabhängig Programmieren mit C# (Teil 1)

Einführung

Wenn man Anwendungen plattformunabhängig programmieren möchte, dann gibt es zwar verschiedenste Möglichkeiten, Frameworks und Programmiersprachen, aber der Teufel steckt im Detail und oft kommt man nicht darum herum das ganze Projekt in mehreren Ausführungen herauszubringen - jeweils für verschiedene Plattformen.

Ideal wären aber Programme, die binärkompatibel sind und ohne irgendeine Anpassung auf verschiedenen Betriebssystemen und Hardwarekonfigurationen laufen. Das lässt sich allerdings aufgrund der erheblichen Unterschiede nicht mit nativ laufenden Code erzeugen. Abhilfe schaffen hier aber interpretierte Sprachen, die sich zum Primärziel setzen, auf verschiedenen Plattformen zu funktionieren – ganz nach dem Prinzip: "Write Once Run Anywhere". Neben verschiedenen Scriptsprachen stechen hier vor allem Java und C# (.NET) hervor.

Im ersten Teil dieser Serie wird eine einfache Notenverwaltung implementiert, die ohne Anpassungen oder Tricks sowohl unter Windows mit .NET und unter Linux mit Mono funktioniert.

Anforderungen

Die Notenverwaltung soll folgendes leisten:

  1. Die Anwendung wird komplett mithilfe der Konsole bedient
  2. Man kann Noten (1-6) einem Fach hinzufügen
  3. Man kann sich den aktuellen Durchschnitt für ein Fach anzeigen lassen
  4. Man kann neue Fächer anlegen
  5. Alle Daten werden persistent auf der Festplatte gespeichert.
  6. Das Programm soll unter Windows und Linux laufen

Bevor es losgehen kann, muss man sich erstmal eine Entwicklungsumgebung einrichten. Da die Entwicklung mit VisualStudio weit verbreitet und den meisten sicherlich bekannt ist, werden stattdessen Alternativen aufgezeigt. In diesem Teil wird auf die Entwicklung unter Linux mit Monodevelop gesetzt.

Erste Schritte

Um die erste Anforderung zu erfüllen, legt man sich zuerst ein neues Projekt als Konsolenapplikation unter Verwendung der Sprache C# an.

Neues Konsolenproject

Damit wäre die Anforderung 1 schon mal erfüllt. Um Noten überhaupt handhaben zu können, ist allerdings erstmal ein Modell notwendig. Man kann das auf verschiedenste Art und Weise erzeugen, z.B. aus einer XSD-Datei, einem UML-Diagramm oder einem Datenbank-Schema. In diesen einfachen Beispiel kann man aber auch die Klassen direkt von Hand anlegen.

Neue Klasse anlegen

public class Fach { 
    private string name; 
    private ICollection<int> noten; 
 
    public Fach(string name) { 
        this.name = name; 
        this.noten = new LinkedList<int>(); 
    } 
 
    public AddNote(int note) {             
        noten.Add(note); 
    } 
 
    public double Average { 
        get { 
            if (noten.Count == 0) return 0; 
            int sum = 0; 
            foreach (int note in noten) { 
                sum += note; 
            } 
            return ((double)sum) / noten.Count; 
        } 
    } 
}

Der Einfachheit halber wird hier eine Note direkt als Ganzzahl gespeichert. Wer das für zu unpräzise hält, der kann an dieser Stelle auch eine Enumeration verwenden, die alle Noten auf 1 bis 6 einschränkt.

Unit Testing

Man könnte an dieser Stelle direkt mit den restlichen Code weitermachen, um dann irgendwann, wenn das Hauptprogramm fertig ist, endlich testen zu können, ob die Durchschnittsnotenberechnung wirklich stimmt, aber wesentlich besser ist es hier auf das unterschätzte Unit Testing zu setzen.

Dazu einfach über einen Rechtsklick auf das Projekt auf Add und dann auf New File gehen, um dann ein NUnit-TestFixture zu erstellen. Bevor allerdings getestet werden kann, muss vorher noch NUnit dem Projekt als Referenz hinzugefügt werden. Das ist sicherlich schon aus Visual Studio bekannt und wird mithilfe eines Dialogfenster erreicht, das sich über einen Rechtsklick auf References öffnen lässt.

NUnit Referenz hinzufügen

[TestFixture] 
public class FachTest { 
 
    private Fach fach; 
 
    [SetUp] 
    public void Init() { 
        fach = new Fach("Unit-Testing"); 
    } 
 
    [Test]
    public void TestAverage() { 
        fach.AddNote(2); 
        fach.AddNote(4); 
        Assert.AreEqual(3.0, fach.Average); 
    } 
}

Man sollte seine Business-Logik immer mit automatischen Test-Cases abdecken, da man sonst entweder zeitaufwendige manuelle Tests durchführen muss oder aber mit zunehmender Komplexität der Software eine immer instabilere und unsichere Anwendung hat. MonoDevelop enthält eine sehr gute Integration von NUnit, einem Unit-Testing-Framework, dass sich an dem populären JUnit orientiert.

NUnit Tests

Wie die Tests zeigen, erfüllt das Programm nun auch die Anforderungen 2 und 3. Für die vierte Anforderung muss jetzt nur noch eine Klasse für die Fächerverwaltung her.

XML-Kommentare

public class FaecherSammlung { 
    private ICollection<Fach> faecher;
 
    public FaecherSammlung() { 
        faecher = new LinkedList<Fach>(); 
    } 
 
    public void AddFach(Fach fach) { 
        faecher.Add(fach); 
    } 
 
    /// <summary> 
    /// Sucht ein Fach mit dem gegebenen Namen. 
    /// </summary> 
    /// <param name="name"> Der Name des Fachs</param> 
    /// <returns> 
    /// Das gefundene <see cref="Fach"/>, 
    /// oder null, wenn kein Fach mit dem Namen gefunden wurde. 
    /// </returns> 
    public Fach GetFach(string name) { 
        foreach (Fach fach in faecher) { 
            if (fach.Name == name) return fach;
        }
        return null; 
    }
}

Dieser Code muss natürlich auch wieder mit einem automatischen Test abgedeckt sein, der zeigt, dass nun auch die Anforderung 4 erfüllt ist. Der Kommentar mit den drei Slashes wurde zum größten Teil von MonoDevelop selbst erzeugt, sobald drei Slashes direkt vor der Methoden/Klassen/etc. Deklaration einfügt werden. Kommentare dieser Art sind sehr wichtig, da sie standardisiert sind und somit von den Werkzeugen gut genutzt werden können. So kann man beispielsweise aus den XML-Kommentaren eine technische API-Dokumentation komplett automatisch erzeugen lassen, aber am Wichtigsten ist, dass die automatische Vervollständigung – im Visual Studio-Umfeld IntelliSense genannt – diese Kommentare als Benutzerhinweis anzeigt. Bei der unüberschaubaren und ständig wachsenden Anzahl von Methoden und Klassen ist es unrealistisch, von einem Entwickler zu erwarten, dass er die gesamte Spezifikation mit alles Randfällen auswendig beherrscht. Bei selbsterklärenden Methoden sind solche Kommentare zwar nicht sehr wichtig, aber bei der Methode GetFach kann es durchaus vorkommen, dass man selbst oder andere Entwickler später nicht mehr genau wissen, ob bei einer erfolglosen Suche eine Ausnahme geworfen wird oder ob null zurückgeliefert wird. Die Kenntnis dieses Verhaltens ist für den Benutzer dieser Methode allerdings unabdingbar. Eine gute deutsche Einführung zu XML-Kommentaren in C# lässt sich hier finden.

Hauptprogramm

Das Protokoll für diese Konsolenapplikation wurde nun bewusst simpel gewählt. Normalerweise sollte man die Bedienung etwas erweitern, aber es soll schließlich nur das Grundprinzip veranschaulicht werden.

class MainClass { 
    static FaecherSammlung sammlung = new FaecherSammlung(); 
 
    public static void Main(string[] args) { 
        if (args.Length == 2) { 
            sammlung.AddFach(new Fach(args[1]));
        }
        if (args.Length == 3) {
            Fach fach = sammlung.GetFach(args[1]); 
            if (fach == null) return; 
            int note; 
            if (int.TryParse(args[2], out note)) { 
                fach.AddNote(note); 
            } else { 
                Console.WriteLine("Durchschnitt in {0} ist {1}.", 
                                  fach.Name, 
                                  fach.Average); 
            } 
        } 
    } 
} 

Einfache Persistenz mit XML-Datei

Leider bringt diese ganze Anwendung in dieser Form überhaupt nichts. Das Problem ist hierbei, dass man entweder den Durchschnitt abfragen kann, ein Fach hinzufügen kann oder eine Note, aber egal welche Aktion man ausführt, danach sind die Daten jeweils wieder komplett weg. Der Schlüssel zum Erfolg ist hier Persistenz. Um diese zu erreichen gibt es verschiedene Möglichkeiten. Die populärsten sind sicherlich die Speicherung in einer Datenbank oder als Datei auf der Festplatte. Mit C# gibt es hier einen sehr einfachen Weg, die Noten als XML-Datei auf der Festplatte zu speichern. Man muss dazu nur ein paar Annotationen – in der .NET Welt Attribute genannt vor den entsprechenden Datenfeldern schreiben und kann dann die eingebaute Serialisierung nutzen. Allerdings muss man dabei einige Einschränkungen hinnehmen. Man muss sich beispielsweise (leider) auf eine konkrete Art der Aufzählung festlegen. Während vorher jede beliege ICollection genutzt werden konnte, so muss man sich bei der automatischen Serialisierung auf eine Liste oder ein Array festlegen.

Zwei andere Sachen sind hierbei auch zu beachten: Wenn der Serialisierer die Daten speichern soll, so muss er auch darauf zugreifen können, das bedeutet, dass alle relevanten Daten entweder direkt oder über ein Property öffentlich also public sein müssen. Außerdem muss eine solche Datenklasse auch immer einen parameterlosen öffentlichen Konstruktor haben. Die Änderungen sind hier zusammengefasst:

[XmlType]
public class Fach { 
 
    private string name;
 
    [XmlElement]
    public List<int> noten; 
 
    [XmlElement]
    public string Name {
        get { return name; }
        set { name = value; }
    }
 
    public Fach() : this(string.Empty) {}
 
    ...
}
 
[XmlType]
[XmlRoot]
public class FaecherSammlung {
     [XmlElement]
     public List<Fach> faecher;
 
    ...
} 

Nun ist das Laden und Speichern überhaupt kein Problem mehr. Man kann jedes XMLRoot-Element mithilfe eines Serializers in einen XML-String oder auch gleich in eine Datei umwandeln und auch wieder auf die gleiche Art und Weise laden. Ohne die (wichtige) Ausnahmebehandlung sieht das folgendermaßen aus:

class MainClass { 
    static FaecherSammlung sammlung = new FaecherSammlung(); 
 
    static XmlSerializer serializer = new XmlSerializer(typeof(FaecherSammlung)); 
 
    static string fileName = "/tmp/noten.xml"; 
 
    public static void Main(string[] args) { 
        Load(); 
        ...
        Save();
    }
 
    private static void Load() { 
        using(FileStream fs = File.Open(fileName, FileMode.Open)) { 
            sammlung = (FaecherSammlung)(serializer.Deserialize(fs)); 
        } 
    }
 
    private static void Save() { 
        using(FileStream fs = File.Create(fileName)) { 
            serializer.Serialize(fs, sammlung); 
        } 
    } 
}

Man kann nun die Anwendung starten, wobei man beim ersten Start auf das Laden verzichten sollte, da die Datei noch gar nicht existiert. Danach können allerdings unter der Konsole Noten und Fächer hinzugefügt und der Durchschnitt abgefragt werden. Somit wäre auch Anforderung 5 von 6 erfüllt.

$ mono Noten.exe Musik
$ mono Noten.exe Musik 3
$ mono Noten.exe Musik 2
$ mono Noten.exe Musik durchschnitt
Durchschnitt in Musik ist 2.5.

Umgang mit Dateien

Unter Linux funktioniert die Anwendung bereits wunderbar, aber dennoch sind wird nicht fertig, denn an einem ganz entscheidenden Punkt wurde bis jetzt noch nicht berücksichtigt: Der Dateipfad der XML-Datei kann so nicht für den produktiven Betrieb genutzt werden.

static string fileName = "/tmp/noten.xml";

Einerseits würden dann alle Benutzer die gleichen Noten haben, die möglicherweise auch nur temporär vorhanden wären und andererseits gibt es diesen Pfad bei Windows gar nicht. Eine Lösung bietet hier die Verwendung der Environment-Klasse, die alle speziellen Verzeichnisse direkt zur Verwendung enthält, ohne dass man sich um verschiedene Versionen von Windows, Linux oder anderen Betriebssystemen kümmern muss.

static string fileName = 
    Environment.GetFolderPath(Environment.SpecialFolder.Personal) 
    + "/noten.xml";

Sind wir jetzt fertig? Nein noch nicht ganz. Ein einziges Zeichen würde dafür sorgen, dass die Anwendung unter Windows zu merkwürdigen Resultaten führt. Denn während in der Unix-Welt der Slash / als Verzeichnissymbol benutzt wird, so ist es in der Windows-Welt der Backslash \ . Glücklicherweise gibt es auch dafür eine Konstante, sodass der portable Pfad nun so aussieht:

static string fileName = 
    Environment.GetFolderPath(Environment.SpecialFolder.Personal) 
    + Path.DirectorySeparatorChar 
    + "noten.xml";

Ausführen auf der Windows Konsole

Fazit

Die Anforderung 6 wäre somit auch erfüllt, da die Datei "Noten.exe" auf Windows und Linux exakt gleich funktioniert!

Es scheint so, als würde die Plattformunabhängigkeit dank .NET und Mono nun gar kein Problem mehr darstellen. Und in der Tat kann bei einfachen Konsolenanwendungen wie dieser hier eine Binärkompatibilität erreicht werden. Doch auch diese hat leider ihre Grenzen: Im zweiten Teil dieser Serie wird eine grafische Benutzeroberfläche für die Notenverwaltung entworfen, wobei sich die beiden Zielplattformen doch erheblich unterscheiden, was durchaus eine Herausforderung darstellen kann.

(originally posted 2008-04-09)