Jak porządnie zrobić (de)serializację z/do JSON na potrzeby komunikacji z klientem...

0

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?

2
kmph napisał(a):

To da się zrobić - albo implementować własne deserializatory (rrgh, przerost formy nad treścią) albo użyć wyklętego TypeNameHandling w Json.NET

Cudów nie ma, jak chcesz mieć statyczne typowanie, to musisz jakoś zapisać typ (czyli jakieś pole explicite) albo rozpoznać go automatycznie na podstawie zawartości (na przykład po zestawie pól). I tak, to jest zrobienie tego porządnie.

W dynamicznie typowanych językach (jak JS) tego rodzaju problemy po prostu nie występują

Występują tak samo. Esencją problemu nie jest parsowanie "per se" tylko użycie tych danych potem. Żeby ich użyć, musisz znać strukturę (albo mieć sensowne założenia pozwalające na "dynamiczne" obsłużenie dowolnych danych), więc musisz tak samo rozpoznać obiekt, jedynie odbywa się to później. W ogóle, stwierdzenie W dynamicznie typowanych językach (jak JS) tego rodzaju problemy po prostu nie występują jest niepoprawne niemal z definicji, bo w C# też można użyć słownika i wtedy sytuacja jest zredukowana do tej z JS. Jeżeli umiemy rozwiązać ten problem w JS, to umiemy go rozwiązać w C#.

1

No i w C# też jest dynamic, więc można go używać jako dynamicznie typowanego. Jeśli oczywiście ktoś lubi przybić sobie rękę gwoździem do stołu, a potem biegać wokół niego.

Ja osobiście preferuję porównanie struktury przychodzącego JSONa ze strukturą klas C#, to jest raptem kilka linijek refleksji. Do tego jakiś atrybut, żeby oznaczyć te pola/właściwości, które mogą być polimorficzne.

1 użytkowników online, w tym zalogowanych: 0, gości: 1