Czy obraz powinien mieć możliwość zmiany rozmiaru w OOP?

9

Piszę aplikację, która będzie miała Imageencję, i już mam problemy z ustaleniem, za kogo powinno być każde zadanie.

Najpierw mam Imageklasę. Ma ścieżkę, szerokość i inne atrybuty.

Następnie stworzyłem ImageRepositoryklasę do wyszukiwania obrazów za pomocą jednej i przetestowanej metody, np findAllImagesWithoutThumbnail(). :

Ale teraz muszę też móc createThumbnail(). Kto powinien sobie z tym poradzić? Myślałem o posiadaniu ImageManagerklasy, która byłaby klasą specyficzną dla aplikacji (byłby też wybrany element do manipulacji obrazem innej firmy, nie zmieniam koła).

A może byłoby 0K, aby pozwolić na Imagezmianę rozmiaru? Albo niech ImageRepositoryi ImageManagerbyć tej samej klasy?

Co myślisz?

ChocoDeveloper
źródło
Co robi Image do tej pory?
Winston Ewert,
Jak spodziewasz się zmienić rozmiar zdjęć? Czy kiedykolwiek zmniejszasz tylko obrazy, czy możesz sobie wyobrazić chęć powrotu do pełnego rozmiaru?
Gort the Robot
@WinstonEwert Generuje dane takie jak sformatowana rozdzielczość (np .: ciąg „1440x900”), różne adresy URL i pozwala modyfikować niektóre statystyki, takie jak „widoki” lub „głosy”.
ChocoDeveloper
@StevenBurnap Uważam, że oryginalny obraz jest najważniejszy, używam miniatur tylko w celu szybszego przeglądania lub jeśli użytkownik chce zmienić rozmiar, aby dopasować go do pulpitu lub czegoś, więc są one osobną rzeczą.
ChocoDeveloper
Sprawiłbym, że klasa Image jest niezmienna, abyś nie musiał jej defensywnie kopiować podczas przekazywania.
dan_waterworth

Odpowiedzi:

8

Zadane pytanie jest zbyt niejasne, aby dać prawdziwą odpowiedź, ponieważ tak naprawdę zależy od tego, w jaki sposób Imagebędą używane obiekty.

Jeśli używasz obrazu tylko w jednym rozmiarze i zmieniasz rozmiar, ponieważ obraz źródłowy ma niewłaściwy rozmiar, najlepiej może być zmiana rozmiaru przez odczytany kod. Niech twoja createImagemetoda przyjmie szerokość / wysokość, a następnie zwróci obraz, którego rozmiar został zmieniony na tę szerokość / wysokość.

Jeśli potrzebujesz wielu rozmiarów i jeśli pamięć nie stanowi problemu, najlepiej jest zachować obraz w pamięci jako pierwotnie odczytany i wykonać dowolną zmianę rozmiaru w czasie wyświetlania. W takim przypadku możesz użyć kilku różnych wzorów. Obraz widthi heightbędzie ustalona, ale trzeba mieć albo displaysposobu, który zajął stanowisko i docelową wysokość / szerokość lub chcesz mieć jakąś metodę robienia szerokość / wysokość, że zwracany obiekt, który bez względu na system wyświetlania używasz. Większość używanych interfejsów API do rysowania pozwala określić docelowy rozmiar w czasie rysowania.

Jeśli wymagania powodują rysowanie w różnych rozmiarach wystarczająco często, aby wydajność stanowiła problem, możesz mieć metodę, która tworzy nowy obraz o innym rozmiarze na podstawie oryginału. Alternatywą byłoby Imagewewnętrzne przechowywanie w pamięci podręcznej różnych reprezentacji, aby po pierwszym wywołaniu displayz rozmiarem miniatury zmienił rozmiar, a za drugim razem po prostu narysował buforowaną kopię, którą zapisał z ostatniego razu. To zużywa więcej pamięci, ale rzadko zdarza się, że masz więcej niż kilka typowych zmian rozmiaru.

Inną alternatywą jest posiadanie jednej Imageklasy, która zachowuje obraz podstawowy i aby klasa zawierała jedną lub więcej reprezentacji. ImageSama nie miałaby wysokość / szerokość. Zamiast tego zaczynałby się od takiego, ImageRepresentationktóry miał wysokość i szerokość. Narysowałbyś tę reprezentację. Aby „zmienić rozmiar” obrazu, należy poprosić Imageo reprezentację z pewnymi wskaźnikami wysokości / szerokości. Spowodowałoby to, że zawierałoby to nową reprezentację, a także oryginał. Daje to dużą kontrolę nad tym, co dokładnie kręci się w pamięci, kosztem dodatkowej złożoności.

