Python-Zustandsmuster

Die Kernidee des Zustandsmusters besteht darin, den Zustand eines Objekts in unabhängige Klassen zu kapseln und das Verhalten des Objekts an das aktuelle Zustandsobjekt zu delegieren. Wenn sich der Zustand des Objekts ändert, wechselt es zu einem anderen Zustandsobjekt und ändert dadurch sein Verhalten.

Warum brauchen wir das Zustandsmuster?

Ohne die Verwendung des Zustandsmusters verwenden wir normalerweise eine große Anzahl vonif-elseoderswitch-caseAnweisungen, um Verhalten in verschiedenen Zuständen zu behandeln. Dieser Ansatz hat mehrere Probleme:

  • Code-Aufblähung: Mit zunehmender Anzahl von Zuständen werden bedingte Anweisungen immer komplexer
  • Schwer zu warten: Das Ändern des Verhaltens eines Zustands kann andere Zustände beeinflussen
  • Verletzt das Open-Closed-Prinzip: Das Hinzufügen eines neuen Zustands erfordert die Änderung vorhandenen Codes

Das Zustandsmuster löst diese Probleme, indem jeder Zustand in eine unabhängige Klasse gekapselt wird.


Struktur des Zustandsmusters

Hauptrollen

Das Zustandsmuster besteht aus drei Kernrollen:

  1. Context (Kontext): Verwaltet eine Instanz eines konkreten Zustandsobjekts; diese Instanz definiert den aktuellen Zustand
  2. State (Zustandsschnittstelle): Definiert eine Schnittstelle zum Kapseln von Verhalten, das einem bestimmten Zustand des Context zugeordnet ist
  3. ConcreteState (konkreter Zustand): Implementiert die State-Schnittstelle; jede konkrete Zustandsklasse implementiert ein Verhalten, das mit einem Zustand des Context zusammenhängt

UML-Klassendiagramm


Implementierung des State-Musters

Grundlegende Implementierungsschritte

Lassen Sie uns die Implementierung des State-Musters anhand eines einfachen Beispiels verstehen. Angenommen, wir haben ein Aufzugssystem, bei dem der Aufzug verschiedene Zustände hat: offen, geschlossen, fahrend und angehalten.

Schritt 1: Die State-Schnittstelle definieren

Beispiel

from abc import ABC, abstractmethod

class ElevatorState(ABC):
    """Aufzug-Zustandsschnittstelle"""
   
    @abstractmethod
    def open_doors(self):
        """Türöffnungsvorgang"""
        pass
   
    @abstractmethod
    def close_doors(self):
        """Vorgang zum Schließen der Tür"""
        pass
   
    @abstractmethod
    def move(self):
        """Bewegungsvorgang"""
        pass
   
    @abstractmethod
    def stop(self):
        """Stoppvorgang"""
        pass

Schritt 2: Konkrete Zustandsklassen implementieren

Beispiel

class DoorOpenState(ElevatorState):
    """Geöffneter Zustand"""
   
    def open_doors(self):
        print("Die Tür ist bereits offen")
        return self
   
    def close_doors(self):
        print("Tür wird geschlossen...")
        return DoorClosedState()
   
    def move(self):
        print("Fehler: Die Tür ist noch offen, Bewegung nicht möglich")
        return self
   
    def stop(self):
        print("Der Aufzug hat angehalten")
        return self

class DoorClosedState(ElevatorState):
    """Geschlossener Zustand"""
   
    def open_doors(self):
        print("Tür wird geöffnet...")
        return DoorOpenState()
   
    def close_doors(self):
        print("Die Tür ist bereits geschlossen")
        return self
   
    def move(self):
        print("Der Aufzug beginnt sich zu bewegen...")
        return MovingState()
   
    def stop(self):
        print("Der Aufzug hat angehalten")
        return self

class MovingState(ElevatorState):
    """Bewegungszustand"""
   
    def open_doors(self):
        print("Fehler: Der Aufzug bewegt sich, die Tür kann nicht geöffnet werden")
        return self
   
    def close_doors(self):
        print("Die Tür ist bereits geschlossen")
        return self
   
    def move(self):
        print("Der Aufzug bewegt sich")
        return self
   
    def stop(self):
        print("Der Aufzug hält an...")
        return DoorClosedState()

