Dobra, poddaję się - zapytam, bo nie widzę jak to zrobić normalnie.
Serwer wysyła klientowi JSONy, które określają, co się stało w grze. Jedna tura składa się z listy takich JSONów.
Jak do niedawna wyglądały klasy DTO na tę potrzebę?
public class BattleUpdate
{
public int? teamNo;
public string command;
public string[] details;
}
details
czyli JUŻ ZSERIALIZOWANE pola, które koncepcyjnie - jak by się zdawało - winny być polami klas dziedziczących. Takie zagnieżdżone JSONy wydały mi się być brzydkie, a ponadto tablica pól nic o nich nie mówi (nieco lepszy byłby słownik zserializowanych pól, ale - poprawcie mnie, jeśli się mylę - nadal wydawałby mi się brzydki).
Już zserializowane pola... Nawet nie zawsze do JSONa. Czasami robiłem kludge'y, które, po bliższemu przyjrzeniu się, były subtelnie błędne. Oto kawałek JSowego kodu parsującego wiadomość od serwera, której pole commad
jest równe DisplayNotice
:
let message = pieceOfUpdate.details[0].
replace("monname", (match, offset, string) => (player ? (own ? team[mon] : (offset == 0 ? 'The opposing ' : 'the opposing ') + team[mon]) : (team[mon] + ' of ' + sideNick))).
replace("playername", (match, offset, string) => sideNick).
replace("thisname", (match, offset, string) => ownNick).
replace("thatname", (match, offset, string) => enemyNick)
Niech któryś gracz da sobie nick thisname
i problem gotowy.
Dlatego byłem z siebie dumny, gdy przerobiłem powyższe na coś takiego:
public abstract class ClientUpdate
{
public abstract string Type { get; }
public int? Team { get; private set; }
}
public class StartPhase : ClientUpdate
{
public override string Type => "StartPhase";
public string Phase { get; private set; }
}
public class DisplayResources : SpeciesUpdate
{
public override string Type => "DisplayResources";
public long Health { get; private set; }
public long Stamina { get; private set; }
public long Shield { get; private set; }
}
Itd, itp. (SpeciesUpdate
dziedziczy po ClientUpdate
i zawiera jeszcze pole z id potworka, którego tyczy się wiadomość) A nawet mamy tu matrioszkę:
public class ConsoleInfo : ClientUpdate
{
public override string Type => "ConsoleInfo";
public List<ConsoleInfoPart> Parts { get; private set; }
public string Importance { get; private set; }
}
public abstract class ConsoleInfoPart
{
public abstract string Type { get; }
}
public class StringPart : ConsoleInfoPart
{
public override string Type => "String";
public string Content { get; private set; }
}
public class NickPart : ConsoleInfoPart
{
public override string Type => "Nick";
public string Side { get; private set; }
}
(rozwiązanie problemu thisname
)
public class PlaceVisibleMonEffect : SpeciesUpdate
{
public override string Type => "PlaceVisibleMonEffect";
public VisibleEffect Effect { get; private set; }
}
Oczywiście VisibleEffect
to kolejna klasa abstrakcyjna... Niektóre efekty też mają argumenty. Przykładem jest Ablaze
(czyli po prostu burn, ale burn już jest w pokemonach, więc chciałem uniknąć powtarzania nazwy), który daje damage over time oraz debuff do szybkości potworka, więc klient powinien wyświetlić nie tylko nazwę i ikonkę ale i siłę tego efektu.
Ze strony JSowej parsowanie tego wygląda ładnie, polimorficzna serializacja jest bez problemu obsługiwana przez Json.NET... ALE jest problem, który przeoczyłem - Deserializowanie tego JSONA w C#! Dlaczego potrzebne: Replaye (czyli listy takich update'ów) są zapisywane do bazy danych. Na życzenie usera wysyłamy replay dowolnej gry. No to deserializujemy update'y z bazy i wysyłamy je klientowi. (Dałoby się to obejść i wysyłać już zserializowane dane, ale to byłby kolejny kludge - tak czy siak co raz zostało zserializowane, przydałoby się zdeserializować - jak nie na potrzeby obecne, to może przyszłe).
To da się zrobić - albo implementować własne deserializatory (rrgh, przerost formy nad treścią) albo użyć wyklętego TypeNameHandling w Json.NET. (możnaby wtedy nawet pozbyć się tej property Type
, albo jeszcze lepiej nie - niech Type
będzie na potrzeby klienta, a $type
na wewnętrzne potrzeby serwera).
Jednak - jak się rozglądam po internecie to ciągle widzę ostrzeżenia, żeby polimorfizmu starać się unikać w JSONach. Nie mogę oprzeć się wrażeniu, że robię coś głupiego, coś czego nie powinienem robić, tylko nie wiem jeszcze, co powinienem robić zamiast tego.
Jak więc tego rodzaju rzeczy robi się porządnie? Jak wyglądałoby modelowe rozwiązanie tego problemu?