Osobiście nie lubię klas zawierających to słowo, Managerponieważ „manager” to bardzo niejasne słowo, które tak naprawdę niewiele mówi o tym, co robi klasa. Czy zarządza czasem życia obiektu? Czy stoi między resztą aplikacji a tym, czym zarządza?

Gort the Robot
źródło
„to naprawdę zależy od tego, w jaki sposób będą używane obiekty Image”. To stwierdzenie nie jest tak naprawdę zgodne z pojęciem enkapsulacji i luźnego sprzęgania (choć w tym przypadku zasada może być nieco za daleko). Lepszym punktem różnicowania byłoby to, czy i stan obrazu znacząco obejmuje rozmiar, czy nie.
Chris Bye
Wygląda na to, że mówisz o punkcie widzenia aplikacji do edycji obrazu. Nie do końca jasne, czy OP tego chciał, czy nie?
andho
6

Uprość wszystko, o ile jest tylko kilka wymagań, i popraw swój projekt, kiedy będziesz musiał. Myślę, że w większości rzeczywistych przypadków nie ma nic złego w rozpoczęciu od takiego projektu:

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

Rzeczy mogą się różnić, gdy musisz przekazać więcej parametrów createThumbnail(), a parametry te wymagają własnego życia. Załóżmy na przykład, że masz zamiar utworzyć miniatury dla kilkuset zdjęć, wszystkie o określonym rozmiarze docelowym, algorytmie zmiany rozmiaru lub jakości. Oznacza to, że możesz przenieść createThumbnailinną klasę, na przykład klasę menedżera lub ImageResizerklasę, w której te parametry są przekazywane przez konstruktor, a zatem są związane z czasem życia ImageResizerobiektu.

Właściwie zacznę od pierwszego podejścia i refaktoryzuję później, kiedy naprawdę będę tego potrzebować.

Doktor Brown
źródło
cóż, jeśli już deleguję połączenie do osobnego komponentu, dlaczego nie uczynić tego obowiązkiem ImageResizerklasy? Kiedy delegujesz rozmowę zamiast przenosić odpowiedzialność na nową klasę?
Songo
2
@Songo: 2 możliwe powody: (1) kod do delegowania do - być może już istniejącego - osobnego komponentu nie jest zwykłym pojedynczym liniałem, być może tylko sekwencją 4 do 6 poleceń, dlatego potrzebujesz miejsca do przechowywania . (2) Cukier syntaktyczny / łatwość użycia: pisanie będzie bardzo przystojne Image thumbnail = img.createThumbnail(x,y).
Doc Brown
+1 aah widzę. Dzięki za wyjaśnienie :)
Songo
4

Myślę, że musiałby to być element Imageklasy, ponieważ zmiana rozmiaru w klasie zewnętrznej wymagałaby od resizera znajomości implementacji Image, co naruszyłoby enkapsulację. Zakładam, że Imagejest to klasa podstawowa, a ty skończysz z osobnymi podklasami dla konkretnych typów obrazów (PNG, JPEG, SVG itp.). Dlatego musisz albo mieć odpowiednie klasy zmiany rozmiaru, albo ogólny resizer z switchinstrukcją, która zmienia rozmiar w oparciu o klasę implementacji - klasyczny zapach projektowy.

Jednym z podejść może być Imagepobranie konstruktora od zasobu zawierającego obraz oraz parametry wysokości i szerokości i utworzenie go odpowiednio. Następnie zmiana rozmiaru może być tak prosta, jak utworzenie nowego obiektu przy użyciu oryginalnego zasobu (buforowanego wewnątrz obrazu) i nowych parametrów rozmiaru. Np . Po foo.createThumbnail()prostu return new Image(this.source, 250, 250). ( oczywiście Imagez konkretnym typem foo). Dzięki temu twoje obrazy pozostają niezmienne, a ich implementacje prywatne.

TMN
źródło
Podoba mi się to rozwiązanie, ale nie rozumiem, dlaczego mówisz, że resizer musi znać wewnętrzną implementację Image. Wszystko czego potrzebuje to wymiary źródłowe i docelowe.
ChocoDeveloper
Chociaż myślę, że posiadanie klasy zewnętrznej nie ma sensu, ponieważ byłoby to nieoczekiwane, ani nie musiałoby to naruszać enkapsulacji, ani nie wymagałoby posiadania instrukcji switch () podczas przeprowadzania zmiany rozmiaru. Ostatecznie obraz jest dwuwymiarową tablicą czegoś i można zdecydowanie zmienić rozmiar obrazów, o ile interfejs pozwala na pobieranie i ustawianie poszczególnych pikseli.
whatsisname
4

Wiem, że OOP polega na enkapsulacji danych i zachowaniu razem, ale nie sądzę, że dobrym pomysłem jest, aby Image miał w tym przypadku logikę zmiany rozmiaru, ponieważ Obraz nie musi wiedzieć, jak się zmienić obraz.

