Une image doit-elle pouvoir se redimensionner dans la POO?

9

J'écris une application qui aura une Imageentité et j'ai déjà du mal à décider à qui incombera chaque tâche.

J'ai d'abord la Imageclasse. Il a un chemin, une largeur et d'autres attributs.

Ensuite , je créé une ImageRepositoryclasse, pour récupérer des images avec une méthode simple et testé, par exemple: findAllImagesWithoutThumbnail().

Mais maintenant, je dois aussi pouvoir createThumbnail(). Qui devrait s'en occuper? Je pensais avoir une ImageManagerclasse, qui serait une classe spécifique à l'application (il y aurait aussi un composant réutilisable de manipulation d'image tiers de choix, je ne réinvente pas la roue).

Ou peut-être que ce serait 0K pour laisser le Imageredimensionnement lui-même? Ou laissez le ImageRepositoryet ImageManagerêtre la même classe?

Qu'est-ce que tu penses?

ChocoDeveloper
la source
Que fait Image jusqu'à présent?
Winston Ewert
Comment comptez-vous redimensionner les images? Êtes-vous en train de rétrécir des images ou pouvez-vous imaginer vouloir revenir à la taille réelle?
Gort the Robot
@WinstonEwert Il génère des données comme la résolution formatée (par exemple: la chaîne '1440x900'), différentes URL, et vous permet de modifier certaines statistiques comme 'vues' ou 'votes'.
ChocoDeveloper
@StevenBurnap Je considère que l'image originale est la chose la plus importante, j'utilise uniquement des miniatures pour une navigation plus rapide ou si l'utilisateur veut redimensionner pour s'adapter à son bureau ou quelque chose, donc c'est une chose distincte.
ChocoDeveloper
Je rendrais la classe Image immuable afin que vous n'ayez pas à la copier défensivement lorsque vous la faites circuler.
dan_waterworth

Réponses:

8

La question posée est trop vague pour avoir une vraie réponse car elle dépend vraiment de la façon dont les Imageobjets vont être utilisés.

Si vous n'utilisez l'image qu'à une seule taille et que vous redimensionnez car l'image source n'est pas de la bonne taille, il peut être préférable que le code lu effectue le redimensionnement. Demandez à votre createImageméthode de prendre une largeur / hauteur, puis de renvoyer l'image ayant été redimensionnée à cette largeur / hauteur.

Si vous avez besoin de plusieurs tailles, et si la mémoire n'est pas un problème, il est préférable de conserver l'image en mémoire telle qu'elle a été lue à l'origine et d'effectuer un redimensionnement au moment de l'affichage. Dans un tel cas, vous pouvez utiliser plusieurs modèles différents. L'image widthet heightserait fixe, mais vous auriez soit une displayméthode qui prendrait une position et une hauteur / largeur cible, soit une méthode qui prendrait une largeur / hauteur qui renvoyait un objet qui, quel que soit le système d'affichage que vous utilisez. La plupart des API de dessin que j'ai utilisées vous permettent de spécifier la taille cible au moment du dessin.

Si les exigences entraînent un dessin de différentes tailles suffisamment souvent pour que les performances soient un problème, vous pouvez avoir une méthode qui crée une nouvelle image d'une taille différente basée sur l'original. Une alternative serait d'avoir votre Imageclasse cache différentes représentations en interne afin que la première fois que vous appelez displayavec la taille de la vignette, il redimensionne tandis que la deuxième fois, il dessine simplement la copie en cache qu'il a enregistrée la dernière fois. Cela utilise plus de mémoire, mais il est rare que vous ayez plus de quelques redimensionnements courants.

Une autre alternative consiste à avoir une seule Imageclasse qui conserve l'image de base et à ce que cette classe contienne une ou plusieurs représentations. Un Imagelui-même n'aurait pas de hauteur / largeur. Au lieu de cela, il commencerait par celui ImageRepresentationqui avait une hauteur et une largeur. Vous dessinez cette représentation. Pour «redimensionner» une image, vous demanderiez Imageune représentation avec certaines mesures de hauteur / largeur. Cela la ferait alors contenir cette nouvelle représentation ainsi que l'original. Cela vous donne beaucoup de contrôle sur exactement ce qui traîne en mémoire au prix d'une complexité supplémentaire.

Personnellement, je n'aime pas les classes qui contiennent le mot Managerparce que "manager" est un mot très vague qui ne vous dit pas grand-chose sur ce que fait la classe. Gère-t-il la durée de vie des objets? Se situe-t-il entre le reste de l'application et la chose qu'il gère?

Gort le robot
la source
"cela dépend vraiment de la façon dont les objets Image vont être utilisés." Cette affirmation n'est pas vraiment conforme à la notion d'encapsulation et de couplage lâche (bien qu'elle puisse pousser le principe un peu trop loin dans ce cas). Un meilleur point de différenciation serait de savoir si l'état de l'image inclut ou non la taille de manière significative.
Chris Bye
Il semble que vous parliez du point de vue d'une application de retouche d'image. Pas tout à fait clair si OP le voulait ou pas hein?
andho
6

Gardez les choses simples tant qu'il n'y a que quelques exigences et améliorez votre conception lorsque vous le devez. Je suppose que dans la plupart des cas réels, il n'y a rien de mal à commencer avec un design comme celui-ci:

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

Les choses peuvent devenir différentes lorsque vous devez transmettre plus de paramètres à createThumbnail(), et ces paramètres ont besoin d'une durée de vie qui leur est propre. Par exemple, supposons que vous allez créer des miniatures pour plusieurs centaines d'images, toutes avec une taille de destination, un algorithme de redimensionnement ou une qualité donnés. Cela signifie que vous pouvez déplacer le createThumbnaildans une autre classe, par exemple, une classe de gestionnaire ou une ImageResizerclasse, où ces paramètres sont passés par le constructeur et sont donc liés à la durée de vie de l' ImageResizerobjet.

En fait, je commencerais par la première approche et refaçonnerais plus tard quand j'en aurais vraiment besoin.

Doc Brown
la source
eh bien, si je délègue déjà l'appel à un composant séparé, pourquoi ne pas en faire la responsabilité d'une ImageResizerclasse en premier lieu? Quand déléguez-vous un appel au lieu de déplacer la responsabilité dans une nouvelle classe?
Songo
2
@Songo: 2 raisons possibles: (1) le code de délégation au composant séparé - peut-être déjà existant - n'est pas seulement un simple one-liner, peut-être juste une séquence de 4 à 6 commandes, donc vous avez besoin d'un endroit pour le stocker . (2) Sucre syntaxique / facilité d'utilisation: ce sera juste très beau à écrire Image thumbnail = img.createThumbnail(x,y).
Doc Brown
+1 aah je vois. Merci pour l'explication :)
Songo
4

Je pense que cela devrait faire partie de la Imageclasse, car le redimensionnement dans une classe externe nécessiterait que le redimensionneur connaisse la mise en œuvre du Image, violant ainsi l'encapsulation. Je suppose que Imagec'est une classe de base, et vous vous retrouverez avec des sous-classes distinctes pour les types d'images concrètes (PNG, JPEG, SVG, etc.). Par conséquent, vous devrez soit avoir des classes de redimensionnement correspondantes, soit un redimensionneur générique avec une switchinstruction qui se redimensionne en fonction de la classe d'implémentation - une odeur de conception classique.

Une approche pourrait être de demander à votre Imageconstructeur de prendre une ressource contenant l'image, les paramètres de hauteur et de largeur et de se créer de manière appropriée. Le redimensionnement peut alors être aussi simple que la création d'un nouvel objet à l'aide de la ressource d'origine (mise en cache dans l'image) et des nouveaux paramètres de taille. Par exemple foo.createThumbnail(), tout simplement return new Image(this.source, 250, 250). (avec Imagele type concret de foo, bien sûr). Cela garde vos images immuables et leurs implémentations privées.

TMN
la source
J'aime cette solution, mais je ne comprends pas pourquoi vous dites que le resizer doit connaître l'implémentation interne de Image. Tout ce dont il a besoin, c'est de la source et des dimensions cibles.
ChocoDeveloper
Bien que je pense qu'avoir une classe externe n'a pas de sens car ce serait inattendu, cela ne devrait ni violer l'encapsulation ni exiger une instruction switch () lors d'un redimensionnement. En fin de compte, une image est un tableau 2D de quelque chose, et vous pouvez certainement redimensionner des images tant que l'interface permet d'obtenir et de définir des pixels individuels.
whatsisname
4

Je sais que la POO consiste à encapsuler des données et un comportement ensemble, mais je ne pense pas que ce soit une bonne idée pour une image d'avoir la logique de redimensionnement intégrée dans ce cas, car une image n'a pas besoin de savoir comment se redimensionner pour être une image.

Une vignette est en fait une image différente. Peut-être pourriez-vous avoir une structure de données qui détient la relation entre une photographie et sa miniature (qui sont toutes deux des images).

J'essaie de diviser mes programmes en choses (comme des images, des photographies, des miniatures, etc.) et des services (comme PhotographRepository, ThumbnailGenerator, etc.). Obtenez vos bonnes structures de données, puis définissez les services qui vous permettent de créer, manipuler, transformer, persister et récupérer ces structures de données. Je ne mets pas plus de comportement dans mes structures de données que de m'assurer qu'elles sont créées correctement et utilisées de manière appropriée.

Par conséquent, non, une image ne doit pas contenir la logique de création d'une miniature. Il devrait y avoir un service ThumbnailGenerator qui a une méthode comme:

Image GenerateThumbnailFrom(Image someImage);

Ma plus grande structure de données pourrait ressembler à ceci:

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

    public Image Thumbnail { get; private set; }
}

Bien sûr, cela pourrait signifier que vous faites des efforts que vous ne voulez pas faire lors de la construction de l'objet, donc je considérerais aussi quelque chose comme ça:

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

... dans le cas où vous souhaitez une structure de données avec évaluation paresseuse. (Désolé, je n'ai pas inclus mes vérifications nulles et je ne l'ai pas rendu thread-safe, ce que vous voudriez si vous essayiez d'imiter une structure de données immuable).

Comme vous pouvez le voir, l'une ou l'autre de ces classes est construite par une sorte de PhotographRepository, qui a probablement une référence à un ThumbnailGenerator obtenu via l'injection de dépendances.

Scott Whitlock
la source
On m'a dit que je ne devrais pas créer de classes sans comportement. Je ne sais pas si quand vous dites «structures de données», vous faites référence aux classes ou à quelque chose de C ++ (est-ce ce langage?). Les seules structures de données que je connais et utilisent sont les primitives. Les services et la partie DI sont parfaits, je pourrais finir par le faire.
ChocoDeveloper
@ChocoDeveloper: parfois des classes sans comportement sont utiles ou nécessaires, selon la situation. Ce sont des classes de valeurs . Les classes OOP normales sont des classes avec un comportement codé en dur. Les classes OOP composables ont également un comportement codé en dur, mais la structure de leur composition peut donner lieu à de nombreux comportements nécessaires à une application logicielle.
rwong
3

Vous avez identifié une seule fonctionnalité que vous souhaitez implémenter, alors pourquoi ne devrait-elle pas être distincte de tout ce que vous avez identifié jusqu'à présent? C'est ce que le principe de responsabilité unique suggérerait est la solution.

Créez une IImageResizerinterface qui vous permet de passer une image et une taille cible, et qui renvoie une nouvelle image. Créez ensuite une implémentation de cette interface. Il existe en fait des tonnes de façons de redimensionner les images, vous pouvez donc vous retrouver avec plusieurs!

Chris Pitman
la source
+1 pour le SRP pertinent, mais je n'implémente pas le redimensionnement réel, qui est déjà délégué à une bibliothèque tierce comme indiqué.
ChocoDeveloper
3

Je suppose que ces faits sur la méthode, qui redimensionne les images:

  • Il doit renvoyer une nouvelle copie de l'image. Vous ne pouvez pas modifier l'image elle-même, car cela casserait tout autre code faisant référence à cette image.
  • Il n'a pas besoin d'accéder aux données internes de la classe Image. Les classes d'images doivent généralement offrir un accès public à ces données (ou copie).
  • Le redimensionnement de l'image est complexe et nécessite de nombreux paramètres différents. Peut-être même des points d'extensibilité pour différents algorithmes de redimensionnement. Passer tout résulterait en une grande signature de méthode.

Sur la base de ces faits, je dirais qu'il n'y a aucune raison pour que la méthode de redimensionnement d'image fasse partie de la classe d'image elle-même. Il serait préférable de l'implémenter comme méthode auxiliaire statique de classe.

Euphorique
la source
Bonnes hypothèses. Je ne sais pas pourquoi cela devrait être une méthode statique, j'essaie de les éviter pour la testabilité.
ChocoDeveloper
2

Une classe de traitement d'image peut être appropriée (ou Image Manager, comme vous l'avez appelé). Passez votre image à une méthode CreateThumbnail du processeur d'image, par exemple, pour récupérer une image miniature.

L'une des raisons pour lesquelles je suggère cette voie est que vous dites que vous utilisez une bibliothèque de traitement d'images tierce. La suppression de la fonctionnalité de redimensionnement de la classe Image elle-même peut vous permettre d'isoler plus facilement tout code spécifique à une plate-forme ou tiers. Donc, si vous pouvez utiliser votre classe Image de base sur toutes les plates-formes / applications, vous n'avez pas à la polluer avec du code spécifique à la plate-forme ou à la bibliothèque. Tout cela peut être localisé dans le processeur d'image.

GrandmasterB
la source
Bon point. La plupart des gens ici ne comprenaient pas que je déléguais déjà la partie la plus compliquée à une bibliothèque tierce, peut-être que je n'étais pas assez clair.
ChocoDeveloper
2

Fondamentalement, comme Doc Brown l'a déjà dit:

Créez une getAsThumbnail()méthode pour la classe d'image, mais cette méthode devrait en fait simplement déléguer le travail à une ImageUtilsclasse. Donc, cela ressemblerait à quelque chose comme ceci:

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

Et

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

Cela permettra un code plus facile à consulter. Comparez les éléments suivants:

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

Ou

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

Si ce dernier vous semble bon, vous pouvez également vous limiter à créer cette méthode d'assistance quelque part.

Deiwin
la source
1

Je pense que dans "Image Domain", vous avez juste l'objet Image qui est immuable et monadique. Vous demandez à l'image une version redimensionnée et elle renvoie une version redimensionnée d'elle-même. Ensuite, vous pouvez décider si vous souhaitez vous débarrasser de l'original ou conserver les deux.

Maintenant, les versions miniature, avatar, etc. de l'image sont un autre domaine entièrement, qui peut demander au domaine image différentes versions d'une certaine image à donner à un utilisateur. Habituellement, ce domaine n'est pas aussi énorme ou générique, vous pouvez donc probablement le conserver dans la logique d'application.

Dans une application à petite échelle, je redimensionnais les images au moment de la lecture. Par exemple, je pourrais avoir une règle de réécriture apache qui délègue à php un script si l'image 'http://my.site.com/images/thumbnails/image1.png', où le fichier sera récupéré en utilisant le nom image1.png et redimensionné et stocké dans «vignettes / image1.png». Ensuite, à la prochaine demande de cette même image, apache servira l'image directement sans exécuter le script php. Apache répond automatiquement à votre question sur findAllImagesWithoutThumbnails, à moins que vous n'ayez besoin de faire des statistiques?

Dans une application à grande échelle, j'enverrais les toutes nouvelles images à un travail d'arrière-plan, qui prend soin de générer les différentes versions de l'image et l'enregistre aux endroits appropriés. Je ne prendrais pas la peine de créer un domaine ou une classe entière car ce domaine est très peu susceptible de se transformer en un terrible gâchis de spaghettis et de mauvaise sauce.

andho
la source
0

Réponse courte:

Ma recommandation ajoute cette méthode à la classe d'images:

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

L'objet Image est toujours immuable, ces méthodes renvoient une nouvelle image.

Tulains Córdova
la source
0

Il y a déjà pas mal de bonnes réponses, donc je vais développer un peu une heuristique derrière la façon d'identifier les objets et leurs responsabilités.

La POO diffère de la vie réelle en ce que les objets de la vie réelle sont souvent passifs et en POO, ils sont actifs. Et c'est un noyau de la pensée objet . Par exemple, qui redimensionnerait une image dans la vraie vie? Un humain, intelligent à cet égard. Mais en POO, il n'y a pas d'humains, donc les objets sont plutôt intelligents. Une façon de mettre en œuvre cette approche «centrée sur l'humain» dans la POO consiste à utiliser des classes de service, par exemple, des Managerclasses notoires . Ainsi, les objets sont traités comme des structures de données passives. Ce n'est pas une façon OOP.

Il y a donc deux options. La première, la création d'une méthode Image::createThumbnail(), est déjà envisagée. La seconde consiste à créer une ResizedImageclasse. Il peut être un décorateur d'un Image(cela dépend de votre domaine de conserver Imageou non une interface), bien que cela entraîne des problèmes d'encapsulation, car il ResizedImagedevrait avoir une Imagesource. Mais un Imagene serait pas submergé par le redimensionnement des détails, le laissant à un objet de domaine séparé, agissant conformément à un SRP.

Vadim Samokhin
la source