Schritt 3: Erstelle die Kontextklasse

Beispiel

class Elevator:
    """Kontextklasse für den Aufzug"""
   
    def __init__(self):
        # Anfangszustand ist geschlossener Türzustand
        self._state = DoorClosedState()
   
    @property
    def state(self):
        """Aktuellen Zustand abrufen"""
        return self._state
   
    def set_state(self, state):
        """Neuen Zustand setzen"""
        self._state = state
        print(f"Zustand gewechselt zu: {state.__class__.__name__}")
   
    def open_doors(self):
        """Türöffnungsvorgang"""
        print("Türöffnungsvorgang wird ausgeführt...")
        self.set_state(self._state.open_doors())
   
    def close_doors(self):
        """Vorgang zum Schließen der Tür"""
        print("Türschließvorgang wird ausgeführt...")
        self.set_state(self._state.close_doors())
   
    def move(self):
        """Bewegungsvorgang"""
        print("Fahrvorgang wird ausgeführt...")
        self.set_state(self._state.move())
   
    def stop(self):
        """Stoppvorgang"""
        print("Stoppvorgang wird ausgeführt...")
        self.set_state(self._state.stop())

Schritt 4: Verwende das Zustandsmuster

Beispiel

# Aufzug-Instanz erstellen
elevator = Elevator()

# Zustandsübergänge testen
print("=== Test der Zustandsübergänge des Aufzugs ===")

elevator.open_doors()   # Wechsel vom geschlossenen Zustand in den offenen Zustand
elevator.close_doors()  # Wechsel zurück vom offenen Zustand in den geschlossenen Zustand
elevator.move()         # Wechsel vom geschlossenen Zustand in den fahrenden Zustand
elevator.stop()         # Wechsel zurück vom fahrenden Zustand in den geschlossenen Zustand