Miniatura jest w rzeczywistości innym obrazem. Być może masz strukturę danych, która utrzymuje relację między zdjęciem a jego miniaturą (oba są obrazami).

Próbuję podzielić moje programy na rzeczy (takie jak obrazy, fotografie, miniatury itp.) I usługi (takie jak PhotographRepository, ThumbnailGenerator itp.). Popraw swoje struktury danych, a następnie zdefiniuj usługi, które pozwalają tworzyć, manipulować, przekształcać, utrwalać i odzyskiwać te struktury danych. Nie stawiam więcej zachowań w moich strukturach danych niż upewnianie się, że są one prawidłowo tworzone i odpowiednio wykorzystywane.

Dlatego nie, obraz nie powinien zawierać logiki dotyczącej sposobu tworzenia miniatury. Powinna istnieć usługa ThumbnailGenerator, która ma metodę taką jak:

Image GenerateThumbnailFrom(Image someImage);

Moja większa struktura danych może wyglądać następująco:

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

    public Image Thumbnail { get; private set; }
}

Oczywiście może to oznaczać, że robisz wysiłek, którego nie chcesz robić podczas konstruowania obiektu, więc rozważę coś takiego:

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

... w przypadku, gdy chcesz mieć strukturę danych z leniwą oceną. (Przepraszam, że nie uwzględniłem moich zerowych czeków i nie uczyniłem go bezpiecznym dla wątków, czego chciałbyś, gdybyś próbował naśladować niezmienną strukturę danych).

Jak widać, każda z tych klas jest budowana przez pewnego rodzaju PhotographRepository, który prawdopodobnie ma odniesienie do ThumbnailGenerator, który otrzymał poprzez wstrzyknięcie zależności.

Scott Whitlock
źródło
Powiedziano mi, że nie powinienem tworzyć zajęć bez zachowania. Nie jestem pewien, czy kiedy mówisz „struktury danych”, masz na myśli klasy lub coś z C ++ (czy to ten język?). Jedyne struktury danych, które znam i używam, to prymitywy. Usługi i część DI są na miejscu, mogę to zrobić.
ChocoDeveloper
@ChocoDeveloper: czasami klasy bez zachowania są przydatne lub konieczne, w zależności od sytuacji. Są to tak zwane klasy wartości . Normalne klasy OOP to klasy z zakodowanym zachowaniem. Komponowalne klasy OOP mają również zakodowane zachowanie, ale ich struktura składu może powodować wiele zachowań potrzebnych aplikacji.
rwong
3

Zidentyfikowałeś jedną funkcjonalność, którą chcesz wdrożyć, więc dlaczego nie powinna być oddzielna od wszystkiego, co dotychczas zidentyfikowałeś? To właśnie sugerowałaby zasada jednolitej odpowiedzialności .

Utwórz IImageResizerinterfejs, który pozwala przekazać obraz i rozmiar docelowy i który zwraca nowy obraz. Następnie utwórz implementację tego interfejsu. Istnieje wiele sposobów na zmianę rozmiaru zdjęć, więc możesz nawet uzyskać więcej niż jeden!

Chris Pitman
źródło
+1 dla odpowiedniego SRP, ale nie wdrażam rzeczywistej zmiany rozmiaru, która została już przekazana do biblioteki strony trzeciej, jak podano.
ChocoDeveloper
3

Zakładam te fakty na temat metody, która zmienia rozmiar obrazów:

  • Musi zwrócić nową kopię obrazu. Nie można zmodyfikować samego obrazu, ponieważ spowodowałby uszkodzenie innego kodu, który ma odniesienie do tego obrazu.
  • Nie potrzebuje dostępu do wewnętrznych danych klasy Image. Klasy obrazów zazwyczaj muszą oferować publiczny dostęp do tych danych (lub ich kopii).
  • Zmiana rozmiaru obrazu jest złożona i wymaga wielu różnych parametrów. Może nawet punkty rozszerzalności dla różnych algorytmów zmiany rozmiaru. Przekazanie wszystkiego spowoduje duży podpis metody.

Na podstawie tych faktów powiedziałbym, że nie ma powodu, aby metoda zmiany rozmiaru obrazu była częścią samej klasy Image. Najlepszym rozwiązaniem byłoby wdrożenie go jako statycznej metody pomocniczej klasy.

Euforyk
źródło
Dobre założenia. Nie jestem pewien, dlaczego powinna to być metoda statyczna, staram się ich unikać pod kątem testowalności.
ChocoDeveloper
2

Odpowiednia może być klasa przetwarzania obrazu (lub menedżer obrazu, jak ją nazywacie). Przekaż swój obraz do metody CreateThumbnail w procesorze obrazu, na przykład, aby uzyskać obraz miniatury.

