¿Debería una imagen poder cambiar su tamaño en OOP?

9

Estoy escribiendo una aplicación que tendrá una Imageentidad, y ya estoy teniendo problemas para decidir de quién es la responsabilidad de cada tarea.

Primero tengo la Imageclase. Tiene una ruta, ancho y otros atributos.

Entonces creé una ImageRepositoryclase, para la recuperación de imágenes con un sencillo y probado método, por ejemplo: findAllImagesWithoutThumbnail().

Pero ahora también necesito poder hacerlo createThumbnail(). ¿Quién debería ocuparse de eso? Estaba pensando en tener una ImageManagerclase, que sería una clase específica de la aplicación (también habría un componente reutilizable de manipulación de imágenes de terceros, no estoy reinventando la rueda).

¿O tal vez sería 0K dejar que el cambio de Imagetamaño en sí mismo? ¿O dejar que ImageRepositoryy ImageManagersea ​​la misma clase?

¿Qué piensas?

ChocoDeveloper
fuente
¿Qué hace Image hasta ahora?
Winston Ewert
¿Cómo esperas cambiar el tamaño de las imágenes? ¿Solo estás reduciendo las imágenes o te imaginas querer volver al tamaño completo?
Gort the Robot
@ WinstonEwert Genera datos como la resolución formateada (por ejemplo: la cadena '1440x900'), diferentes URL, y le permite modificar algunas estadísticas como 'vistas' o 'votos'.
ChocoDeveloper
@StevenBurnap Considero que la imagen original es lo más importante, solo uso miniaturas para una navegación más rápida o si el usuario quiere cambiar el tamaño para que se ajuste a su escritorio o algo así, son algo diferente.
ChocoDeveloper
Haría que la clase de imagen sea inmutable para que no tenga que copiarla defensivamente cuando la pase.
dan_waterworth

Respuestas:

8

La pregunta que se hace es demasiado vaga para tener una respuesta real, ya que realmente depende de cómo Imagese van a utilizar los objetos.

Si solo está usando la imagen en un tamaño y está cambiando el tamaño porque la imagen de origen es del tamaño incorrecto, puede ser mejor que el código de lectura cambie el tamaño. Haga que su createImagemétodo tome un ancho / alto y luego devuelva la imagen que ha cambiado de tamaño a ese ancho / alto.

Si necesita varios tamaños, y si la memoria no es una preocupación, es mejor mantener la imagen en la memoria como se leyó originalmente y cambiar el tamaño en el momento de la visualización. En tal caso, hay un par de diseños diferentes que puede usar. La imagen widthy heightsería fija, pero tendría un displaymétodo que tomara una posición y un alto / ancho objetivo o tendría algún método que tomara un ancho / alto que devolviera un objeto que sea cual sea el sistema de visualización que esté utilizando. La mayoría de las API de dibujo que he usado le permiten especificar el tamaño objetivo en el momento del dibujo.

Si los requisitos provocan que el dibujo sea de diferentes tamaños con la frecuencia suficiente como para que el rendimiento sea una preocupación, podría tener un método que cree una nueva imagen de un tamaño diferente en función del original. Una alternativa sería hacer que su Imageclase almacene diferentes representaciones internamente para que la primera vez que llame displaycon el tamaño de miniatura aumente el tamaño, mientras que la segunda vez simplemente dibuje la copia en caché que guardó la última vez. Esto usa más memoria, pero es raro que tenga más de unos pocos cambios de tamaño comunes.

Otra alternativa es tener una sola Imageclase que conserve la imagen base y que esa clase contenga una o más representaciones. Un Imagesí mismo no tendría una altura / ancho. En cambio, comenzaría con uno ImageRepresentationque tuviera altura y anchura. Dibujarías esta representación. Para "redimensionar" una imagen, le pediría Imageuna representación con ciertas métricas de alto / ancho. Esto provocaría que contuviera esta nueva representación, así como la original. Esto le da un gran control sobre exactamente lo que está en la memoria a costa de una complejidad adicional.

Personalmente, no me gustan las clases que contienen la palabra Managerporque "gerente" es una palabra muy vaga que realmente no te dice mucho sobre exactamente lo que hace la clase. ¿Gestiona la vida útil del objeto? ¿Se interpone entre el resto de la aplicación y lo que administra?

Gort the Robot
fuente
"Realmente depende de cómo se van a utilizar los objetos de imagen". Esta afirmación no está realmente en línea con la noción de encapsulación y acoplamiento flojo (aunque en este caso puede llevar el principio demasiado lejos). Un mejor punto de diferenciación sería si el estado de la imagen incluye significativamente el tamaño o no.
Chris Bye
Parece que estás hablando del punto de vista de una aplicación de edición de imágenes. No está del todo claro si OP quería esto o no, ¿eh?
andho
6

