Jeder kann coden / Programmieren & TicTacToe / Objektorientierte Programmierung / Objektorientierte Programmierung in C#
Beispiel: Spielfeldmanagement in C#¶
Spielstände speichern und laden mit Interfaces und Exception Handling¶
Einführung¶
In der Softwareentwicklung sind Wiederverwendbarkeit und Robustheit entscheidende Eigenschaften gut gestalteter Anwendungen. In diesem Artikel zeigen wir, wie man in C# ein Spielfeld für ein Spiel modelliert, das Spielstände speichern und laden kann. Dabei setzen wir auf bewährte Praktiken wie die Verwendung von Interfaces, abstrakten Klassen und Exception Handling.
Zielsetzung¶
Unsere Aufgabe ist es, eine Klasse Spielfeld
zu entwickeln, die Folgendes kann:
- Spielstände in eine Datei speichern und daraus laden.
- Fehler (z. B. beim Dateizugriff oder bei ungĂĽltigem Dateiformat) robust behandeln.
- Wiederverwendbare Funktionalität durch Interfaces und Vererbung anbieten.
Modellierung des Spielfelds¶
Ein Spielfeld besteht aus Zellen, die jeweils einen Spielstein enthalten. Die Klasse Spielstein
hat eine Eigenschaft Wert
, die den Zustand einer Zelle beschreibt.
internal class Spielstein
{
public string Wert { get; set; }
}
Das Spielfeld selbst wird durch ein zweidimensionales Array von Spielstein
-Objekten dargestellt:
internal class Spielfeld
{
private Spielstein[,] data;
public Spielfeld(int zeilen, int spalten)
{
data = new Spielstein[zeilen, spalten];
// Initialisiere Spielfeld mit leeren Spielsteinen
for (int i = 0; i < zeilen; i++)
{
for (int j = 0; j < spalten; j++)
{
data[i, j] = new Spielstein { Wert = "-" };
}
}
}
}
Interfaces und Abstrakte Klassen¶
Um die Funktionalität flexibel und erweiterbar zu halten, nutzen wir das Interface ISpielstandSpeicher
. Es definiert zwei Methoden: Speichern
und Laden
.
internal interface ISpielstandSpeicher
{
void Speichern(string dateipfad);
void Laden(string dateipfad);
}
Die abstrakte Klasse SpielfeldSpeicher
implementiert grundlegendes Exception Handling. Dadurch können konkrete Spielfeldklassen sich auf die Kernlogik konzentrieren.
using System;
using System.IO;
internal abstract class SpielfeldSpeicher : ISpielstandSpeicher
{
public abstract void Speichern(string dateipfad);
public abstract void Laden(string dateipfad);
protected void FehlerAusgabe(Exception ex)
{
switch (ex)
{
case IOException ioEx:
Console.WriteLine("Fehler beim Zugriff auf die Datei: " + ioEx.Message);
break;
case UnauthorizedAccessException uaEx:
Console.WriteLine("Zugriff verweigert: " + uaEx.Message);
break;
case FormatException fmtEx:
Console.WriteLine("UngĂĽltiges Dateiformat: " + fmtEx.Message);
break;
default:
Console.WriteLine("Ein allgemeiner Fehler ist aufgetreten: " + ex.Message);
break;
}
}
}
Die Spielfeld-Klasse¶
Die konkrete Klasse Spielfeld
erbt von SpielfeldSpeicher
. Die Methoden Speichern
und Laden
implementieren die Kernlogik. Mithilfe von using
-Blöcken stellen wir sicher, dass Ressourcen (z. B. Datei-Handles) korrekt freigegeben werden.
internal class Spielfeld : SpielfeldSpeicher
{
private Spielstein[,] data;
public Spielfeld(int zeilen, int spalten)
{
data = new Spielstein[zeilen, spalten];
for (int i = 0; i < zeilen; i++)
{
for (int j = 0; j < spalten; j++)
{
data[i, j] = new Spielstein { Wert = "-" };
}
}
}
public override void Speichern(string dateipfad)
{
try
{
using (StreamWriter writer = new StreamWriter(dateipfad))
{
for (int i = 0; i < data.GetLength(0); i++)
{
for (int j = 0; j < data.GetLength(1); j++)
{
writer.Write(data[i, j].Wert + " ");
}
writer.WriteLine();
}
}
Console.WriteLine("Spielstand erfolgreich gespeichert.");
}
catch (Exception ex)
{
FehlerAusgabe(ex);
}
}
public override void Laden(string dateipfad)
{
try
{
using (StreamReader reader = new StreamReader(dateipfad))
{
for (int i = 0; i < data.GetLength(0); i++)
{
string? zeile = reader.ReadLine();
if (zeile == null)
{
throw new FormatException("UngĂĽltiges Dateiformat: Eine Zeile fehlt.");
}
string[] werte = zeile.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (werte.Length != data.GetLength(1))
{
throw new FormatException("UngĂĽltiges Dateiformat: Falsche Anzahl an Werten in einer Zeile.");
}
for (int j = 0; j < data.GetLength(1); j++)
{
data[i, j].Wert = werte[j];
}
}
}
Console.WriteLine("Spielstand erfolgreich geladen.");
}
catch (Exception ex)
{
FehlerAusgabe(ex);
}
}
public void AusgabeSpielfeld()
{
for (int i = 0; i < data.GetLength(0); i++)
{
for (int j = 0; j < data.GetLength(1); j++)
{
Console.Write(data[i, j].Wert + " ");
}
Console.WriteLine();
}
}
}
Beispielnutzung¶
In der Main
-Methode erstellen wir ein Spielfeld, speichern den Spielstand in eine Datei und laden ihn anschlieĂźend wieder.
Spielfeld spielfeld = new Spielfeld(3, 3);
spielfeld.AusgabeSpielfeld();
Console.WriteLine("Speichern des Spielstands...");
spielfeld.Speichern("spielstand.txt");
Console.WriteLine("Laden des Spielstands...");
spielfeld.Laden("spielstand.txt");
spielfeld.AusgabeSpielfeld();
- - - - - - - - - Speichern des Spielstands... Spielstand erfolgreich gespeichert. Laden des Spielstands... Spielstand erfolgreich geladen. - - - - - - - - -
Fazit¶
Mit diesem Ansatz kombinieren wir bewährte Programmierpraktiken:
- Wiederverwendbarkeit durch Interfaces und abstrakte Klassen.
- Robustheit durch detailliertes Exception Handling.
- Lesbarkeit durch die Verwendung von
using
.
Das Ergebnis ist ein flexibles System, das leicht erweiterbar ist und zugleich eine saubere Fehlerbehandlung gewährleistet.