Un'immagine dovrebbe essere in grado di ridimensionarsi in OOP?


9

Sto scrivendo un'app che avrà Imageun'entità e ho già problemi a decidere di chi dovrebbe essere la responsabilità di ciascuna attività.

Prima ho la Imagelezione. Ha un percorso, larghezza e altri attributi.

Poi ho creato una ImageRepositoryclasse, per il recupero di immagini con un singolo e collaudato metodo, ad esempio: findAllImagesWithoutThumbnail().

Ma ora devo anche essere in grado di farlo createThumbnail(). Chi dovrebbe occuparsene? Stavo pensando di avere una ImageManagerclasse, che sarebbe una classe specifica per l'app (ci sarebbe anche un componente riutilizzabile di manipolazione dell'immagine di terze parti, non sto reinventando la ruota).

O forse sarebbe 0K per lasciare il Imageridimensionamento stesso? O lascia ImageRepositoryche ImageManagersia la stessa classe?

Cosa ne pensi?


Cosa fa Image finora?
Winston Ewert,

Come prevedete di ridimensionare le immagini? Riduci sempre le immagini o immagini di voler tornare alle dimensioni originali?
Gort il robot il

@WinstonEwert Genera dati come la risoluzione formattata (ad esempio: la stringa "1440x900"), URL diversi e consente di modificare alcune statistiche come "viste" o "voti".
ChocoDeveloper,

@StevenBurnap Considero l'immagine originale la cosa più importante, uso solo miniature per una navigazione più veloce o se l'utente vuole ridimensionare per adattarsi al suo desktop o qualcosa del genere, quindi sono una cosa separata.
ChocoDeveloper

Renderei immutabile la classe Image in modo che tu non debba copiarla in modo difensivo quando la passi in giro.
dan_waterworth,

Risposte:


8

La domanda posta è troppo vaga per avere una risposta reale in quanto dipende davvero da come Imageverranno utilizzati gli oggetti.

Se stai usando l'immagine a una sola dimensione e stai ridimensionando perché l'immagine di origine ha le dimensioni sbagliate, potrebbe essere meglio che il codice letto esegua il ridimensionamento. Chiedi al tuo createImagemetodo di prendere una larghezza / altezza e quindi restituire l'immagine dopo essere stata ridimensionata a quella larghezza / altezza.

Se hai bisogno di più dimensioni e se la memoria non è un problema, è meglio mantenere l'immagine in memoria come originariamente letta e fare qualsiasi ridimensionamento al momento della visualizzazione. In tal caso, è possibile utilizzare un paio di design diversi. L'immagine widthe heightsarebbe fissa, ma avresti un displaymetodo che ha preso una posizione e un'altezza / larghezza target o avresti un metodo che prende una larghezza / altezza che restituisce un oggetto che qualunque sistema di visualizzazione stai usando. La maggior parte delle API di disegno che ho usato ti consentono di specificare la dimensione target al momento del disegno.

Se i requisiti fanno sì che il disegno a dimensioni diverse sia abbastanza spesso perché le prestazioni rappresentino un problema, potresti avere un metodo che crea una nuova immagine di dimensioni diverse in base all'originale. Un'alternativa sarebbe quella di avere la tua Imageclasse nella cache diverse rappresentazioni internamente in modo che la prima volta che chiami displaycon le dimensioni della miniatura esegua il ridimensionamento mentre la seconda volta disegna solo la copia cache salvata dall'ultima volta. Questo utilizza più memoria, ma è raro che tu abbia più di alcuni ridimensionamenti comuni.

Un'altra alternativa è quella di avere una singola Imageclasse che conserva l'immagine di base e che quella classe contenga una o più rappresentazioni. Uno Imagestesso non avrebbe altezza / larghezza. Invece, inizierebbe con uno ImageRepresentationche aveva un'altezza e una larghezza. Disegneresti questa rappresentazione. Per "ridimensionare" un'immagine, dovresti chiedere Imageuna rappresentazione con determinate metriche di altezza / larghezza. Ciò indurrebbe quindi a contenere questa nuova rappresentazione e l'originale. Questo ti dà un sacco di controllo su ciò che è appeso in memoria a scapito di una maggiore complessità.

