Jakiś czas temu pisalem w pracy serwer gier, technologie: Java, Spring, Redis, Websocket & STOMP.
Kod, który napisałem działa, są testy jednak z perspektywy czasu uważam, że chyba źle to podzieliłem
architektonicznie.
Zacznijmy może od tego jak działa prosta architektura warstowa.
Mamy API czyli zestaw nazwijmy to kontrolerów, które przyjmują żądania.
Kontrolery powinny być jak najmniejsze, ich odpowiedzialnością jest tylko
aby przyjąć żądanie i oddelegować daną czynność do serwisu, który jest
odpowiedzialny za wykonanie logiki biznesowej i zwrócenie wyniku następnie
wynik jest zwracany do żądającego. Kontrolerem w tym przypadku nie musi
być kontroler restowy, właśnie dlatego taki podział, wydzielamy (separation of concerns)
naszą logikę biznesową, nasze funkcje systemu do serwisów, fasad aby
później mogły być one wywoływane z dowolnego api "klienckiego" rest, kolejka, konsola itd.
Serwer, który pisałem był tylko jednym z mikroserwisów całego projektu.
Podzieliłem ten serwis na moduły, omówię tylko 2 najważniejsze.
- game-server
- entrypoint
- storage
entrypoint
- tutaj miałem wszystkie kontrolery głównie przyjmujące eventy websocketowe
i rzeczy zwiazane z infrastruktura, konfiguracja - moduł ten zależy od modułu
storage
poprzez zależność w maven
storage
- modul ten wystawial serwisy, które tak naprawde zapisywały stan gry
do redisa w postaci eventow - w tym module byla tak naprawde cala logika biznesowa gry
- tutaj byl pakiet
model
, w ktorym byly bogate klasy nieanemiczne
które realizowaly logike biznesową - nad klasami z pakietu
model
były serwisy, które w skrócie pracowały na modelu
i po wykonaniu akcji biznesowej zapisywały zmiany do redisa zmieniając w ten sposób stan gry - stan gry trzeba bylo gdzies zapisywac poniewaz serwer działa na wielu instancjach
Flow servera jest takie, że przychodzi event websocketowy w entrypoint
w kontrolerze
następnie ten kontroler deleguje do jakiegoś serwisu z storage
, serwis zwraca jakiś
wynik następnie na podstawie tego wyniku coś jest robione np. leci event po websocketach,
że gra się rozpoczeła.
Problem w tym, że kontrolery w entrypoint
rozrosły mi się,
tak naprawdę też po części wykonują logikę biznesową.
Dodatkowo zastosowałem sterowanie programem poprzez wyjątki,
w każdej metodzie kontrolera łapane są jakieś wyjątki rzucane
z storage
.
Przykład (wyimaginowany):
@Controller
public class SomeGameController {
private final SomeGameServiceFromStorage gameService;
private final SimpMessagingTemplate template;
@MessageMapping("/player/wants/do/something")
public void playerWantsDoSomething(PlayerCommand playerCommand, Principal principal) {
String playerId = principal.getPlayerId();
try {
gameService.doSomethingInteresting(playerId, playerCommand);
template.convertAndSend("/all/playerDidSomethingInteresting", new Message("Not important"));
if(gameService.isWinnerForThisRound(playerId)) {
gameService.endGameForPlayer(playerId);
}
} catch(GameNotStartedException e) {
gameService.doSomethingElse(playerId);
template.convertAndSendToUser("/error", new Error("GameNotStarted!"));
} catch(TooManyWrongAnswers e) {
gameService.endGameForPlayer(playerId);
template.convertAndSendToUser("/endGame", new GameEnded("Too many wrong answers!"));
}
}
}
Chyba widać problem...
Na początku wydawało mi się, że rozdzielenie tego w taki sposób będzie
ok ponieważ tj. wcześniej wspominałem kontrolery przyjmują input i zwracają
output a między tym delegują do serwisów, które zwracają wynik. Serwisy nie powinny
wiedzieć kto i jak pyta, czy restem czy przez konsole czy jeszcze inaczej.
One po prostu zwracają wynik w postaci jakiegoś obiektu dto nie powinny też wiedzieć o tym jak zwrócić wynik
dla użytkownka.
Tak myślałem do tej pory ale to chyba się sprawdza tylko w rest i ogolnie webie.
W rest mamy prostsze flow gdzie serwis wykonuje logikę biznesową i zwraca wynik i tyle.
W moim serwerze gier na websocketach gdy jakiś event przyjdzie do systemu to wykonuje się jakaś
logika i chyba w tej logice "pomiędzy wierszami" powinienem komunikować się protokołem websocket
i wysyłać jakieś dane co się zadziało w grze. Teraz tj. możecie zobaczyć na przykładzie
gdy użytkownik coś chce zrobić to ja wykonuje jakąś logikę biznesową na serwisie
a pózniej pytam co ta logika spowodowała i na tej podstawie wysyłam coś do klienta/ów i jakoś reaguje
na to co się zdarzyło w grze.
Dzięki temu osiągnąłem niby to, że serwis nic nie wie, nie jest związany
z danym protokołem. Ale teraz myśle, że to jest średnie rozwiązanie, i jeszcze te wyjątki, które sterują
przebiegiem programu...
Jestem ciekaw co myślicie na ten temat.