Die Handhabung von Zustand in modernem C++ stellt Entwickler regelmäßig vor Herausforderungen – insbesondere, wenn dieser Zustand nicht direkt zur Domänenlogik gehört, aber dennoch für die Implementierung benötigt wird. Die Lösung liegt in der klaren Trennung zwischen Domänenmodell und internem Modulzustand.
Warum Domänenmodelle nicht mit Implementierungsdetails überladen werden sollten
In vielen Softwareprojekten wird Zustand oft undifferenziert behandelt. Während zentrale Domänenobjekte wie Spieler, Spielfeld oder Nahrung klar zum Modell gehören, gibt es zahlreiche technische Details, die zwar für die Implementierung notwendig sind, aber keine semantische Bedeutung für die Domäne haben. Beispiele hierfür sind:
- Parser, die sich Positionen im Eingabestrom merken müssen
- Caches, die zuvor getätigte Abfragen speichern
- Ereignisfilter, die Tastatureingaben vorverarbeiten
Würde man solchen Zustand auf dieselbe Weise behandeln wie Domänenobjekte, würde dies das Modell unnötig verkomplizieren. Nicht nur würde die Lesbarkeit leiden, sondern es bestünde auch die Gefahr, dass sich unrelated Logic an diese Details bindet. Die Folge wäre ein System, das zwar explizit ist, aber dessen Implementierungsdetails in die Domäne durchsickern – ein klarer Verstoß gegen das Prinzip der Trennung von Belangen.
Reines C++: Module mit lokalem Zustand ohne Objektorientierung
In der objektorientierten Programmierung wäre die naheliegende Lösung, den internen Zustand hinter einer Klassen-Schnittstelle zu verstecken. Doch wer funktionale Programmierungsprinzipien bevorzugt, steht vor der Frage: Wie lässt sich Zustand sauber kapseln, ohne auf Objekte mit verborgenem, mutierbarem Zustand zurückzugreifen?
Die Antwort liegt in der Verwendung eines zustandstragenden Moduls, das aus reinen Funktionen besteht. Diese Funktionen arbeiten auf einem gemeinsamen Modulzustand, aktualisieren ihn jedoch auf explizite Weise, indem sie den neuen Zustand als Rückgabewert zurückgeben. Der entscheidende Vorteil: Der Zustand bleibt lokal zum Modul, während die Schnittstelle sauber und funktional bleibt.
Ein solches Modul besteht aus zwei grundlegenden Komponenten:
- Einem
State-Typ, der den internen Zustand des Moduls definiert - Einem oder mehreren Funktionen, die diesen Zustand als Eingabe erhalten und den aktualisierten Zustand zurückgeben
Die Namensraum-Struktur (namespace) übernimmt dabei die Rolle der Kapselung. Sie gruppiert den Zustandstyp und die zugehörigen Funktionen und schafft so eine klare Abgrenzung nach außen.
Praktisches Beispiel: Tastatureingaben in einem Snake-Spiel
Ein konkretes Beispiel findet sich in einem Snake-Spiel, bei dem die Richtung der Schlangen über Tastatureingaben gesteuert wird. Da Spielschleife und Tastaturereignisse asynchron ablaufen, muss die Richtung der Schlangen zu Beginn jedes Schleifendurchlaufs aktualisiert werden. Hierfür ist das Modul direction_command_filter zuständig, das Tastatureingaben filtert und die korrekten Bewegungsrichtungen zurückgibt.
Der interne Zustand dieses Moduls ist eine Warteschlange von Richtungskommandos pro Spieler. Dieser Zustand existiert ausschließlich zur Implementierung der Filterlogik und gehört nicht zum Domänenmodell des Spiels – also nicht zu Objekten wie Schlangen, Nahrung oder Spielfeld.
namespace direction_command_filter {
struct State {
using PerPlayerDirectionQueue = std::map<PlayerId, std::deque<Direction>>;
PerPlayerDirectionQueue queues;
};
}Die Schnittstelle des Moduls besteht aus zwei Funktionen:
tryAdd: Fügt ein neues Richtungskommando zur Warteschlange hinzu und gibt den aktualisierten Zustand zurücktryConsumeNext: Konsumiert das nächste gültige Richtungskommando und aktualisiert den Zustand
State tryAdd(State state, const PerPlayerSnakes& snakes, const DirectionCommand& cmd);
std::tuple<State, PerPlayerDirection> tryConsumeNext(State state);Im Hauptmodul (shell) wird der Modulzustand zusammen mit dem Domänenmodell verwaltet:
namespace shell {
struct GameState {
// Domänenmodell
PerPlayerSnakes snakes;
// Modulinterner Zustand
direction_command_filter::State direction_command_filter_state;
};
class GameEngineActor : public Actor<GameEngineActor> {
void Update() {
// Zustand aktualisieren
state_.direction_command_filter_state =
direction_command_filter::tryAdd(
state_.direction_command_filter_state,
state_.snakes,
new_command);
auto [new_state, direction] =
direction_command_filter::tryConsumeNext(
state_.direction_command_filter_state);
state_.direction_command_filter_state = new_state;
}
};
}Der allgemeingültige Modul-Pattern-Ansatz
Die in diesem Beispiel erkennbare Struktur lässt sich auf beliebige Module übertragen, die internen Zustand benötigen. Das generische Muster sieht wie folgt aus:
namespace modulname {
struct State {
// Definition des internen Zustands
};
State operation(State state, Input input) {
// Reine Funktion, die den Zustand aktualisiert
return new_state;
}
}Die wichtigsten Merkmale dieses Ansatzes:
- Explizite Zustandshandhabung: Der Zustand wird als Parameter übergeben und als Rückgabewert zurückgegeben, wodurch die Funktionalität transparent bleibt.
- Kapselung durch Namensraum: Der Modulzustand und seine Funktionen sind in einem Namensraum gruppiert, der als Abgrenzung dient.
- Testbarkeit: Da die Funktionen rein sind, lassen sie sich leicht durch Unit-Tests validieren, indem ein
Stateerstellt, die Funktion aufgerufen und der Rückgabewert überprüft wird. - Keine verborgenen Seiteneffekte: Der Zustand wird nicht mutiert, sondern durch saubere Rückgabewerte aktualisiert.
Fazit: Saubere Architektur durch klare Trennung
Die Kombination aus reinem C++ und modularer Zustandshandhabung bietet eine elegante Lösung für ein häufiges Problem: die Vermeidung von Domänenverschmutzung durch technische Implementierungsdetails. Durch die klare Trennung zwischen Domänenmodell und modularem Zustand bleibt die Architektur übersichtlich, wartbar und anpassbar.
Diese Vorgehensweise eignet sich besonders für Systeme mit komplexen Abläufen, in denen technische Details wie Ereignisverarbeitung, Caching oder Parser-Logik eine Rolle spielen. Wer funktionale Prinzipien in C++ umsetzen möchte, findet hier einen bewährten Weg, um saubere und verständliche Code-Strukturen zu schaffen – ohne auf Objektorientierung oder verborgene Zustände angewiesen zu sein.
KI-Zusammenfassung
C++ projelerinizde durum yönetimini nasıl daha anlaşılır ve bakımı kolay hale getirebilirsiniz? Fonksiyonel programlama prensiplerini kullanarak domain modelinizi korurken iç durumları nasıl kapsayacağınızı öğrenin.