Personalmente non mi piacciono le classi che contengono la parola Managerperché "manager" è una parola molto vaga che non ti dice molto su cosa fa esattamente la classe. Gestisce la durata degli oggetti? Si trova tra il resto dell'applicazione e la cosa che gestisce?


"dipende davvero da come verranno utilizzati gli oggetti Image." Questa affermazione non è in linea con la nozione di incapsulamento e accoppiamento libero (anche se in questo caso potrebbe portare il principio un po 'troppo lontano). Un migliore punto di differenziazione sarebbe se e lo stato di Image includesse significativamente le dimensioni o meno.
Chris Bye,

Sembra che tu stia parlando di un punto di vista dell'applicazione di modifica delle immagini. Non è del tutto chiaro se OP lo voleva o no eh?
andho,

6

Mantieni le cose semplici fintanto che ci sono solo alcuni requisiti e migliora il tuo design quando devi. Immagino che nella maggior parte dei casi del mondo reale non ci sia nulla di sbagliato ad iniziare con un design del genere:

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

Le cose possono diventare diverse quando è necessario passare più parametri createThumbnail()e tali parametri richiedono una vita propria. Ad esempio, supponiamo che stai per creare miniature per diverse centinaia di immagini, tutte con una determinata dimensione di destinazione, algoritmo di ridimensionamento o qualità. Ciò significa che è possibile spostare l'oggetto createThumbnailin un'altra classe, ad esempio una classe manager o una ImageResizerclasse, in cui tali parametri vengono passati dal costruttore e quindi legati alla durata ImageResizerdell'oggetto.

In realtà, inizierei con il primo approccio e il refactor più tardi quando ne ho davvero bisogno.


bene, se sto già delegando la chiamata a un componente separato perché non renderlo una responsabilità di una ImageResizerclasse in primo luogo? Quando delegare una chiamata invece di spostare la responsabilità in una nuova classe?
Songo,

2
@Songo: 2 possibili motivi: (1) il codice per delegare al componente separato - forse già esistente - non è solo un semplice one-liner, forse solo una sequenza da 4 a 6 comandi, quindi hai bisogno di un posto dove memorizzarlo . (2) Zucchero sintattico / facilità d'uso: sarà molto bello scrivere Image thumbnail = img.createThumbnail(x,y).
Doc Brown,

+1 aah vedo. Grazie per la spiegazione :)
Songo,

4

Penso che dovrebbe far parte della Imageclasse, poiché il ridimensionamento in una classe esterna richiederebbe al resizer di conoscere l'implementazione della Image, violando così l'incapsulamento. Suppongo Imagesia una classe base e finirai con sottoclassi separate per tipi di immagini concrete (PNG, JPEG, SVG, ecc.). Pertanto, dovrai avere classi di ridimensionamento corrispondenti o un ridimensionatore generico con switchun'istruzione che si ridimensiona in base alla classe di implementazione - un odore di design classico.

Un approccio potrebbe essere quello di far Imageprendere al costruttore una risorsa contenente l'immagine, i parametri di altezza e larghezza e crearla in modo appropriato. Quindi ridimensionare potrebbe essere semplice come creare un nuovo oggetto usando la risorsa originale (memorizzata nella cache all'interno dell'immagine) e i nuovi parametri di dimensione. Ad esempio foo.createThumbnail(), semplicemente return new Image(this.source, 250, 250). ( ovviamente con Imageil tipo concreto foodi). Ciò mantiene le tue immagini immutabili e le loro implementazioni private.


Mi piace questa soluzione, ma non capisco perché dici che il resizer deve conoscere l'implementazione interna di Image. Tutto ciò di cui ha bisogno è l'origine e le dimensioni target.
ChocoDeveloper,

Anche se penso che avere una classe esterna non abbia senso perché sarebbe inaspettato, né dovrebbe violare l'incapsulamento né richiedere un'istruzione switch () quando si esegue un ridimensionamento. In definitiva un'immagine è una matrice 2d di qualcosa e puoi sicuramente ridimensionare le immagini purché l'interfaccia consenta di ottenere e impostare singoli pixel.
whatsisname

4

So che OOP riguarda l'incapsulamento di dati e comportamenti insieme, ma non penso che sia una buona idea per un'immagine avere la logica di ridimensionamento incorporata in questo caso, perché un'immagine non ha bisogno di sapere come ridimensionare se stessa per essere un'immagine.