Mantenga las cosas simples siempre que solo haya unos pocos requisitos y mejore su diseño cuando sea necesario. Supongo que en la mayoría de los casos del mundo real no hay nada de malo en comenzar con un diseño como ese:

class Image
{
    public Image createThumbnail(int sizeX, int sizeY)
    {
         // ...
         // later delegate the actual resize operation to a separate component
    }
}

Las cosas pueden volverse diferentes cuando necesita pasar más parámetros createThumbnail(), y esos parámetros necesitan una vida propia. Por ejemplo, supongamos que va a crear miniaturas para varios cientos de imágenes, todas con un determinado tamaño de destino, algoritmo o calidad de cambio de tamaño. Eso significará que podría mover createThumbnaila otra clase, por ejemplo, una clase de administrador o una ImageResizerclase, donde el constructor pasa esos parámetros y, por lo tanto, están vinculados a la vida útil del ImageResizerobjeto.

En realidad, comenzaría con el primer enfoque y refactorizaría más tarde cuando realmente lo necesite.

Doc Brown
fuente
bueno, si ya estoy delegando la llamada a un componente separado, ¿por qué no hacer que sea una responsabilidad de una ImageResizerclase en primer lugar? ¿Cuándo delega una llamada en lugar de trasladar la responsabilidad a una nueva clase?
Songo
2
@Songo: 2 posibles razones: (1) el código para delegar al componente separado, tal vez ya existente, no es solo una línea simple, tal vez solo una secuencia de 4 a 6 comandos, por lo que necesita un lugar para almacenarlo . (2) Azúcar sintáctico / facilidad de uso: será muy atractivo para escribir Image thumbnail = img.createThumbnail(x,y).
Doc Brown
+1 aah ya veo. Gracias por la explicación :)
Songo
4

Creo que debería ser parte de la Imageclase, ya que cambiar el tamaño en una clase externa requeriría que el redimensionador conozca la implementación de la clase Image, lo que viola la encapsulación. Supongo que Imagees una clase base, y terminarás con subclases separadas para tipos de imágenes concretas (PNG, JPEG, SVG, etc.). Por lo tanto, deberá tener las clases de cambio de tamaño correspondientes o un cambio de tamaño genérico con una switchdeclaración que cambie de tamaño según la clase de implementación: un olor de diseño clásico.

Un enfoque podría ser que su Imageconstructor tome un recurso que contenga la imagen y los parámetros de altura y anchura y se cree adecuadamente. Luego, cambiar el tamaño podría ser tan simple como crear un nuevo objeto utilizando el recurso original (almacenado en caché dentro de la imagen) y los nuevos parámetros de tamaño. Por ejemplo foo.createThumbnail(), simplemente return new Image(this.source, 250, 250). (con Imageser el tipo concreto de foo, por supuesto). Esto mantiene sus imágenes inmutables y sus implementaciones privadas.

TMN
fuente
Me gusta esta solución, pero no entiendo por qué dice que el redimensionador necesita conocer la implementación interna de Image. Todo lo que necesita es la fuente y las dimensiones de destino.
ChocoDeveloper
Si bien creo que tener una clase externa no tiene sentido porque sería inesperado, no tendría que violar la encapsulación ni requeriría tener una declaración switch () al realizar un cambio de tamaño. En última instancia, una imagen es una matriz 2D de algo, y definitivamente puede cambiar el tamaño de las imágenes siempre que la interfaz permita obtener y configurar píxeles individuales.
whatsisname
4

Sé que OOP se trata de encapsular datos y comportamiento juntos, pero no creo que sea una buena idea que una Imagen tenga la lógica de cambio de tamaño incrustada en este caso, porque una Imagen no necesita saber cómo cambiar el tamaño para ser una imagen.

Una miniatura es en realidad una imagen diferente. Quizás tenga una estructura de datos que mantenga la relación entre una Fotografía y su Miniatura (ambas son Imágenes).

Intento dividir mis programas en cosas (como Imágenes, Fotografías, Miniaturas, etc.) y Servicios (como PhotographRepository, ThumbnailGenerator, etc.). Obtenga las estructuras de datos correctas y luego defina los servicios que le permiten crear, manipular, transformar, persistir y recuperar esas estructuras de datos. No pongo más comportamiento en mis estructuras de datos que asegurarme de que se creen correctamente y se usen adecuadamente.

