¿La serialización y deserialización deben ser responsabilidad de la clase que se serializa?

16

Actualmente estoy en la fase de (re) diseño de varias clases de modelos de una aplicación C # .NET. (Modelo como en M de MVC). Las clases modelo ya tienen muchos datos, comportamientos e interrelaciones bien diseñados. Estoy reescribiendo el modelo de Python a C #.

En el viejo modelo de Python, creo que veo una verruga. Cada modelo sabe cómo serializarse, y la lógica de serialización no tiene nada que ver con el resto del comportamiento de ninguna de las clases. Por ejemplo, imagine:

  • Imageclase con un .toJPG(String filePath) .fromJPG(String filePath)método
  • ImageMetaDataclase con un .toString()y .fromString(String serialized)método.

Puede imaginar cómo estos métodos de serialización no son coherentes con el resto de la clase, aunque solo se puede garantizar que la clase conozca datos suficientes para serializarse.

¿Es una práctica común que una clase sepa cómo serializarse y deserializarse? ¿O me falta un patrón común?

kdbanman
fuente

Respuestas:

16

Por lo general, evito que la clase sepa cómo serializarse, por un par de razones. Primero, si desea (des) serializar a / desde un formato diferente, ahora necesita contaminar el modelo con esa lógica adicional. Si se accede al modelo a través de una interfaz, también contaminará el contrato.

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

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

Pero, ¿qué pasa si desea serializarlo a / desde un PNG y GIF? Ahora la clase se convierte

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) { ... }
}

En cambio, normalmente me gusta usar un patrón similar al siguiente:

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) { ... }
}

Ahora, en este punto, una de las advertencias con este diseño es que los serializadores necesitan conocer identityel objeto que está serializando. Algunos dirían que este es un mal diseño, ya que la implementación se filtra fuera de la clase. El riesgo / recompensa de esto depende realmente de usted, pero podría modificar ligeramente las clases para hacer algo como

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);
    }
}

Este es más un ejemplo general, ya que las imágenes generalmente tienen metadatos que lo acompañan; cosas como el nivel de compresión, el espacio de color, etc., que pueden complicar el proceso.

Zymus
fuente
2
Recomiendo que se serialice a / desde un IOStream abstracto o el formato binario (el texto es un tipo específico de formato binario). De esta manera, no está restringido a escribir en un archivo. Querer enviar los datos a través de la red sería una importante ubicación alternativa de salida.
unholysampler
Muy buen punto. Estaba pensando en eso, pero tuve un pedo cerebral. Actualizaré el código.
Zymus
Supongo que a medida que se admitan más formatos de serialización (es decir ImageSerializer, se escriben más implementaciones de la interfaz), la ImageSerializerinterfaz también necesitará crecer. EJ: Un nuevo formato admite compresión opcional, los anteriores no lo hicieron -> agregar configuración de compresión a la ImageSerializerinterfaz. Pero luego, los otros formatos están llenos de características que no se aplican a ellos. Cuanto más lo pienso, menos creo que la herencia se aplique aquí.
kdbanman
Si bien entiendo de dónde vienes, creo que no es un problema, por un par de razones. Si es un formato de imagen existente, lo más probable es que el serializador ya sepa cómo lidiar con los niveles de compresión, y si es uno nuevo, tendrá que escribirlo de todos modos. Una solución es sobrecargar los métodos, algo así como void serialize(Image image, Stream outputStream, SerializerSettings settings);Entonces es solo un caso de conectar la compresión existente y la lógica de metadatos al nuevo método.
Zymus
3

La serialización es un problema de dos partes:

  1. Conocimiento sobre cómo instanciar una estructura de clase aka .
  2. Conocimiento sobre cómo persistir / transferir la información que se necesita para instanciar una clase, también conocida como mecánica .

En la medida de lo posible, la estructura debe mantenerse separada de la mecánica . Esto aumenta la modularidad de su sistema. Si entierra la información en el n. ° 2 dentro de su clase, entonces rompe la modularidad porque ahora su clase debe modificarse para mantener el ritmo de las nuevas formas de serialización (si aparecen).

En el contexto de la serialización de imágenes, mantendría la información sobre la serialización separada de la clase misma y la mantendría más bien en los algoritmos que pueden determinar el formato de serialización, por lo tanto, diferentes clases para JPEG, PNG, BMP, etc. aparece el algoritmo de serialización, simplemente codifica ese algoritmo y su contrato de clase permanece sin cambios.

En el contexto de IPC, puede mantener su clase separada y luego declarar selectivamente la información que se necesita para la serialización (por anotaciones / atributos). Luego, su algoritmo de serialización puede decidir si usar JSON, Google Protocol Buffers o XML para la serialización. Incluso puede decidir si usar el analizador Jackson o su analizador personalizado: ¡hay muchas opciones que obtendrá fácilmente cuando diseñe de forma modular!

Apoorv Khurasia
fuente
1
¿Me puede dar un ejemplo de cómo esas dos cosas se pueden desacoplar? No estoy seguro de entender la distinción.
kdbanman