print("\n=== Test einer unzulässigen Operation ===)
elevator.open_doors()   # Die Tür normal öffnen
elevator.move()         # Versuchen zu fahren, während die Tür offen ist (sollte einen Fehler auslösen)

Ausführungsergebnis

=== 电梯状态转换测试 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行关门操作...
正在关门...
状态已切换为: DoorClosedState
执行移动操作...
电梯开始移动...
状态已切换为: MovingState
执行停止操作...
电梯停止中...
状态已切换为: DoorClosedState

=== 测试非法操作 ===
执行开门操作...
正在开门...
状态已切换为: DoorOpenState
执行移动操作...
错误:门还开着,不能移动
状态已切换为: DoorOpenState

Fortgeschrittene Anwendungen des Zustandsmusters

Zustandsmuster mit geteiltem Zustand

In manchen Fällen müssen mehrere Kontexte möglicherweise dasselbe Zustandsobjekt gemeinsam nutzen. Wir können die gemeinsame Nutzung des Zustands durch das Singleton-Muster erreichen.

Beispiel

class Singleton(type):
    """Singleton-Metaklasse"""
    _instances = {}
   
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class SharedDoorOpenState(ElevatorState, metaclass=Singleton):
    """Gemeinsamer offener Zustand (Singleton)"""
   
    def open_doors(self):
        print("Die Tür ist bereits offen")
        return self
   
    def close_doors(self):
        print("Tür wird geschlossen...")
        return SharedDoorClosedState()
   
    def move(self):
        print("Fehler: Die Tür ist noch offen, Bewegung nicht möglich")
        return self
   
    def stop(self):
        print("Der Aufzug hat angehalten")
        return self

class SharedDoorClosedState(ElevatorState, metaclass=Singleton):
    """Gemeinsamer geschlossener Zustand (Singleton)"""
   
    def open_doors(self):
        print("Tür wird geöffnet...")
        return SharedDoorOpenState()
   
    def close_doors(self):
        print("Die Tür ist bereits geschlossen")
        return self
   
    def move(self):
        print("Der Aufzug beginnt sich zu bewegen...")
        return SharedMovingState()
   
    def stop(self):
        print("Der Aufzug hat angehalten")
        return self

class SharedMovingState(ElevatorState, metaclass=Singleton):
    """Geteilter Bewegungszustand (Singleton)"""
   
    def open_doors(self):
        print("Fehler: Der Aufzug bewegt sich, die Tür kann nicht geöffnet werden")
        return self
   
    def close_doors(self):
        print("Die Tür ist bereits geschlossen")
        return self
   
    def move(self):
        print("Der Aufzug bewegt sich")
        return self
   
    def stop(self):
        print("Der Aufzug hält an...")
        return SharedDoorClosedState()

Zustandsmuster und Zustandsmaschinen

Das Zustandsmuster wird oft in Kombination mit Zustandsmaschinen verwendet. Nachfolgend ein komplexeres Beispiel für eine Zustandsmaschine:

Beispiel

class VendingMachineState(ABC):
    """Zustandsschnittstelle des Verkaufsautomaten"""
   
    @abstractmethod
    def insert_coin(self, machine, amount):
        """Münze einwerfen"""
        pass
   
    @abstractmethod
    def select_product(self, machine, product):
        """Produkt auswählen"""
        pass
   
    @abstractmethod
    def dispense(self, machine):
        """Produkt ausgeben"""
        pass
   
    @abstractmethod
    def refund(self, machine):
        """Rückerstattung"""
        pass

class WaitingState(VendingMachineState):
    """Zustand: Warten auf Münzeinwurf"""
   
    def insert_coin(self, machine, amount):
        print(f"Eingeworfen: {amount} Yuan")
        machine.balance += amount
        return HasMoneyState()
   
    def select_product(self, machine, product):
        print("Bitte zuerst eine Münze einwerfen")
        return self
   
    def dispense(self, machine):
        print("Bitte zuerst eine Münze einwerfen und ein Produkt auswählen")
        return self
   
    def refund(self, machine):
        print("Kein Betrag zu erstatten")
        return self

class HasMoneyState(VendingMachineState):
    """Zustand: Münze eingeworfen"""
   
    def insert_coin(self, machine, amount):
        print(f"Weiter {amount} Yuan einwerfen")
        machine.balance += amount
        return self
   
    def select_product(self, machine, product):
        if product.price <= machine.balance:
            print(f"Produkt ausgewählt: {product.name}")
            machine.selected_product = product
            return ProductSelectedState()
        else:
            print("Unzureichendes Guthaben, bitte weiter Münzen einwerfen oder ein günstigeres Produkt auswählen")
            return self
   
    def dispense(self, machine):
        print("Bitte wählen Sie zuerst ein Produkt aus")
        return self
   
    def refund(self, machine):
        print(f"Rückerstattung {machine.balance} yuan")
        machine.balance = 0
        return WaitingState()

class ProductSelectedState(VendingMachineState):
    """Zustand: Produkt ausgewählt"""
   
    def insert_coin(self, machine, amount):
        print("Produkt bereits ausgewählt, es können keine weiteren Münzen eingeworfen werden")
        return self
   
    def select_product(self, machine, product):
        print("Produkt bereits ausgewählt, bitte schließen Sie zuerst die aktuelle Transaktion ab")
        return self
   
    def dispense(self, machine):
        product = machine.selected_product
        change = machine.balance - product.price
       
        print(f"Ausgabe: {product.name}")
        if change > 0:
            print(f"Wechselgeld: {change} yuan")
       
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()
   
    def refund(self, machine):
        print(f"Rückerstattung {machine.balance} yuan")
        machine.balance = 0
        machine.selected_product = None
        return WaitingState()

class Product:
    """Product-Klasse"""
   
    def __init__(self, name, price):
        self.name = name
        self.price = price

class VendingMachine:
    """Verkaufsautomat"""
   
    def __init__(self):
        self._state = WaitingState()
        self.balance = 0
        self.selected_product = None
   
    def set_state(self, state):
        self._state = state
   
    def insert_coin(self, amount):
        print(f"Vorgang: Münze einwerfen {amount} Yuan")
        self.set_state(self._state.insert_coin(self, amount))
   
    def select_product(self, product):
        print(f"Vorgang: Produkt {product.name} auswählen")
        self.set_state(self._state.select_product(self, product))
   
    def dispense(self):
        print("Vorgang: Produktausgabe anfordern")
        self.set_state(self._state.dispense(self))
   
    def refund(self):
        print("Vorgang: Rückerstattung anfordern")
        self.set_state(self._state.refund(self))

Vor- und Nachteile des State-Musters

Vorteile

  1. Single-Responsibility-Prinzip: Code, der zu einem bestimmten Zustand gehört, in unabhängige Klassen auslagern
  2. Open-Closed-Prinzip: Neue Zustände einführen, ohne vorhandene Zustandsklassen und den Kontext zu ändern
  3. Bedingte Anweisungen eliminieren: Riesige bedingte Verzweigungsanweisungen durch polymorphe Aufrufe beseitigen
  4. Klare Zustandsübergänge: Zustandsübergänge expliziter machen, wodurch Fehler durch Zustandsübergänge reduziert werden

Nachteile

  1. Mögliche Überkonstruktion: Wenn die Anzahl der Zustände klein ist oder sich selten ändert, kann die Verwendung des State-Musters übermäßig komplex sein
  2. Erhöhte Anzahl von Zustandsklassen: Jeder Zustand erfordert eine entsprechende Klasse, was zu einer Zunahme der Anzahl von Klassen führen kann
  3. Kopplung zwischen Kontext und Zustand: Der Kontext muss alle konkreten Zustandsklassen kennen, um Zustandsübergänge durchzuführen

Übungsaufgaben

Übung 1: Das Aufzugsystem verbessern

Versuchen Sie, die folgenden Funktionen zum Aufzugsystem hinzuzufügen:

  • Fügen Sie einen Zustand "Wartung" hinzu, in dem alle Operationen verboten sind
  • Fügen Sie eine Funktion zur Etagenauswahl hinzu, bei der Etagen nur im gestoppten Zustand ausgewählt werden können
  • Implementieren Sie automatische Zustandsübergänge für die Etagenankunft

Übung 2: Ein Ampelsystem implementieren

Verwenden Sie das State-Muster, um ein Ampelsystem mit drei Zuständen zu implementieren: rotes Licht, grünes Licht und gelbes Licht. Die Regeln für die Zustandsübergänge sind wie folgt:

  • Rotes Licht → Grünes Licht (nach 30 Sekunden)
  • Grünes Licht → Gelbes Licht (nach 25 Sekunden)
  • Gelbes Licht → Rotes Licht (nach 5 Sekunden)

Übung 3: Zustände von Spielcharakteren

Entwerfen Sie ein Zustandssystem für einen Spielcharakter, das die folgenden Zustände umfasst:

  • Normaler Zustand: kann sich bewegen und angreifen
  • Vergifteter Zustand: verliert kontinuierlich Gesundheit, Bewegungsgeschwindigkeit halbiert
  • Betäubter Zustand: kann sich nicht bewegen oder angreifen
  • Unbesiegbarer Zustand: immun gegen Schaden

Zusammenfassung

Das Zustandsmuster ist ein leistungsfähiges Entwurfsmuster, besonders geeignet für Szenarien, in denen das Verhalten eines Objekts von seinem Zustand abhängt und in denen es viele Zustände mit komplexen Übergängen gibt. Indem jeder Zustand als eine unabhängige Klasse gekapselt wird, macht das Zustandsmuster den Code klarer, leichter wartbar und leichter erweiterbar.

Kernpunkte:

  • Das Zustandsmuster eliminiert bedingte Anweisungen, indem es Zustände als Objekte kapselt
  • Der Kontext delegiert zustandsbezogenes Verhalten an das aktuelle Zustandsobjekt
  • Zustandsübergänge können vom Kontext oder von den Zustandsobjekten selbst ausgelöst werden
  • Eine sinnvolle Verwendung des Zustandsmusters kann die Wartbarkeit des Codes erheblich verbessern
Andere Erweiterungen