Sollte sich ein Bild in OOP in der Größe ändern können?


9

Ich schreibe eine App, die eine ImageEntität haben wird, und ich habe bereits Probleme zu entscheiden, wessen Verantwortung jede Aufgabe sein soll.

Zuerst habe ich die ImageKlasse. Es hat einen Pfad, eine Breite und andere Attribute.

Dann habe ich eine ImageRepositoryKlasse erstellt, um Bilder mit einer einzigen und getesteten Methode abzurufen, z findAllImagesWithoutThumbnail().

Aber jetzt muss ich auch dazu in der Lage sein createThumbnail(). Wer sollte sich darum kümmern? Ich habe darüber nachgedacht, eine ImageManagerKlasse zu haben, die eine App-spezifische Klasse sein würde (es würde auch eine wiederverwendbare Komponente zur Bildbearbeitung von Drittanbietern geben, ich erfinde das Rad nicht neu).

Oder wäre es vielleicht 0K, die ImageGröße selbst ändern zu lassen ? Oder lassen Sie die ImageRepositoryund ImageManagerdie gleiche Klasse sein?

Was denkst du?


Was macht Image bisher?
Winston Ewert

Wie erwarten Sie eine Größenänderung der Bilder? Schrumpfen Sie immer nur Bilder oder können Sie sich vorstellen, wieder in voller Größe zu arbeiten?
Gort the Robot

@WinstonEwert Er generiert Daten wie die formatierte Auflösung (z. B. die Zeichenfolge '1440x900') und verschiedene URLs und lässt Sie einige Statistiken wie 'Ansichten' oder 'Stimmen' ändern.
ChocoDeveloper

@StevenBurnap Ich halte das Originalbild für das Wichtigste. Ich verwende nur Miniaturansichten, um schneller zu surfen, oder wenn der Benutzer die Größe an seinen Desktop oder etwas anderes anpassen möchte, sind sie eine separate Sache.
ChocoDeveloper

Ich würde die Image-Klasse unveränderlich machen, damit Sie sie nicht defensiv kopieren müssen, wenn Sie sie weitergeben.
dan_waterworth

Antworten:


8

Die gestellte Frage ist zu vage, um eine echte Antwort zu erhalten, da sie wirklich davon abhängt, wie die ImageObjekte verwendet werden sollen.

Wenn Sie das Bild nur in einer Größe verwenden und die Größe ändern, weil das Quellbild die falsche Größe hat, ist es möglicherweise am besten, den Lesecode die Größe ändern zu lassen. Lassen Sie Ihre createImageMethode eine Breite / Höhe annehmen und dann das Bild zurückgeben, dessen Größe auf diese Breite / Höhe geändert wurde.

Wenn Sie mehrere Größen benötigen und der Speicher keine Rolle spielt, ist es am besten, das Bild wie ursprünglich gelesen im Speicher zu belassen und die Größe zur Anzeigezeit zu ändern. In einem solchen Fall können Sie verschiedene Designs verwenden. Das Bild widthund heightwäre fixiert, aber Sie hätten entweder eine displayMethode, die eine Position und eine Zielhöhe / -breite einnimmt, oder eine Methode, die eine Breite / Höhe verwendet, die ein Objekt zurückgibt, das von dem von Ihnen verwendeten Anzeigesystem verwendet wird. Mit den meisten Zeichnungs-APIs, die ich verwendet habe, können Sie die Zielgröße zum Zeitpunkt des Zeichnens angeben.

Wenn die Anforderungen dazu führen, dass häufig mit unterschiedlichen Größen gezeichnet wird, damit die Leistung ein Problem darstellt, können Sie eine Methode verwenden, mit der ein neues Bild mit einer anderen Größe basierend auf dem Original erstellt wird. Eine Alternative wäre, Ihre ImageKlasse intern verschiedene Darstellungen zwischenspeichern zu lassen, damit beim ersten Aufruf displaymit der Miniaturbildgröße die Größenänderung vorgenommen wird, während beim zweiten Mal nur die zwischengespeicherte Kopie gezeichnet wird, die beim letzten Mal gespeichert wurde. Dies verbraucht mehr Speicher, aber es kommt selten vor, dass Sie mehr als ein paar häufige Größenänderungen haben.

