Přeskočit na hlavní obsah

Jak na práci s obrázky?

Tak se nám hezky rozjela diskuze u Jakuba Vrány nad prací s obrázky v aplikaci. Samozřejmě vůbec nejde o žádné obrázky, ale o různé přístupy k návrhu a pojetí objektového modelu a vůbec celé aplikace a její architektury.

Jako neprogramující teoretik prvního druhu mám na věc svůj systémově čistý dogmatický pohled. Každopádně pojďme se teď chvíli bavit o věcech praktických. A to nejlépe na konkrétním příkladu.

Ten není až tak úplně uměle vykonstruovaný, jak by se mohlo zdát, ale vychází z čistě praktických případů, které jsme už v reálu řešili nebo na ně v nejbližší době dost pravděpodobně narazíme.

Společné základní předpoklady, které jsou doufám dostatečně nesvazující a akceptovatelné:

  • máme sdílenou knihovnu či framework používanou napříč mnoha různými aplikacemi
  • mám v ní minimálně třídu Image, která reprezentuje jeden obrázek
  • jedna z aplikací napsaných nad knihovnou je moje CMS běžící na různých webech a serverech
  • v CMS mám znovupoužitelnou komponentu s metodou, která načte nějaký obrázek z disku, resizuje ho na požadované rozměry a uloží ho zpátky na disk, přičemž původní a nové jméno obrázku i požadované rozměry dostane jako parametr

Zajímá mě, jak v rámci naší knihovny, CMS i konkrétní aplikace nad ním rozeběhnuté vyřešit následující věci:

  1. Jak vypadá celá implementace třídy Image v knihovně?
  2. Pokud mám nějakou instanci třídy Image, jak zjistím formát obrázku a jeho rozměry?
  3. Pokud mám nějakou instanci třídy Image, jak pošlu tento obrázek na standardní výstup?
  4. Jak vypadá zadaná komponenta v rámci CMS včetně implementace dané resizovací metody?
  5. Co musím udělat, když na některém konkrétním webu běžícím nad CMS chci používat jiný algoritmus pro resize?
  6. Co musím udělat, když na některém konkrétním webu běžícím nad CMS nemůžu používat GD a musím místo toho volat ImageMagick?
  7. Co musím udělat, když na některém konkrétním webu běžícím nad CMS chci pro ukládání a načítání obrázků používat databázi?
  8. Co musím udělat, když v některé konkrétní implementaci mého CMS chci z nějakého důvodu místo třídy Image používat nějakou jinou třídu MyImage? Abych ale všechny věci, které jsem doteď mohl v knihovně a zbytku CMS dělat s třídou Image, mohl dělat i s touto jinou implementací?
  9. Totéž jako předchozí bod, plus v konkrétní instalaci místo GD musím používat ImageMagick.
  10. Totéž jako předchozí bod, plus ještě navíc neukládáme obrázky na disk, ale do databáze.

Pro jednoduchost a přehlednost neřešme teď ani dále žádné možné chybové stavy, jako je neexistence načítaného obrázku, kolizi jména ukládaného obrázku s jiným již existujícím, neplatný formát obrázku, kontroly na nulové bajty, možné chybové odpovědi, kontrolu platnosti zadaného jména souboru apod. Stejně tak pokud je někde z pohledu celého návrhu nedůležité nebo jasné, jak bude implementovaný vnitřek nějaké funkce, není nutné jej podrobně rozepisovat.

Jakube, mohl bys prosím na konkrétních příkladech vycházejících ideálně z Tvého přímočarého řešení ukázat, jak bys řešil všechny tyto body? Já za sebe samozřejmě v dalším článku ukážu zase své vlastní řešení postavené na pohledu teoretiků prvního druhu.