Una miniatura è in realtà un'immagine diversa. Forse potresti avere una struttura di dati che contiene la relazione tra una fotografia e la sua miniatura (entrambe sono immagini).

Cerco di dividere i miei programmi in cose (come immagini, fotografie, miniature, ecc.) E servizi (come PhotographRepository, ThumbnailGenerator, ecc.). Ottimizza le strutture dei dati, quindi definisci i servizi che ti consentono di creare, manipolare, trasformare, persistere e ripristinare tali strutture di dati. Non ho alcun comportamento in più nelle mie strutture di dati che assicurarmi che siano create correttamente e utilizzate in modo appropriato.

Pertanto, no, un'immagine non dovrebbe contenere la logica su come realizzare un'anteprima. Dovrebbe esserci un servizio ThumbnailGenerator che ha un metodo come:

Image GenerateThumbnailFrom(Image someImage);

La mia struttura di dati più grande potrebbe apparire così:

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

    public Image Thumbnail { get; private set; }
}

Naturalmente ciò potrebbe significare che stai facendo uno sforzo che non vuoi fare mentre costruisci l'oggetto, quindi prenderei in considerazione anche qualcosa del genere OK:

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

... nel caso in cui si desideri una struttura di dati con valutazione pigra. (Mi dispiace non aver incluso i miei controlli nulli e non l'ho reso sicuro per i thread, il che è qualcosa che vorresti se stessi cercando di imitare una struttura di dati immutabile).

Come puoi vedere, una di queste classi è stata creata da un qualche tipo di PhotographRepository, che probabilmente ha un riferimento a un ThumbnailGenerator ottenuto attraverso l'iniezione di dipendenza.


Mi è stato detto che non avrei dovuto creare lezioni senza comportamento. Non sono sicuro se quando dici "strutture di dati" ti riferisci alle classi o qualcosa del C ++ (è questo il linguaggio?). Le uniche strutture di dati che conosco e utilizzo sono le primitive. I servizi e la parte DI sono esatti, potrei finire per farlo.
ChocoDeveloper

@ChocoDeveloper: a volte le classi senza comportamento sono utili o necessarie, a seconda della situazione. Quelle sono chiamate classi di valore . Le classi OOP normali sono classi con comportamento codificato. Anche le classi OOP compostabili hanno un comportamento codificato, ma la loro struttura della loro composizione può dare origine a molti comportamenti necessari a un'applicazione software.
rwong

3

Hai identificato una singola funzionalità che desideri implementare, quindi perché non dovrebbe essere separata da tutto ciò che hai identificato finora? Questo è ciò che il principio di responsabilità singola suggerirebbe sia la soluzione.

Crea IImageResizerun'interfaccia che ti consenta di passare un'immagine e una dimensione target e che restituisca una nuova immagine. Quindi creare un'implementazione di tale interfaccia. Esistono in realtà molti modi per ridimensionare le immagini, quindi potresti persino trovarne più di una!


+1 per SRP rilevanti, ma non sto implementando il ridimensionamento effettivo, che è già delegato a una libreria di terze parti come indicato.
ChocoDeveloper,

3

Presumo quei fatti sul metodo, che ridimensiona le immagini:

  • Deve restituire una nuova copia dell'immagine. Non è possibile modificare l'immagine stessa, poiché si spezzerebbe altro codice che fa riferimento a questa immagine.
  • Non ha bisogno di accedere ai dati interni della classe Image. Le classi di immagini di solito devono offrire accesso pubblico a tali dati (o copia di).
  • Il ridimensionamento delle immagini è complesso e richiede molti parametri diversi. Forse anche punti di estensibilità per diversi algoritmi di ridimensionamento. Passare tutto comporterebbe la firma di un metodo grande.

Sulla base di questi fatti, direi che non vi è alcun motivo per cui il metodo di ridimensionamento delle immagini faccia parte della classe Image stessa. L'implementazione come metodo di classe dell'helper statico sarebbe la cosa migliore.


Buone ipotesi. Non sono sicuro del motivo per cui dovrebbe essere un metodo statico, provo ad evitarli per la testabilità.
ChocoDeveloper,

2

Una classe di elaborazione delle immagini potrebbe essere appropriata (o Image Manager, come l'hai chiamata). Passa la tua immagine a un metodo CreateThumbnail del processore di immagini, ad esempio, per recuperare un'immagine in miniatura.

Uno dei motivi per cui suggerirei questo percorso è che dici di utilizzare una libreria di elaborazione delle immagini di terze parti. Rimuovere la funzionalità di ridimensionamento dalla classe Image stessa potrebbe semplificare l'isolamento di qualsiasi codice specifico di piattaforma o di terze parti. Pertanto, se è possibile utilizzare la classe di immagini di base su tutte le piattaforme / app, non è necessario inquinarla con il codice specifico della piattaforma o della libreria. Tutto ciò può trovarsi nel processore di immagini.


Buon punto. La maggior parte delle persone qui non capiva che sto già delegando la parte più complicata a una biblioteca di terze parti, forse non ero abbastanza chiaro.
ChocoDeveloper

2

Fondamentalmente, come ha già detto Doc Brown:

Crea un getAsThumbnail()metodo per la classe immagine, ma questo metodo dovrebbe effettivamente delegare il lavoro ad una ImageUtilsclasse. Quindi sarebbe simile a questo:

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

E

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

Ciò consentirà un codice più facile da consultare. Confronta quanto segue:

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

O

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

Se quest'ultimo sembra buono per te, puoi anche continuare a creare questo metodo di supporto da qualche parte.


1

Penso che in "Image Domain", hai solo l'oggetto Image che è immutabile e monadico. Chiedi all'immagine una versione ridimensionata e restituisce una versione ridimensionata di se stessa. Quindi puoi decidere se vuoi liberarti dell'originale o conservare entrambi.

Ora la versione in miniatura, avatar, ecc. Dell'immagine è interamente un altro dominio, che può chiedere al dominio immagine le diverse versioni di una determinata immagine da offrire a un utente. Di solito questo dominio non è nemmeno così vasto o generico, quindi probabilmente puoi mantenerlo nella logica dell'applicazione.

In un'app su piccola scala, ridimensionerei le immagini in fase di lettura. Ad esempio potrei avere una regola di riscrittura di Apache che delega a php uno script se l'immagine 'http://my.site.com/images/thumbnails/image1.png', dove il file verrà recuperato usando il nome image1.png e ridimensionato e memorizzato in 'thumbnails / image1.png'. Quindi alla successiva richiesta a questa stessa immagine, apache servirà l'immagine direttamente senza eseguire lo script php. La tua domanda su findAllImagesWithoutThumbnails viene automaticamente risposta da apache, a meno che tu non debba fare statistiche?

In un'app su larga scala, invierei tutte le nuove immagini a un processo in background, che si occupa di generare le diverse versioni dell'immagine e di salvarle in luoghi appropriati. Non mi preoccuperei di creare un intero dominio o classe poiché è molto improbabile che questo dominio cresca in un terribile pasticcio di spaghetti e salsa brutta.


0

Risposta breve:

La mia raccomandazione è l'aggiunta di questi metodi alla classe immagine:

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

L'oggetto Image è ancora immutabile, questi metodi restituiscono una nuova immagine.


0

Ci sono già alcune ottime risposte, quindi tratterò un po 'di un'euristica dietro il modo di identificare gli oggetti e le loro responsabilità.

OOP differisce dalla vita reale in quanto gli oggetti nella vita reale sono spesso passivi e in OOP sono attivi. E questo è un nucleo del pensiero oggettivo . Ad esempio, chi ridimensionerebbe un'immagine nella vita reale? Un essere umano, che è intelligente in questo senso. Ma in OOP non ci sono umani, quindi gli oggetti sono intelligenti. Un modo per implementare questo approccio "incentrato sull'uomo" in OOP consiste nell'utilizzare classi di servizio, ad esempio Managerclassi note . Pertanto gli oggetti vengono trattati come trame di dati passivi. Non è un modo OOP.

Quindi ci sono due opzioni. Il primo, creando un metodo Image::createThumbnail(), è già considerato. Il secondo è creare una ResizedImageclasse. Può essere un decoratore di un Image(dipende dal tuo dominio se conservare Imageo meno un'interfaccia), anche se provoca alcuni problemi di incapsulamento, dal momento che ResizedImagedovrebbe avere una Imagefonte. Ma un Imagenon verrebbe sopraffatto dal ridimensionamento dei dettagli, lasciandolo a un oggetto di dominio separato, agendo in conformità con un SRP.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.