Eine andere Alternative besteht darin, eine einzelne ImageKlasse zu haben , die das Basisbild beibehält und diese Klasse eine oder mehrere Darstellungen enthält. Ein Imageselbst hätte keine Höhe / Breite. Stattdessen würde es mit einem beginnen ImageRepresentation, der eine Höhe und Breite hatte. Sie würden diese Darstellung zeichnen. Um die Größe eines Bildes zu ändern, müssen Sie Imageeine Darstellung mit bestimmten Höhen- / Breitenmetriken anfordern. Dies würde dazu führen, dass es dann diese neue Darstellung sowie das Original enthält. Dies gibt Ihnen viel Kontrolle darüber, was genau im Speicher herumhängt, auf Kosten der zusätzlichen Komplexität.

Ich persönlich mag keine Klassen, die das Wort enthalten, Managerweil "Manager" ein sehr vages Wort ist, das Ihnen nicht wirklich viel darüber sagt, was die Klasse genau tut. Verwaltet es die Objektlebensdauer? Steht es zwischen dem Rest der Anwendung und dem, was es verwaltet?


"Es hängt wirklich davon ab, wie die Bildobjekte verwendet werden." Diese Aussage entspricht nicht wirklich dem Begriff der Kapselung und losen Kopplung (obwohl das Prinzip in diesem Fall möglicherweise etwas zu weit geht). Ein besserer Unterscheidungspunkt wäre, ob und der Status des Bildes die Größe sinnvoll einschließt oder nicht.
Chris Bye

Anscheinend handelt es sich um eine Anwendungsansicht für die Bildbearbeitung. Nicht ganz klar, ob OP das wollte oder nicht?
Andho

6

Halten Sie die Dinge einfach, solange es nur wenige Anforderungen gibt, und verbessern Sie Ihr Design, wenn Sie müssen. Ich denke, in den meisten Fällen der realen Welt ist nichts falsch daran, mit einem solchen Design zu beginnen:

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

Dinge können anders werden, wenn Sie mehr Parameter übergeben müssen createThumbnail(), und diese Parameter benötigen eine eigene Lebensdauer. Nehmen wir beispielsweise an, Sie erstellen Miniaturansichten für mehrere hundert Bilder mit einer bestimmten Zielgröße, einem bestimmten Größenänderungsalgorithmus oder einer bestimmten Qualität. Dies bedeutet, dass Sie die createThumbnailin eine andere Klasse verschieben können, z. B. eine Manager-Klasse oder eine ImageResizerKlasse, in der diese Parameter vom Konstruktor übergeben werden und somit an die Lebensdauer des ImageResizerObjekts gebunden sind .

Eigentlich würde ich später mit dem ersten Ansatz und Refactor beginnen, wenn ich ihn wirklich brauche.


Nun, wenn ich den Anruf bereits an eine separate Komponente delegiere, warum sollte ich ihn dann nicht ImageResizerin erster Linie in die Verantwortung einer Klasse stellen? Wann delegieren Sie einen Anruf, anstatt die Verantwortung auf eine neue Klasse zu verlagern?
Songo

2
@Songo: 2 mögliche Gründe: (1) Der Code für die Delegierung an die - möglicherweise bereits vorhandene - separate Komponente ist nicht nur ein einfacher Einzeiler, möglicherweise nur eine Folge von 4 bis 6 Befehlen. Sie benötigen daher einen Speicherort . (2) Syntaktischer Zucker / Benutzerfreundlichkeit: Das Schreiben ist einfach sehr schön Image thumbnail = img.createThumbnail(x,y).
Doc Brown

+1 aah ich verstehe. Danke für die Erklärung :)
Songo

4

Ich denke, es müsste Teil der ImageKlasse sein, da die Größenänderung in einer externen Klasse erfordern würde, dass der Resizer die Implementierung von kennt Image, wodurch die Kapselung verletzt wird. Ich gehe davon aus, dass Imagees sich um eine Basisklasse handelt, und Sie erhalten separate Unterklassen für konkrete Bildtypen (PNG, JPEG, SVG usw.). Daher benötigen Sie entweder entsprechende Größenänderungsklassen oder einen generischen Größenänderer mit einer switchAnweisung, deren Größe basierend auf der Implementierungsklasse geändert wird - ein klassischer Designgeruch.