Komentáře

  1. OT: Z čeho usuzuješ (ač jako teoretik), že je ImageMagick horší než GD? Dle mého názoru je to přesně naopak.

    Zbytek komentářů už je na Jakubovi…

  2. [1] Nic takového neusuzuji ;).

  3. Honzo, na VŠE jste mě, programátora, naučili jednu užitečnou věc – myslet taky trochu ekonomicky :).

    Já osobně jsem také velkým příznivcem čistého, testovatelného kódu s pořádnou mírou abstrakce. Jakubův přístup bych ale určitě nezatracoval, naopak je velmi inspirativní a ekonomicky efektivní.

    Sepsal jsi zadání, kde je předem jasné, že bude zapotřebí vytvořit pořádnou míru abstrakce a objekty dekomponovat, jak to jen půjde. Investice do psaní a přemýšlení navíc (nemusí ho být navíc až tolik, ale nějaké bude) se skoro jistě v budoucnu finančně vyplatí.

    Jenomže pak jsou tu projekty, kdy může jít o skutečně předčasnou a zbytečnou abstrakci a dekompozici. Budu-li řešit malý projekt pro malého klienta (s omezenými prostředky), s radostí vezmu NotORM a budu za jedno odpoledne hotov. Samozřejmě – je tu riziko, že se projekt rozjede a za nějakou dobu bude chtít (v té době už zbohatlý) klient přejít na NoSQL databázi a já budu muset přepsat celou aplikaci. Neskutečná práce navíc.

    Jenomže teď ta ekonomie a řízení rizik. Řekněme, že rozpočet na projekt je fixní, protože který klient si připlatí v počátku za nějakou abstrakci, že… Náklady na předčasnou (a možná i zbytečnou) abstrakci hned v úvodu budou řekněme 5.000 PJ /čti peněžních jednotek :)/, a tak při její realizaci budu jako dodavatel v důsledku o 5.000 PJ chudší. Pravděpodobnost, že se jednou bude přecházet na NoSQL databázi, odhadněme třeba na 0,01. Pokud by k tomu náhodou opravdu došlo, obnášel by přechod z abstraktního řešení řekněme 1.000 PJ a z přímočarého řešení řekněme 20.000 PJ. V tom případě já osobně rozhodně půjdu do přímočarého řešení. Ochoten podstoupit 1% riziko, že na tom prodělám a vidinou, že na 99% budu o 5.000 PJ bohatší. :)

    Vývoj přímočarého řešení s 1% šancí, že aplikace bude růst, je něco diametrálně jiného, než vývoj CMF, na kterým už v době návrhu vím, že poběží XYZ webů využívající X úložišť. Myslím, že právě proto se s Jakubem nikdy neshodnete. Ty předem předpokládáš, že bude hojně docházet ke změnám úložišť a je pro Tebe výhodnější se na to připravit, zatímco u Jakuba mám pocit, že ty změny příliš nepředpokládá (a já s ním souhlasím, například jen dvakrát v životě jsem migroval nějaký projekt na jinou databázi), a proto je pro něj přímočaré řešení lepší.

    Pokud by Jakub předem měl vyvíjet zmíněné CMF, jsem si jist, že by taky použil odpovídající abstrakci (sic by ji možná naimplementoval po svém).

  4. Souhlasím s Vojtěchem.

    Ty i Jakub jste se ve svojí „pravdě“ zabarikádovali tak, že už nejste schopní vidět, že každý řešíte úplně jiné zadání (byť možná se stejným nadpisem). Přestože si myslím, že Jakub programuje stylem „účel světí prostředky“ (a ano, účel světí prostředky, ale jenom do chvíle prvního nutného/zbyteč­ného/whatever refaktoringu :-), to, cos tu ty sepsal, nemá s jeho původním problémem vůbec nic společného. A z toho taky plyne, že se prostě nedohodnete, dokud nepochopíte, že univerzální pravda neexistuje a každý mluvíte o něčem jiném.

  5. [3][4] Díky oběma za komentáře. Určitě souhlasím s tím, že jsou to dva různé přístupy, kdy každý se hodí pro jinou situaci. Stejně tak si rozhodně nemyslím, že ten můj přístup je jediný správný a že ten Jakubův se pro žádné případy nehodí – ostatně jsem to i v diskuzích na Jakubově blogu napsal. Ono to není ve skutečnosti zdaleka tak vyhrocené a polarizované, jak to asi navenek vypadá :).

    Nesouhlasím ale s Vojtěchem v hodnocení toho, pro které případy se který přístup hodí. Já si totiž myslím, že Jakubův přístup se paradoxně nejvíc hodí na obrovské aplikace typu Facebook. Tam totiž jde zejména o efektivitu a výkon aplikace, takže každý hack a každá ošklivost je dobrá, když ušetří pár milisekund. Zároveň je to jen jedna aplikace, takže otázky, jako je znovupoužitelnost, nejsou až tak moc důležité.

    Naopak si ale myslím, že právě pro aplikace za 5.000 PJ je téměř nutností ten „můj“ :) přístup. Základní vlastností takových malých levných aplikací totiž je, že jich vyrábíte desítky nebo stovky. A pro dosažení dlouhodobé efektivity právě tady musíte co nejvíc reusovat a všechno snadno paušálně udržovat. Dekompozice do rozumné míry tu není od věci a úvodní náklady na vytvoření nějakého základu se poměrně brzy vrátí – nemluvě o tom, že často už takový základ dávno existuje v podobě různých frameworků.

    Když jsem psal o změně uložiště, tak jsem to nemyslel v rámci jedné aplikace. Ale tak, že jedna aplikace to bude ukládat do databáze, druhá na disk, třetí úplně jinam. Jedna bude běžet nad GD, další nad ImageMagicem. Psát pro každou znovu celou třídu Image nebo kolem ní dělat nějaké ad hoc ob_start obálky a jiná zvěrstva je dlouhodobě strašně neefektivní a neudržovatelné. Radši si místo toho změním jeden řádek v konfigurací DI kontejneru.

  6. … premature optimization is the root of all evil :-)

  7. [5] Já jsem zde tou levnou aplikací myslel spíše nějakou skutečně malou a na míru udělanou, například nějakou microsite k divadelnímu představení. Očekávám, že na ní budou statické informace, možnost rezervace, zobrazení mapky hlediště s označenými sedadly, kde je ještě místo… Zkrátka v mé úvaze „webová aplikace“ !== „typizovatelné webové stránky“. :)

    CMF, kde na jednom jádru mají běžet stovky webů, považuji za rozsáhlou aplikaci, třebaže vyprodukované koncové weby působí jednoduše.

    Ono typů toho, co vše se dá vyvíjet, je nepřeberné množství a vždy se lze bavit jen o vhodnosti konkrétního řešení pro konkrétní zadání. Jinak je celá diskuse zavádějící a někdy jeden velký flamewar. :)

    Jenom ale tak docela nesouhlasím s tím, že přímočará řešení jsou paraxodně vhodná pro aplikace typu Facebook. Má argumentace je následující: přímočará řešení přece jenom bývají hůře testovatelná, udržovatelná, refaktorovatelná a určitě mají větší tendenci stávat se neelegantními a nečistými při dalším růstu aplikace (pokud autor není geniální). Nic z toho není u větších aplikací žádoucí. Tipuji, že u Facebooku musí být výsledná architektura otázkou ohromného kompromisu desítek faktoru (cena, výkon, abstrakce, testovatelnost, udržovatelnost…).

  8. V první řadě doufám, že implementace resizovacího algoritmu nemá být přímo součástí CMS komponenty. Trochu to tak totiž ze čtvrtého bodu předpokladů a čtvrtého bodu zadání vypadá. To by totiž byl hodně špatný návrh – proč bych se měl kvůli použití tohoto algoritmu starat o nějaké CMS? V hierarchii by to bylo asi tak o tři patra vedle (knihovna > framework > jádro CMS > jeho komponenty).

    Na zbytek bych s naprostým klidem použil Nette\Image. Alternativy bych vyřešil definicí rozhraní IImage a jeho implementací třídami Image, ImageMagick a MyImage. Jiný algoritmus pro resize bych věnoval komunitě v podobě nového flagu u současného Nette\Image::resize.

    Asi je nasnadě, že ukládání do databáze bych řešil pomocí $notORM->picture()->insert(array('data' => $image->toString())).

  9. Jakube, tahle Tvoje odpověď je příliš obecná, protože moje řešení by pak bylo „na zbytek bych s klidem použil Medio\Image“. Mně zajímají právě ty konkrétní detaily, jak bys to přesně použil, protože na těch se láme chleba. Každopádně i k tomuhle málu:

    • Nette\Image použít nemůžeš, protože nepodporuje resizování pomocí ImageMagick, ale jen natvrdo pomocí GD knihovny, pokud mi něco neuniklo.
    • I kdyby to podporovalo, nemusel bys pak pro překlopení z GD na ImageMagick dělat změnu na více různých místech aplikace? Nevím, ptám se – proto by se hodilo vidět ty detaily.
    • Kdyby Nette\Image podporovala jak GD i ImageMagick, tak bys pro přidání nového resize flagu přímo dovnitř Nette\Image psal oba dva algoritmy pro obě dvě knihovny?
    • Když budeš chtít použít třídu MyImage v kombinaci s ImageMagick, tak budeš psát ještě další čtvrtou třídu MyImageMagick? A s každou další takovou potřebnou kombinací Ti bude exponenciálně růst teoretický počet tříd, které by v krajním případě bylo potřeba takto psát?
    • Ukládání do databáze bys takhle psal přímo ručně do dané komponenty? Nebo do třídy Image? Nebo kam?

    ** Pokud do komponenty, tak bys měl více různých komponent, z nichž bys občas použil tu a občas tamtu? A co když budeš do komponent chtít přidat nějakou další funkčnost? Jakože budeš, protože proto se to do komponenty uzavírá. To ji budeš rozkopírovávat mezi všechny takové komponenty?
    ** Pokud do třídy Image, tak bys pak měl nakonec jako implementaci IImage třídy Image, ImageMagick, MyImage, DbImage, DbImageMagick, MyDbImageMagick…? A mezi všemi budeš rozkopírovávat věci jako zjištění rozměrů či typu obrázku? Opět – chtělo by to konkrétnější kód, takhle obecně napsáno to může znamenat cokoliv.

  10. Do tvých komentářů se bojím psát jakýkoliv kód, protože by se nejspíš zase rozbil – jako se to stalo u triviální ukázky v prvním komentáři (ale díky za ruční opravu), a nejspíš ani nezvýraznil.

    Takže pokud něco napíšu, tak k sobě. Ale pomalu mě to přestává bavit, protože mám pocit, že čím víc toho napíšu, tím míň toho chápeš href=„http://­php.vrana.cz/mu­ze-mit-trida-image-metodu-resize.php#d-12123“ rel=„nofollow ugc“>http://ph­p.vrana.cz/mu­ze-mit-trida-image-metodu-resize.php#d-12123 :-).

  11. Ač se neshodnete, dali jste světu pojem „předčasná abstrakce a dekompozice“, což třeba jednou vezme za své i Fowler a odpadne dumání nad tím, co vám vytesat na náhrobek ;-)

    Jedna poznámka: přímočará a snadná řešení rozhodně nejsou v rozporu s testovatelností a udržovatelností. Projevem lidského génia je totiž nalezení přímočarého a snadné řešení úkolu, které přitom vyhovuje všem požadavkům na testovatelnost a udržovatelnost ;-)

  12. Ať už Jakub něco napíše nebo, mohl bys Honzo sepsat slibovaný článek s konkrétním tvojím řešením? Dost by mě to zajímalo=) Díky

  13. Přeformuluj zadání prosím tak, aby nehovořilo o třídě MyImage. Jinými slovy: uveď funkčnost, kterou by tahle třída měla podle tvých představ zastávat. Přijde mi trochu zvláštní v zadání úlohy, která se má zabývat návrhem tříd, vynucovat existenci některých z nich.

  14. Ako to nakoniec vyzerá s ďalším článkom? Viem, Jakub ešte nevydal svoje riešenie. Je však možné, že zverejníš ty svoje aj keď Jakub hodenú rukavicu nezdvihne?

  15. [14] Rád bych připomněl, že puk je teď na straně Honzy [13].