Czy serializacja i deserializacja powinna być obowiązkiem serializacji klasy?

16

Obecnie jestem w fazie (ponownego) projektowania kilku klas modeli aplikacji C # .NET. (Model jak w M z MVC). Klasy modeli mają już wiele dobrze zaprojektowanych danych, zachowań i wzajemnych powiązań. Przepisuję model z Python do C #.

W starym modelu Pythona myślę, że widzę brodawkę. Każdy model wie, jak dokonać serializacji, a logika serializacji nie ma nic wspólnego z resztą zachowania którejkolwiek z klas. Na przykład wyobraź sobie:

  • Imageklasa z .toJPG(String filePath) .fromJPG(String filePath)metodą
  • ImageMetaDataklasa za pomocą metody .toString()i .fromString(String serialized).

Możesz sobie wyobrazić, w jaki sposób te metody serializacji nie są spójne z resztą klasy, ale tylko klasie można zagwarantować, że zna wystarczającą ilość danych do serializacji.

Czy powszechną praktyką jest, aby klasa umieła serializować i deserializować się? A może brakuje mi wspólnego wzoru?

kdbanman
źródło

Odpowiedzi:

16

Na ogół unikam, aby klasa wiedziała, jak się serializować, z kilku powodów. Po pierwsze, jeśli chcesz (od) serializować do / z innego formatu, musisz teraz zanieczyścić model tą dodatkową logiką. Jeśli dostęp do modelu uzyskuje się przez interfejs, wówczas również zanieczyszczasz umowę.

public class Image
{
    public void toJPG(String filePath) { ... }

    public Image fromJPG(String filePath) { ... }
}

Ale co, jeśli chcesz serializować to do / z PNG i GIF? Teraz klasa staje się

public class Image
{
    public void toJPG(String filePath) { ... }

    public Image fromJPG(String filePath) { ... }

    public void toPNG(String filePath) { ... }

    public Image fromPNG(String filePath) { ... }

    public void toGIF(String filePath) { ... }

    public Image fromGIF(String filePath) { ... }
}

Zamiast tego zazwyczaj lubię używać wzoru podobnego do następującego:

public interface ImageSerializer
{
    void serialize(Image src, Stream outputStream);

    Image deserialize(Stream inputStream);
}

public class JPGImageSerializer : ImageSerializer
{
    public void serialize(Image src, Stream outputStream) { ... }

    public Image deserialize(Stream inputStream) { ... }
}

public class PNGImageSerializer : ImageSerializer
{
    public void serialize(Image src, Stream outputStream) { ... }

    public Image deserialize(Stream inputStream) { ... }
}

public class GIFImageSerializer : ImageSerializer
{
    public void serialize(Image src, Stream outputStream) { ... }

    public Image deserialize(Stream inputStream) { ... }
}

W tym momencie jednym z zastrzeżeń dotyczących tego projektu jest to, że serializatory muszą znać identityobiekt, który serializuje. Niektórzy twierdzą, że jest to zły projekt, ponieważ implementacja wycieka poza klasę. Ryzyko / zysk zależy od ciebie, ale możesz nieco ulepszyć klasy, aby zrobić coś takiego

public class Image
{
    public void serializeTo(ImageSerializer serializer, Stream outputStream)
    {
        serializer.serialize(this.pixelData, outputStream);
    }

    public void deserializeFrom(ImageSerializer serializer, Stream inputStream)
    {
        this.pixelData = serializer.deserialize(inputStream);
    }
}

Jest to bardziej ogólny przykład, ponieważ obrazy zwykle mają metadane, które są z nimi zgodne; rzeczy takie jak poziom kompresji, przestrzeń kolorów itp., które mogą skomplikować proces.

Zymus
źródło
2
Polecam serializację do / z abstrakcyjnego IOStream lub formatu binarnego (tekst jest specyficznym rodzajem formatu binarnego). W ten sposób nie jesteś ograniczony do zapisu do pliku. Chęć przesłania danych przez sieć byłaby ważną alternatywną lokalizacją wyjściową.
unholysampler
Bardzo dobra uwaga. Myślałem o tym, ale miałem pierdnięcie mózgu. Zaktualizuję kod.
Zymus
Zakładam, że w miarę obsługiwania większej liczby formatów serializacji (tzn. Zapisywania większej liczby implementacji ImageSerializerinterfejsu) ImageSerializerinterfejs również musi się rozwijać. EX: Nowy format obsługuje opcjonalną kompresję, poprzednie nie -> dodawały konfigurowalność kompresji do ImageSerializerinterfejsu. Ale potem inne formaty są zaśmiecone funkcjami, które ich nie dotyczą. Im więcej o tym myślę, tym mniej myślę, że dziedziczenie ma tutaj zastosowanie.
kdbanman,
Rozumiem, skąd pochodzisz, ale z kilku powodów nie jest to problem. Jeśli jest to istniejący format obrazu, istnieje prawdopodobieństwo, że serializator już wie, jak radzić sobie z poziomami kompresji, a jeśli jest to nowy, i tak musisz go napisać. Jednym z rozwiązań jest przeładowanie metod, coś w rodzaju. void serialize(Image image, Stream outputStream, SerializerSettings settings);To tylko połączenie istniejącej logiki kompresji i metadanych z nową metodą.
Zymus,
3

Serializacja to problem dwuczęściowy:

  1. Wiedza na temat tworzenia instancji struktury aka klasy .
  2. Wiedza na temat utrwalania / przekazywania informacji potrzebnych do utworzenia instancji mechaniki aka .

O ile to możliwe, konstrukcja powinna być oddzielona od mechaniki . Zwiększa to modułowość twojego systemu. Jeśli zakopiesz informacje na temat nr 2 w swojej klasie, wówczas przerwiesz modułowość, ponieważ teraz twoja klasa musi zostać zmodyfikowana, aby nadążyć za nowymi sposobami serializacji (jeśli się pojawią).

W kontekście serializacji obrazów informacje o serializacji byłyby oddzielone od samej klasy i trzymane raczej w algorytmach, które mogą określać format serializacji - dlatego różne klasy dla JPEG, PNG, BMP itp. Jeśli jutro nowa algorytm serializacji przychodzi po prostu kod tego algorytmu, a umowa klasy pozostaje niezmieniona.

W kontekście IPC możesz rozdzielić klasę, a następnie selektywnie zadeklarować informacje potrzebne do serializacji (za pomocą adnotacji / atrybutów). Następnie algorytm serializacji może zdecydować, czy do serializacji użyć JSON, buforów protokołu Google, czy XML. Może nawet zdecydować, czy użyć parsera Jacksona, czy niestandardowego - istnieje wiele opcji, które można łatwo uzyskać przy projektowaniu modułowym!

Apoorv Khurasia
źródło
1
Czy możesz podać mi przykład, w jaki sposób te dwie rzeczy można oddzielić? Nie jestem pewien, czy rozumiem to rozróżnienie.
kdbanman