Ein Ansatz könnte darin bestehen, dass Ihr ImageKonstruktor eine Ressource mit dem Bild sowie den Parametern für Höhe und Breite verwendet und sich entsprechend erstellt. Die Größenänderung kann dann so einfach sein wie das Erstellen eines neuen Objekts unter Verwendung der ursprünglichen Ressource (im Bild zwischengespeichert) und der neuen Größenparameter. ZB foo.createThumbnail()würde einfach return new Image(this.source, 250, 250). (mit Imageder konkreten Art foovon natürlich). Dies hält Ihre Bilder unveränderlich und ihre Implementierungen privat.


Ich mag diese Lösung, aber ich verstehe nicht, warum Sie sagen, dass der Resizer die interne Implementierung von kennen muss Image. Alles, was es braucht, ist die Quell- und die Zieldimension.
ChocoDeveloper

Obwohl ich denke, dass eine externe Klasse keinen Sinn ergibt, weil sie unerwartet wäre, müsste sie weder die Kapselung verletzen noch eine switch () -Anweisung benötigen, wenn eine Größenänderung durchgeführt wird. Letztendlich ist ein Bild ein 2D-Array von etwas, und Sie können die Größe von Bildern definitiv ändern, solange die Benutzeroberfläche das Abrufen und Einstellen einzelner Pixel ermöglicht.
Whatsisname

4

Ich weiß , dass OOP ist etwa gemeinsam Daten und Verhalten einkapseln, aber ich glaube nicht , es ist eine gute Idee für ein Bild ist die Resize - Logik in diesem Fall eingebettet zu haben, weil ein Bild nicht müssen wissen , wie sich die Größe zu sein ein Bild.

Ein Miniaturbild ist eigentlich ein anderes Bild. Vielleicht haben Sie eine Datenstruktur, die die Beziehung zwischen einem Foto und seinem Miniaturbild enthält (beide sind Bilder).

Ich versuche, meine Programme in Dinge (wie Bilder, Fotos, Miniaturansichten usw.) und Dienste (wie PhotographRepository, ThumbnailGenerator usw.) zu unterteilen. Machen Sie Ihre Datenstrukturen richtig und definieren Sie dann die Services, mit denen Sie diese Datenstrukturen erstellen, bearbeiten, transformieren, beibehalten und wiederherstellen können. Ich füge meinen Datenstrukturen nicht mehr Verhalten hinzu, als sicherzustellen, dass sie ordnungsgemäß erstellt und ordnungsgemäß verwendet werden.

Daher sollte ein Bild nicht die Logik zum Erstellen eines Miniaturbilds enthalten. Es sollte einen ThumbnailGenerator-Dienst geben, der eine Methode wie die folgende hat:

Image GenerateThumbnailFrom(Image someImage);

Meine größere Datenstruktur könnte folgendermaßen aussehen:

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

    public Image Thumbnail { get; private set; }
}

Das könnte natürlich bedeuten, dass Sie sich beim Erstellen des Objekts keine Mühe machen, also würde ich so etwas auch als OK betrachten:

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

... für den Fall, dass Sie eine Datenstruktur mit verzögerter Auswertung wünschen. (Entschuldigung, ich habe meine Nullprüfungen nicht eingeschlossen und sie nicht threadsicher gemacht. Dies ist etwas, das Sie möchten, wenn Sie versuchen, eine unveränderliche Datenstruktur nachzuahmen.)

Wie Sie sehen können, wird eine dieser Klassen von einer Art PhotographRepository erstellt, das wahrscheinlich einen Verweis auf einen ThumbnailGenerator enthält, den es durch Abhängigkeitsinjektion erhalten hat.


Mir wurde gesagt, ich sollte keine Klassen ohne Verhalten erstellen. Ich bin mir nicht sicher, ob Sie, wenn Sie "Datenstrukturen" sagen, auf die Klassen oder etwas aus C ++ verweisen (ist das diese Sprache?). Die einzigen Datenstrukturen, die ich kenne und verwende, sind die Grundelemente. Der Service- und DI-Teil ist genau richtig, ich könnte dies am Ende tun.
ChocoDeveloper