Jednym z powodów, dla których zasugerowałem tę trasę, jest to, że mówisz, że używasz zewnętrznej biblioteki przetwarzania obrazu. Usunięcie funkcji zmiany rozmiaru z samej klasy Image może ułatwić wyodrębnienie kodu specyficznego dla platformy lub kodu innej firmy. Jeśli więc możesz używać swojej podstawowej klasy obrazu na wszystkich platformach / aplikacjach, nie musisz zanieczyszczać jej kodem specyficznym dla platformy lub biblioteki. Wszystko to może znajdować się w procesorze obrazu.

Grandmaster B.
źródło
Słuszna uwaga. Większość ludzi tutaj nie rozumie, że już deleguję najbardziej skomplikowaną część do biblioteki innej firmy, być może nie byłem wystarczająco jasny.
ChocoDeveloper
2

Zasadniczo, jak już powiedział Doc Brown:

Utwórz getAsThumbnail()metodę dla klasy obrazu, ale ta metoda powinna tak naprawdę delegować pracę do jakiejś ImageUtilsklasy. Więc wyglądałoby to tak:

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

I

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

Umożliwi to łatwiejszy do przeglądania kod. Porównaj następujące:

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

Lub

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

Jeśli ta ostatnia wydaje się dla ciebie dobra, możesz także trzymać się gdzieś tej metody pomocniczej.

Deiwin
źródło
1

Myślę, że w „domenie obrazu” masz tylko obiekt obrazu, który jest niezmienny i monadyczny. Pytasz obraz o wersję o zmienionym rozmiarze, a on zwraca wersję o zmienionym rozmiarze. Następnie możesz zdecydować, czy chcesz pozbyć się oryginału, czy zachować oba.

Teraz miniatura, awatar itp. Wersje obrazu to zupełnie inna domena, która może poprosić domenę obrazu o różne wersje określonego obrazu, które użytkownik może przekazać. Zwykle ta domena również nie jest tak duża ani ogólna, więc prawdopodobnie możesz zachować ją w logice aplikacji.

W aplikacji na małą skalę zmieniałem rozmiar obrazów w czasie odczytu. Na przykład mogę mieć regułę przepisywania apache, która deleguje do php skryptu, jeśli obraz „http://my.site.com/images/thumbnails/image1.png”, gdzie plik zostanie pobrany za pomocą nazwy image1.png oraz zmieniono rozmiar i przechowywano w pliku „thumbnails / image1.png”. Następnie przy następnym żądaniu do tego samego obrazu apache będzie wyświetlał obraz bezpośrednio bez uruchamiania skryptu php. Odpowiedź na pytanie findAllImagesWithoutThumbnails jest automatycznie udzielana przez apache, chyba że potrzebujesz statystyk.

W aplikacji na dużą skalę wysyłałbym wszystkie nowe obrazy do pracy w tle, która zajmuje się generowaniem różnych wersji obrazu i zapisuje go w odpowiednich miejscach. Nie zawracałbym sobie głowy tworzeniem całej domeny lub klasy, ponieważ jest mało prawdopodobne, aby ta domena przerodziła się w straszny bałagan spaghetti i złego sosu.

andho
źródło
0

Krótka odpowiedź:

Moim zaleceniem jest dodanie tej metody do klasy image:

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

Obiekt Image jest nadal niezmienny, te metody zwracają nowy obraz.

Tulains Córdova
źródło
0

Jest już kilka świetnych odpowiedzi, więc rozwinę trochę heurystyki za sposobem identyfikacji obiektów i ich obowiązków.

OOP różni się od prawdziwego życia tym, że przedmioty w prawdziwym życiu są często pasywne, aw OOP są aktywne. I to jest sedno myślenia obiektowego . Na przykład, kto zmieniłby rozmiar obrazu w prawdziwym życiu? Człowiek, który jest mądry pod tym względem. Ale w OOP nie ma ludzi, więc obiekty są inteligentne. Sposobem na wdrożenie tego „skoncentrowanego na człowieku” podejścia w OOP jest wykorzystanie klas usług, na przykład Managerklas notorycznych . Zatem obiekty są traktowane jako pasywne struktury danych. To nie jest metoda OOP.

Istnieją więc dwie opcje. Pierwszy, tworzenie metody Image::createThumbnail(), jest już rozważany. Drugim jest stworzenie ResizedImageklasy. Może być dekoratorem Image(zależy od domeny, czy zachować Imageinterfejs, czy nie), chociaż powoduje pewne problemy z enkapsulacją, ponieważ ResizedImagemusiałby mieć Imageźródło. Ale Imagenie byłby przytłoczony zmianą wielkości szczegółów, pozostawiając go osobnemu obiektowi domeny, działając zgodnie z SRP.

Vadim Samokhin
źródło