Por lo tanto, no, una imagen no debe contener la lógica sobre cómo hacer una miniatura. Debe haber un servicio ThumbnailGenerator que tenga un método como:

Image GenerateThumbnailFrom(Image someImage);

Mi estructura de datos más grande podría verse así:

class Photograph : Image
{
    public Photograph(Image thumbnail)
    {
        if(thumbnail == null) throw new ArgumentNullException("thumbnail");
        this.Thumbnail = thumbnail;
    }

    public Image Thumbnail { get; private set; }
}

Por supuesto, eso podría significar que está haciendo un esfuerzo que no desea hacer mientras construye el objeto, por lo que consideraría algo como esto también:

class Photograph : Image
{
    private Image thumbnail = null;
    private readonly Func<Image,Image> generateThumbnail;

    public Photograph(Func<Image,Image> generateThumbnail)
    {
        this.generateThumbnail = generateThumbnail;
    }


    public Image Thumbnail
    {
        get
        {
            if(this.thumbnail == null)
            {
                this.thumbnail = this.generateThumbnail(this);
            }
            return this.thumbnail;
        }
    }
}

... en el caso de que desee una estructura de datos con evaluación diferida. (Lo siento, no incluí mis comprobaciones nulas y no lo hice seguro para subprocesos, que es algo que desearía si intentara imitar una estructura de datos inmutable).

Como puede ver, cualquiera de estas clases está siendo construida por algún tipo de PhotographRepository, que probablemente tiene una referencia a un ThumbnailGenerator que obtuvo a través de la inyección de dependencia.

Scott Whitlock
fuente
Me han dicho que no debería crear clases sin comportamiento. No estoy seguro si cuando dices 'estructuras de datos' te estás refiriendo a las clases o algo de C ++ (¿es este lenguaje?). Las únicas estructuras de datos que conozco y uso son las primitivas. Los servicios y la parte DI son acertados, podría terminar haciendo esto.
ChocoDeveloper
@ChocoDeveloper: ocasionalmente las clases sin comportamiento son útiles o necesarias, dependiendo de la situación. Esas se llaman clases de valor . Las clases normales de OOP son clases con comportamiento codificado. Las clases de OOP componibles también tienen un comportamiento codificado, pero su estructura de composición puede dar lugar a muchos comportamientos necesarios para una aplicación de software.
rwong
3

Ha identificado una única funcionalidad que desea implementar, entonces, ¿por qué no debería estar separada de todo lo que ha identificado hasta ahora? Eso es lo que sugeriría el Principio de responsabilidad única es la solución.

Cree una IImageResizerinterfaz que le permita pasar una imagen y un tamaño de destino, y que devuelva una nueva imagen. Luego cree una implementación de esa interfaz. En realidad, hay muchas maneras de cambiar el tamaño de las imágenes, por lo que incluso podría terminar con más de una.

Chris Pitman
fuente
+1 para SRP relevante, pero no estoy implementando el cambio de tamaño real, que ya está delegado a una biblioteca de terceros como se indicó.
ChocoDeveloper
3

Asumo esos hechos sobre el método, que cambia el tamaño de las imágenes:

  • Debe devolver una nueva copia de la imagen. No puede modificar la imagen en sí, porque rompería otro código que tenga referencia a esta imagen.
  • No necesita acceso a los datos internos de la clase Image. Las clases de imágenes generalmente necesitan ofrecer acceso público a esos datos (o copia de).
  • El cambio de tamaño de la imagen es complejo y requiere muchos parámetros diferentes. Quizás incluso puntos de extensibilidad para diferentes algoritmos de cambio de tamaño. Pasar todo daría como resultado una gran firma de método.

Basado en esos hechos, diría que no hay razón para que el método de cambio de tamaño de la imagen sea parte de la clase Image en sí. Implementarlo como método auxiliar de clase estático sería lo mejor.

Eufórico
fuente
Buenas suposiciones. Sin embargo, no estoy seguro de por qué debería ser un método estático, trato de evitar su comprobabilidad.
ChocoDeveloper
2

Una clase de Procesamiento de imágenes puede ser apropiada (o Administrador de imágenes, como lo llamó). Pase su imagen a un método CreateThumbnail del procesador de imágenes, por ejemplo, para recuperar una imagen en miniatura.