@ChocoDeveloper: Gelegentlich sind Klassen ohne Verhalten je nach Situation nützlich oder notwendig. Diese werden als Wertklassen bezeichnet . Normale OOP-Klassen sind Klassen mit fest codiertem Verhalten. Zusammensetzbare OOP-Klassen weisen ebenfalls ein fest codiertes Verhalten auf, aber ihre Struktur kann zu vielen Verhaltensweisen führen, die von einer Softwareanwendung benötigt werden.
Rwong

3

Sie haben eine einzelne Funktionalität identifiziert, die Sie implementieren möchten. Warum sollte sie nicht von allem getrennt sein, was Sie bisher identifiziert haben? Das ist es, was das Prinzip der Einzelverantwortung als Lösung vorschlagen würde.

Erstellen Sie eine IImageResizerSchnittstelle, über die Sie ein Bild und eine Zielgröße übergeben können und die ein neues Bild zurückgibt. Erstellen Sie dann eine Implementierung dieser Schnittstelle. Es gibt tatsächlich unzählige Möglichkeiten, die Größe von Bildern zu ändern, sodass Sie sogar mehr als eine haben können!


+1 für relevantes SRP, aber ich implementiere nicht die eigentliche Größenänderung, die wie angegeben bereits an eine Bibliothek eines Drittanbieters delegiert wurde.
ChocoDeveloper

3

Ich nehme diese Fakten über die Methode an, die die Größe von Bildern ändert:

  • Es muss eine neue Kopie des Bildes zurückgegeben werden. Sie können das Bild selbst nicht ändern, da dadurch anderer Code beschädigt wird, der auf dieses Bild verweist.
  • Es ist kein Zugriff auf die internen Daten der Image-Klasse erforderlich. Bildklassen müssen normalerweise öffentlichen Zugriff auf diese Daten (oder Kopien davon) bieten.
  • Die Größenänderung von Bildern ist komplex und erfordert viele verschiedene Parameter. Möglicherweise sogar Erweiterungspunkte für verschiedene Größenänderungsalgorithmen. Alles zu übergeben würde zu einer großen Methodensignatur führen.

Aufgrund dieser Tatsachen würde ich sagen, dass es keinen Grund gibt, dass die Methode zur Größenänderung von Bildern Teil der Bildklasse selbst ist. Die Implementierung als statische Hilfsmethode der Klasse wäre am besten.


Gute Annahmen. Ich bin mir nicht sicher, warum es eine statische Methode sein sollte, aber ich versuche sie aus Gründen der Testbarkeit zu vermeiden.
ChocoDeveloper

2

Möglicherweise ist eine Bildverarbeitungsklasse geeignet (oder ein Bildmanager, wie Sie ihn genannt haben). Übergeben Sie Ihr Bild an eine CreateThumbnail-Methode des Bildprozessors, um beispielsweise ein Miniaturbild abzurufen.

Einer der Gründe, warum ich diese Route vorschlagen würde, ist, dass Sie sagen, dass Sie eine Bildverarbeitungsbibliothek eines Drittanbieters verwenden. Wenn Sie die Größenänderungsfunktion aus der Image-Klasse selbst entfernen, können Sie möglicherweise plattformspezifischen Code oder Code von Drittanbietern leichter isolieren. Wenn Sie Ihre Basis-Image-Klasse also für alle Plattformen / Apps verwenden können, müssen Sie sie nicht mit plattform- oder bibliotheksspezifischem Code verschmutzen. Das kann alles im Bildprozessor sein.


Guter Punkt. Die meisten Leute hier haben nicht verstanden, dass ich den kompliziertesten Teil bereits an eine Bibliothek eines Drittanbieters delegiere. Vielleicht war ich nicht klar genug.
ChocoDeveloper

2

Grundsätzlich wie Doc Brown bereits sagte:

