Dziś prezentuję kolejny wzorzec projektowy, który pozwala uniknąć drabinek if-else if. Mowa tutaj o wzorcu state, który idealnie nada się, jeśli posiadasz różne stany w swoim systemie oraz chcesz mieć możliwość płynnego przechodzenia pomiędzy nimi.
Wzorzec stan
Czynnościowy (behawioralny) wzorzec projektowy stan, pozwala na przechowywanie reprezentacji i zachowań konkretnego stanu systemu w pojedynczym obiekcie. Zamiast deklaracji wielu zmiennych, funkcji i klas, które umownie grupujemy, jako zestaw pewnych zachowań w obrębie logiki, możemy uspójnić nasz kod poprzez zastosowanie wzorca state.
Stosowanie tego wzorca ma najwięcej sensu, kiedy funkcje klas pełne są instrukcji warunkowych zmieniających ich zachowania (stan), w zależności od zestawu właściwości czy wartości pól w obrębie klasy. Inaczej mówiąc, jeżeli Twój kod zmienia swoją reprezentację, kiedy zmieniają się jego właściwości, to znak, że powinieneś/aś rozważyć zastosowanie tego pattern-u.
Problem
Wyobraźmy sobie, że projektujemy grę. Gracz może znajdować się w trzech stanach: żyje, umarł, jest sparaliżowany. W zależności od stanu w jakim się znajduje zmienia się jego zachowanie. Przykładowo, zmarli nie mogą się poruszać, a w przypadku paraliżu mowa jest niezrozumiała przez innych.
enum PlayerStatus {
alive = 'alive',
paralyzed = 'paralyzed',
dead = 'dead',
}
class Player {
constructor(
private readonly id: PlayerId,
private status: PlayerStatus,
private readonly name: string,
private speed: number,
private position: Position,
) {}
move(direction: MoveDirection) {
if (this.status === PlayerStatus.dead) return;
let movementSpeed = this.speed;
if (this.status === PlayerStatus.paralyzed) {
movementSpeed = this.speed / 10;
}
this.position = this.position.change(
direction,
speed,
);
}
say(speech: string): string {
if (this.status === PlayerStatus.dead) return '';
if (this.status === PlayerStatus.paralyzed) {
return `${this.name}: ipposadobvmannmnvc ljsdasalndnsa nbdsad`;
}
return `${this.name}: ${speech}`;
}
}
Teraz wyobraźmy sobie, że dochodzi kolejny stan postaci: boosted. W tym stanie prędkość poruszania się jest dwukrotnie wyższa. Musimy wtedy w obydwu metodach say() i move() dopisać kolejne warunki. Z drugiej strony, co w przypadku, jeżeli dojdzie nowa metoda, np. attack()?
Przykład wzorca stan
Zamiast powielać if-ologię, spróbujmy zamknąć zachowania dedykowane poszczególnych stanom w jeden obiekt. Następnie zapiszmy ten obiekt w samym Playerze. Teraz, w zależności od stanu w którym znajduje się gracz będziemy dynamicznie wywoływać odpowiednią logikę.
interface PlayerState {
move(direction: Direction);
say(speech: string): string;
}
class DeadPlayer implements PlayerState {
constructor(
private readonly player: Player
) {}
move(direction: MoveDirection) {}
say(speech: string): string {
return '';
}
}
class ParalyzedPlayer implements PlayerState {
constructor(
private readonly player: Player
) {}
move(direction: MoveDirection) {
this.player.changePosition(
direction,
this.player.getSpeed() / 10,
);
}
say(speech: string): string {
return `${this.player.getName()}: ipposadobvmannmnvc ljsdasalndnsa nbdsad`;
}
}
W ten sposób zdefiniowane stany mogą być zmieniane w obrębie obiektu Player. Musimy w tym celu udostępnić odpowiednie metody, które będą modyfikowały stan gracza.
class Player {
constructor(
private readonly id: PlayerId,
private readonly name: string,
private speed: number,
private position: Position,
private state = new AlivePlayer(this)
) {}
kill() {
this.state = new DeadPlayer(this);
}
paralyze() {
this.state = new ParalyzedPlayer(this);
}
reBorn() {
this.state = new AlivePlayer(this);
}
move(direction: MoveDirection) {
this.state.move(direction);
}
say(speech: string): string {
return this.state.say(speech);
}
}
State vs. Strategy
Stan może do złudzenia przypominać wzorzec strategii, o którym już wspominałem. Poszczególne stany mogą posiadać odwołania pomiędzy sobą. Możemy także zmieniać stan obiektu w aktualnym stanie. Przykładowo, jeżeli gracz wypije miksturę uleczenia w stanie paraliżu, jego status powinien automatycznie zostać zamieniony na żywy. W przypadku strategy pattern, konkretne strategie nie powinny nic o sobie wiedzieć. Strategie są od siebie odizolowane i niezależne.
Podsumowanie
- Stan jest czynnościowym wzorcem projektowym pozwalającym na ograniczenie ifologii w kodzie.
- Jeżeli Twoje klasy pełne są instrukcji warunkowych, które sprawdzają pola klasy w celu wybrania konkretnego zachowania, to ten wzorzec jest dla Ciebie.
- Stan przypomina wzorzec strategii, jednak różnią się one od siebie i rozwiązują inne problemy.
Dodaj komentarz