Una de las razones por las que sugeriría esta ruta es que usted dice que está utilizando una biblioteca de procesamiento de imágenes de terceros. Eliminar la funcionalidad de cambio de tamaño de la clase Image en sí misma podría facilitarle el aislamiento de cualquier código de plataforma específico o de terceros. Entonces, si puede usar su clase de imagen base en todas las plataformas / aplicaciones, entonces no tiene que contaminarla con el código específico de la plataforma o biblioteca. Todo eso puede ubicarse en el procesador de imágenes.

Gran maestro B
fuente
Buen punto. La mayoría de las personas aquí no entendieron que ya estoy delegando la parte más complicada en una biblioteca de terceros, tal vez no estaba lo suficientemente claro.
ChocoDeveloper
2

Básicamente como Doc Brown ya dijo:

Cree un getAsThumbnail()método para la clase de imagen, pero este método debería delegar el trabajo en alguna ImageUtilsclase. Entonces se vería algo así:

 class Image{
   // ...
   public Thumbnail getAsThumbnail{
     return ImageUtils.convertToThumbnail(this);
   }
   // ...
 }

Y

 class ImageUtils{
   // ...
   public static Thumbnail convertToThumbnail(Image i){
     // ...
   }
   // ...
 }

Esto permitirá un código más fácil de ver. Compare lo siguiente:

Image i = ...
someComponent.setThumbnail(i.getAsThumbnail());

O

Image i = ...
Thumbnail t = ImageUtils.convertToThumbnail(i);
someComponent.setThumbnail(t); 

Si este último le parece bueno, también puede limitarse a crear este método auxiliar en alguna parte.

Deiwin
fuente
1

Creo que en el "Dominio de imagen", solo tienes el objeto Imagen que es inmutable y monádico. Pide a la imagen una versión redimensionada y devuelve una versión redimensionada de sí misma. Luego puede decidir si desea deshacerse del original o conservar ambos.

Ahora, las versiones en miniatura, avatar, etc. de la Imagen son completamente otro Dominio, que puede solicitar al dominio de la Imagen diferentes versiones de una determinada imagen para darle a un usuario. Por lo general, este dominio tampoco es tan grande o genérico, por lo que probablemente pueda mantener esto en la lógica de la aplicación.

En una aplicación a pequeña escala, cambiaría el tamaño de las imágenes en el momento de la lectura. Por ejemplo, podría tener una regla de reescritura de apache que delegue a php un script si la imagen 'http://my.site.com/images/thumbnails/image1.png', donde se recuperará el archivo usando el nombre image1.png y redimensionado y almacenado en 'thumbnails / image1.png'. Luego, en la siguiente solicitud a esta misma imagen, apache servirá la imagen directamente sin ejecutar el script php. Apache responde automáticamente a su pregunta de findAllImagesWithoutThumbnails, a menos que necesite hacer estadísticas.

En una aplicación a gran escala, enviaría todas las imágenes nuevas a un trabajo en segundo plano, que se encarga de generar las diferentes versiones de la imagen y las guarda en los lugares apropiados. No me molestaría en crear un dominio completo o una clase, ya que es muy poco probable que este dominio se convierta en un terrible desastre de espagueti y salsa picante.

andho
fuente
0

Respuesta corta:

Mi recomendación es agregar estos métodos a la clase de imagen:

public Image getResizedVersion(int width, int height);
public Image getResizedVersion(double percentage);

El objeto Imagen aún es inmutable, estos métodos devuelven una nueva imagen.

Tulains Córdova
fuente
0

Ya hay bastantes respuestas excelentes, por lo que explicaré un poco sobre una heurística detrás de la forma de identificar objetos y sus responsabilidades.

OOP difiere de la vida real en que los objetos en la vida real son a menudo pasivos, y en OOP están activos. Y este es un núcleo del pensamiento de objetos . Por ejemplo, ¿quién cambiaría el tamaño de una imagen en la vida real? Un humano, que es inteligente en ese sentido. Pero en OOP no hay humanos, por lo que los objetos son inteligentes. Una forma de implementar este enfoque "centrado en el ser humano" en OOP es utilizar clases de servicio, por ejemplo, Managerclases notorias . Por lo tanto, los objetos se tratan como estructuras de datos pasivas. No es una forma de OOP.

Entonces hay dos opciones. El primero, crear un método Image::createThumbnail(), ya está considerado. El segundo es crear una ResizedImageclase. Puede ser un decorador de un Image(depende de su dominio si preservar una Imageinterfaz o no), aunque da lugar a algunos problemas de encapsulación, ya ResizedImageque tendría que tener una Imagefuente. Pero an Imageno se abrumaría con el cambio de tamaño de los detalles, dejándolo en un objeto de dominio separado, actuando de acuerdo con un SRP.

Vadim Samokhin
fuente