Erstellen Sie eine getAsThumbnail()Methode für die Bildklasse, aber diese Methode sollte die Arbeit eigentlich nur an eine ImageUtilsKlasse delegieren . Es würde also ungefähr so ​​aussehen:

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

Und

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

Dies ermöglicht einen übersichtlicheren Code. Vergleichen Sie Folgendes:

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

Oder

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

Wenn letzteres für Sie gut erscheint, können Sie sich auch daran halten, diese Hilfsmethode irgendwo zu erstellen.


1

Ich denke, in der "Bilddomäne" haben Sie nur das Bildobjekt, das unveränderlich und monadisch ist. Sie fragen das Bild nach einer verkleinerten Version und es gibt eine verkleinerte Version von sich zurück. Dann können Sie entscheiden, ob Sie das Original entfernen oder beide behalten möchten.

Jetzt ist die Miniaturansicht, der Avatar usw. des Bildes eine andere Domäne, die die Bilddomäne nach verschiedenen Versionen eines bestimmten Bildes fragen kann, die sie einem Benutzer geben soll. Normalerweise ist diese Domain auch nicht so groß oder generisch, sodass Sie dies wahrscheinlich in der Anwendungslogik beibehalten können.

In einer kleinen App würde ich die Größe der Bilder zum Zeitpunkt des Lesens ändern. Zum Beispiel könnte ich eine Apache-Umschreiberegel haben, die delegiert, um ein Skript zu phpieren, wenn das Bild 'http://my.site.com/images/thumbnails/image1.png', wobei die Datei unter dem Namen image1.png abgerufen wird und in der Größe geändert und in 'thumbnails / image1.png' gespeichert. Bei der nächsten Anforderung an dasselbe Image wird Apache das Image dann direkt bereitstellen, ohne das PHP-Skript auszuführen. Ihre Frage zu findAllImagesWithoutThumbnails wird automatisch von Apache beantwortet, es sei denn, Sie müssen Statistiken erstellen?

In einer groß angelegten App würde ich die neuen Bilder an einen Hintergrundjob senden, der sich darum kümmert, die verschiedenen Versionen des Bildes zu generieren und an geeigneten Stellen zu speichern. Ich würde mir nicht die Mühe machen, eine ganze Domain oder Klasse zu erstellen, da es sehr unwahrscheinlich ist, dass diese Domain zu einem schrecklichen Durcheinander von Spaghetti und schlechter Sauce wird.


0

Kurze Antwort:

Meine Empfehlung ist, diese Methoden der Bildklasse hinzuzufügen:

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

Das Image-Objekt ist immer noch unveränderlich. Diese Methoden geben ein neues Image zurück.


0

Es gibt bereits einige gute Antworten, daher werde ich ein wenig auf eine Heuristik eingehen, die hinter der Identifizierung von Objekten und ihrer Verantwortung steckt.

OOP unterscheidet sich vom realen Leben darin, dass Objekte im realen Leben oft passiv sind und in OOP aktiv sind. Und das ist ein Kern des Objektdenkens . Wer würde beispielsweise die Größe eines Bildes im wirklichen Leben ändern? Ein Mensch, der in dieser Hinsicht klug ist. Aber in OOP gibt es keine Menschen, also sind Objekte stattdessen intelligent. Eine Möglichkeit, diesen "menschenzentrierten" Ansatz in OOP zu implementieren, besteht darin, Serviceklassen zu verwenden, beispielsweise berüchtigte ManagerKlassen. Somit werden Objekte als passive Datenstrukturen behandelt. Es ist kein OOP-Weg.

Es gibt also zwei Möglichkeiten. Die erste Methode zum Erstellen einer Methode Image::createThumbnail()wird bereits berücksichtigt. Die zweite besteht darin, eine ResizedImageKlasse zu erstellen . Es kann ein Dekorateur von sein Image(es hängt von Ihrer Domain ab, ob eine ImageSchnittstelle beibehalten werden soll oder nicht), obwohl dies zu einigen Kapselungsproblemen führt, da ResizedImageeine ImageQuelle erforderlich wäre . Es Imagewürde jedoch nicht überfordert sein, die Größe von Details zu ändern und sie einem separaten Domänenobjekt zu überlassen, das gemäß einer SRP handelt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.