Může mít třída Image metodu resize?
Školení, která pořádám
U článku Práce s vlastnostmi pomocí metod se rozvinula zajímavá diskuse několika vynikajících teoretiků o tom, v čem vlastně spočívá objektově orientované programování. Přes několik příkladů jsme se dostali ke třídě Image
, která reprezentuje obrázek. V diskusi jsme se bavili o metodě save
, která obrázek uloží do souboru na disku, ale tato metoda je trochu složitější a diskuse o ní ještě nedospěla ke konci (chtěl bych totiž, aby tato metoda vybrala formát souboru podle jeho názvu – tedy aby save("krajina.jpg")
vytvořilo soubor ve formátu JPG – to současné návrhy dosud nezvládnou). Současné poznatky bych tedy shrnul na jednodušší metodě resize
, která by měla zmenšit nebo zvětšit obrázek.
Přímočaré řešení
Nejprve moje přímočará implementace a její použití:
<?php
class Image {
private $resource;
function resize($width, $height) {
$resized = imagecreatetruecolor($width, $height);
imagecopyresampled($resized, $this->resource, 0, 0, 0, 0, $width, $height, imagesx($this->resource), imagesy($this->resource));
$this->resource = $resized;
}
}
$image->resize(320, 240);
?>
Část teoretiků
Část teoretiků (Jiří Knesl, Ondřej Mirtes, Jan Tichý) tvrdí, že třída Image
by měla mít jen jednu zodpovědnost a to sice reprezentaci obrázku. Veškeré operace (v diskusi ukládání do souboru, ale totéž se dá vztáhnout i na změnu rozměrů) by měly být vyčleněny do samostatných tříd implementujících patřičná rozhraní. Jak taková implementace vypadá?
<?php
interface ImageResizer {
function resize(Image $image, $width, $height);
}
class ImageResizerResampled implements ImageResizer {
function resize(Image $image, $width, $height) {
$original = imagecreatefromstring($image->toString());
$resized = imagecreatetruecolor($width, $height);
imagecopyresampled($resized, $original, 0, 0, 0, 0, $width, $height, imagesx($original), imagesy($original));
ob_start();
imagepng($resized);
$image->fromString(ob_get_clean());
}
}
class Image {
private $resource;
function fromString($data) {
$this->resource = imagecreatefromstring($data);
}
function toString() {
ob_start();
imagepng($this->resource);
return ob_get_clean();
}
}
$imageResizer = new ImageResizerResampled;
$imageResizer->resize($image, 320, 240);
?>
Implementace je tak složitá proto, že třída Image
rozhodně nesmí zveřejnit svůj $resource
, protože to by bylo porušení zapouzdření a vnitřnosti třídy bychom později nemohli snadno vyměnit třeba za ImageMagick. Jaké jsou výhody tohoto řešení?
- Celkem snadno si můžeme vytvořit nový resizer, který bude změnu rozměrů provádět jiným algoritmem.
- Při testování můžeme resizer snadno mocknout, protože zjistíme, že ten normální je pro testování moc pomalý (aby ne).
Jaké vidím nevýhody?
- Pokud se v budoucnu rozhodneme resizer změnit, tak ho musíme vyměnit na všech místech programu.
- Kvůli dvojnásobné serializaci do formátu PNG a načtení z něj bude kód zbytečně pomalý.
- Kvůli implementaci rozhraní nemůže metoda
resize
dostat žádné další parametry (např. $blur
) – ty musíme předat konstruktoru, takže když chceme vygenerovat třeba víc verzí obrázku s různým blurem, musíme pro každou verzi vytvořit nový resizer.
- Kód je poměrně komplikovaný a špatně se používá.
Druhá část teoretiků
Druhá část teoretiků (Jan Tichý, např. René Stein) jde o něco dál. Souhlasí s tím, že by funkčnost měla být vyčleněna do samostatných tříd, ale zároveň se kloní k tomu, že by třída Image
neměla být jen primitivní obálka na data, se kterou pracují externí metody, ale že by tyto metody měla sama nabízet. Kód tedy využije všechno z předchozího příkladu a přidá k tomu:
<?php
class Image {
private $resizer;
function __construct() {
$this->resizer = new ImageResizerResampled;
}
function setResizer(ImageResizer $resizer) {
$this->resizer = $resizer;
}
function resize($width, $height) {
$this->resizer->resize($this, $width, $height);
}
}
$image->resize(320, 240);
?>
Tohle je řešení podle Reného. Nelíbí se mi na něm dvě věci. První je to, že se v konstruktoru vytváří dost možná zbytečný objekt. Druhá námitka je závažnější – třída Image
musí vědět o existenci ImageResizerResampled
a třída ImageResizerResampled
musí pochopitelně vědět o existenci třídy Image
.
Honza nabízí alternativu: konstruktor a metodu setResizer
vyhodíme a použijeme anotaci:
<?php
class Image {
/** @inject */
private $resizer;
function resize($width, $height) {
$this->resizer->resize($this, $width, $height);
}
}
?>
Přiznám se, že netuším, čím bude to @inject
zpracované, ani jak vlastně funguje. Nicméně takhle rozsáhlá magie mě u odpůrců metod __get a __set opravdu překvapuje a na řešení mi vadí ze všeho nejvíc.
Závěr
Objektový návrh by měl vést především ke snižování složitosti jednotlivých částí aplikace. To spočívá mimo jiné i v návrhu API, se kterým se bude dobře pracovat. Další bod se týká omezení závislostí mezi jednotlivými třídami. Lepší je podle mě navrhnout třídu Image
tak, aby měla metody save
, resize
a další, které fungují samy o sobě. A když někomu nevyhovují, tak ať si nějak poradí – můžu zveřejnit metody toString
a (statickou) fromString
a tím to uživateli trochu usnadnit, ale když nechám API jen přes soubory, tak se to dá taky zvládnout. A víte co? Já bych zpřístupnil i $resource
– ať si uživatel zkontroluje, jestli mu to vrátí to, co čeká, a když ne, tak ať použije náhradní řešení nebo prostě vyhodí výjimku. Výsledkem je kód, který se dá použít mnohem snadněji a bude fungovat mnohem rychleji, i když se při něm uživatel může střelit do nohy.
Přijďte si o tomto tématu popovídat na školení Programování v PHP 5.
Diskuse
bene:
Problem se zbytečným vytvářením třídy ImageResizerResampled lze vyřešit velmi elegantně.
<?php
class Image {
private $resizer;
function setResizer(ImageResizer $resizer) {
$this->resizer = $resizer;
}
function getResizer()
{
if ($this->resizer === null) {
$this->resizer = new ImageResizerResampled;
}
return $this->resizer;
}
function resize($width, $height) {
$this->getResizer()->resize($this, $width, $height);
}
}
$image->resize(320, 240);
?>
Martin Hruška:
To je přece věčný boj. Dnes mezi Javisty a Rubysty.
Java: cizí programátoři jsou zlí. Musíme před nimi náš kód ochránit!
Ruby: cizí programátoři jsou hodní. Můžou rozvrtat úplně všechno a klidně můžou v naší knihovně změnit plus za mínus když se jim to hodí.
Stačí si tedy přečíst pár starých článků s diskusemi a je to. Tahle diskuse je už dávno vyčpělá.
Dnes už na diskuze nebudu mít moc času, ale to přece není moje řešení - já jsem říkal, že metodu Resize i s implementací *za určitých okolností* nechám ve třídě Image a že vidím velké rozdíly mezi metodou Save a Resize.
Je to napsáno i u mě v diskuzi. Viz i dále. A teoretik nejsem.
"Metodu Resize mít objekt může a může ji i implementovat. V některých řešeních se vám ale transformace s obrázky mohou začít (Rotate, Skew) množit, a proto řešení refaktorizujete a zavedete raději ImageServices, aby nemusela vše řešit jedna třída. V průběhu návrhu a vývoje se objevují další doposud skryté doménové objekty a služby."
Jaké jsou tedy přesně ty rozdíly mezi metodou Save a Resize?
Od Save chci, aby dokázala obrázek uložit do souboru a případně i do jiného úložiště (třeba do databáze), které si definuji bokem.
A od Resize chci, aby dokázala obrázek zmenšit metodou nejbližšího souseda a případně i jakoukoliv další (třeba bikubicky), což si tedy také definuji bokem.
Problémy jsou zcela analogické a nevidím jediný důvod, proč bych pro ně měl používat různé postupy.
Jednou je to interakce Image <-> Image, tj. resize pracuje jen s Image, podruhé je to vztah Image <-> Filesytem, tj. save() serializuje data do určitého formátu _a navíc jej ukládá na FS_.
Daniel Steigerwald:
Jakube, než se tu spustí diskuze, doporučuji si přečíst těchto 38 vynikajících stránek (Guide: Writing Testable Code)
http://goo.gl/DirZjDost to odabstraktní diskuzi, problémy které zmiňoval Jan Tichý, pak budou zřejmější.
Myšlenka je jasná, ale jako s celkem s tím nesouhlasím. Za prvé některé věci neplatí pro PHP (např. omezení zavedená kvůli možnosti testovat multi-threadově – ha, ha). Za druhé některé příklady podle mě fatálním způsobem porušují zapouzdření, které je pro udržení kvalitního kódu obvykle mnohem důležitější než snadná testovatelnost. Za třetí nám ten „jediný správný způsob“ (Guice nebo manuální DI) nezabrání v tom, střílet se do nohy – jeho používání nejde vynutit např. na úrovni kompilátoru. Za čtvrté jde často o demagogii – je snad následující kód netestovatelný?
<?php
class C {
var $data;
function save($filename) {
file_put_contents($filename, $this->data);
}
}
?>
Nikoliv, jen kvůli vazbě na souborový systém test možná proběhne maličko pomaleji. Což vzhledem k možnosti ukládat do tmpfs nebo (v PHP) zaregistrovat vlastní protokol (např. var://) ani nemusí být pravda. Totéž se dá říct o databázi – mockovat ji je obvykle úplně samoúčelné a stačí ji nakonfigurovat tak, aby používala testovací instanci.
A za páté rychlost testů není vše – podřizovat tomu např. rychlost běhu samotné aplikace mi přijde absurdní. Navíc když si můžu vybrat jednoduchý a přehledný kód, který se trochu pomaleji testuje, ale o to hůř v něm mohu udělat chybu, neváhám ani chvíli.
Asi napíšu samostatný článek.
"Přiznám se, že netuším, čím bude to @inject zpracované, ani jak vlastně funguje (...) a na řešení mi vadí ze všeho nejvíc."
Promiň, Jakube, tady ses poměrně odkopal :) Když nevíš jak to funguje, tak je dost pošetilé to zhodnotit jako špatné řešení.
Já jsem daleko od toho být php oop guru, ale osobně se mi použití anotací pro Dependency Injection dost líbí.
Je to přesně naopak. Na řešení mi vadí ze všeho nejvíc právě to, že netuším, jak funguje. PHP znám myslím „docela dobře“, o souvisejících knihovnách mám také slušné povědomí, přesto je pro mě tohle magie, kterou nechápu a která navíc nebude v PHP fungovat univerzálně, ale závisí na tom, co je okolo.
Za magii mnozí považují i prosté <?php
class C {
private $x;
function __get($name) {
return $this->$name;
}
}
?>, protože podle nich není na první pohled patrné, jaktože funguje $c->$x, když je $x přece privátní. A @inject je magie mnohem hrubšího zrna - pro její pochopení se nestačí ani podívat do třídy, ale člověk si musí nastudovat nějaký proprietární mechanismus, který funkčnost zajišťuje.
Na všem mi přijde nejhorší to, že skupina lidí považující __get za příliš magické zároveň prosazuje takovéhle použití anotací.
Daniel Steigerwald:
Je to Jakube přesně naopak. Injectováním závislostí přes konstruktor, se třída stane podstatně přehlednější, než když máš service locatory roztroušené všude uvnitř třídy. Patrně tě čeká aha efekt spojený se zjištěním, že si doposud nepsal zdaleka tak dobrý kód, jak bys psát mohl. (mírně řečeno) Čím dříve si to uvědomíš, tím lépe. Mne se to stalo také ;) Právě po přečtení odkazovaného Miškova dokumentu.
A kde probůh v tomhle (http://php.vrana.cz/muze-mit-trida-image-….php#inject) vidíš nějaké injectování závislostí přes konstruktor? Já teda nikde a navíc ani netuším, kde bych to měl hledat.
Navíc i kdyby to nebylo tak magické, tak bych k tomu měl výhrady. Co když bude mít třída 7±2 závislostí a já z nich budu potřebovat jen pár? Musím je konstruktoru předat všechny? Není to zbytečné a nebude to trochu zmatek?
Daniel Steigerwald:
přečti si Miška pls
analytik:
Nadavat na to, ze nerozumim magii frameworku ktery neznam, mi pripada, s prominutim, hloupe. Dvakrat tak hloupe, protoze je to jako rikat ze kod by nemel pouzivat design patterny, protoze k nim muze prijit programator, ktery dany design pattern nezna.
Prakticky nikdo z vas tady panove neuvadi real-world examply, pouziti konkretnich frameworku na konkretni ucel. To se pak snadno odsuzuje "@inject se mi nelibi protoze nevim jak funguje".
V těchto dvou věcech vidím zcela zásadní rozdíl. Proti návrhovým vzorům nic nemám, protože využívají standardní vlastnosti jazyka. Takže kdo zná jazyk, dokáže pochopit i návrhový vzor.
Stejně tak nemám nic ani proti „magii“ zpřístupněné PHP v podobě metod __set a podobných. Opět – kdo zná jazyk, dokáže to pochopit, jen se občas musí podívat maličko hlouběji.
Ale u @inject už jazyk znát nestačí – musíš znát knihovnu, která to zpracovává, a nedá se na to spolehnout, protože každá knihovna to může zpracovat jinak.
A ještě jednou si dovolím připomenout, že tenhle článek vzešel do značné míry jako odpověď na nesouhlasné reakce o přílišném používání „PHP magie“: http://php.vrana.cz/prace-s-vlastnostmi-pomoci-metod.php.
K tomu bych pár věcí doplnil:
* v PHP je tu problém, že anotace nejsou součástí jazyka a tím (kromě absence kontroly validity) chybí takové základní věci jako namespaces, což může způsobit tu jinou interpretaci v jiném frameworku. Přesněji řečeno, teoreticky může existovat implementace bez těchto neduhů, ale ještě jsem ji neviděl.
* Ačkoli v Javě jsou anotace standardní součástí, zase je tam potřeba používat DI kontejner, jinak to může (proti pravidlu fail fast) skončit na NullPointerException při použití. Ale se slušným JavaDocem k anotaci Inject (nebo podobné) se s tím dá už lépe žít.
* Ve Scale jsou k dispozici implicitní parametry, kterými jde objekty bez explicitní zmínky předávat ostatním objektům. Je v tom sice troška magie (oostatně jako u každé automatiky), ale dá se s tím celkem žít (kompilátor dokonce asi umí vypsat kód s doplněnými implicitními parametry) a při zachování automatizovanosti to splňuje pravidlo fail fast. Taky to možná není až tak robustní a 'enterprise' jako některé DI kontejnery, ale většinou to může stačit.
Uf, původně jsem chtěl Tvůj příspěvek lehce doplnit, ale teď to může působit jako agitace.
analytik:
OK, souhlasim s tim, ze pokus se nekomu nelibi magie PHP, pretlacovat magii frameworku s funkcionalitou zapouzdrenou v komentari je trochu oxymoron.
Kazdopadne design patterny podle me potrebuji vysvetleni - potrebuji kontext, priklady kdy ano a kdy ne, atd. Jenom z precteni kodu clovek sam nepochopi *proc* se to dela takhle - muze pochopit jak to funguje, ale jak s dobrym OOP, nestaci jenom vysvetlit co je konstruktor a co je dedicnost, vyzaduje si zmenu mysleni.
Souhlasim, ze az budou v PHP5.4/6 traits, bude to mnohem elegantnejsi nez funkcionalita v komentarich. Jako funkcionalita konkretniho frameworku to je podle me ale dostacujici, jde o to zdokumentovat to chovani a popsat jak ho debugovat. Problem s magii je podle me prave v tom, ze se da snadne prehlednout a nesnadno debugovat.
Mně se to stalo také :) Seznámení s DI u Honzy Tichého a přednášky od Miška Hevery považuji za přelom ve svém programování.
Landy:
Tohle řešení se třeba využívá v MVC kde mám nějakou IControllerFactory která vrací inicializované kontrollery a pokud treba v PostControlleru chci použít IPostRepository tak si v controlleru takhle označím třeba konstruktor. Nasledně je potřeba mit implementaci IControllerFactory která je DI kontejnerem a ta už podle určité konfigurace vrací inicializované objekty controlleru a díky tomu budu mít v mém PostControlleru nainicializovaný repozitář. jediné co musím udělat je to, že tomu kontejneru reknu ze pokazde kdyz uvidi @inject IPostRepository tak má vložit DBPostRepository
Je přece normální, že se člověk učí. Prostě přijde k novému projektu, kde mu stávající tým řekne "hele, podívej, používáme tu dependency-injection s anotacemi - pokud ses s tím ještě nesetkal, přečti si tohle a tohle."
Jasně, nový vývojář s tím stráví půl den, než se mu to v hlavě nově přeuspořádá :) - ale pokud bude pak následující měsíce až roky psát přehlednější, testovatelnější, flexibilnější a udžovatelnější kód, tak mělo kouzlo smysl.
Myslím, že Jakubovi jde o to, že pokud potřebuje z obrázku vyrobit avatar o rozměrech 64x64, tak raději napíše cca
<?php
Image::fromFile($uploaded)->resize(64, 64)->sharpen()->save($destFile);
?>
než aby se učil firemních dependency-injection containery a jejich konfiguráky využívající anotace k dosažení téhož. V tomto konkrétním případě lze obhájit výhody dekompozice a následné DI kompozice jen stěží, na druhou stranu to nelze použít ani jako obecný protiargument. Tedy vnímám to jako útok pragmatika na dogmatiky.
Ono by asi bylo dobře zmínit i kontext, ve kterém se ta třída bude používat. Pokud to má být jednoduchá standalone třída na zpracování obrázků, nemá smysl nějaké DI moc řešit. Pokud to má být součást nějakého většího celku (projektu, knihovny, frameworku, čehokoli), kde DI nějakým způsobem funguje, je jenom logické ho použít i tady.
Denis:
Toto je nejcennější příspěvek celé diskuze. Jelikož pokud je příklad vytřený z kontextu, potom se dají obhájit všechna řešení :) Protože udělám řešení a pak si k němu vymyslím kontext.
"Má smysl si na výměnu pneumatik kupovat rovnou celý autoservis?" Jsem schopen (a určitě většina ostatních taky) obhájit i odpověď ANO i odpověď NE :)
Takže jde jen o to, že když něco používáte tak musíte vědět proč to používáte. Pokud bych chtěl změnu velikosti avatara, zadal bych to Jakubovi a on by mi po měsíci práce přinesl mega hnihovny ala "Část teoretiků"... pak by neco nebylo v pořádku.
Pokud bych zadal Jakubovi ať připraví nějaké jádro pro knihovny pro práci s obrýzkovými daty s možností pozdějšího rozšiřování a on by mi donesl koncepl ala "Jakub Vrána", pak by něco nebylo v pořádku.
Odpověď na otázku: "Může mít třída Image metodu resize?" je tedy kvantová :D ANO i NE...
Denis:
Ještě si zareaguji sám na sebe:
Proto je dobré se neučit jako dogmata věci typu "Může mít třída Image metodu resize?" Je třeba pouze vědět, jak se věci dají řešit a že existuje více možností. Cílem programování je potom vhodně zvolit pro řešení daného problému tu nejvhodnější variantu a né všude za každou cenu "rvát" bezhlavě jen jeden způsob.
Když se zeptám nějakého začínajíciho programátora: "A proč jsi to napsal takto a né takto?" a jediné, co mi k tomu je schopen říct je "protože to tak psali na internetu"... tak to není dobré znamení :D
Jiří Knesl:
Jak jsem zmiňoval v diskuzi u Reného S., existuje celá řada řešení, ocituju:
"V případě e-mailu, nebo obrázku (jako jsou v Jakubově článku) existuje několik možností, jak se s potřebou "konat více operací na jedněmi daty" vyrovnat. Delegací, kategoriemi (jako jsou ve Smalltalku, Obj-C), dědičností (často chyba) nebo obrácením kontroly (nemyslím teď IoC), tedy vytvořením servisní vrstvy - což jste sám v komentářích přiznal, že může být žádoucí. A v tu chvíli se už dostaneme k tomu, že máme objekty, které konají a objekty, které reprezentují a anemický model to zjevně není."
Já jsem navrhl jedno z řešení, ale výsledek by se dal implementovat kterýmkoliv z výčtu v citovaném komentáři. Proč jsem vybral právě servisní vrstvu? Protože:
- delegace: znamená v PHP odchycení __call a předávání delegátovi, v ideálním případě ještě s rozhodováním, jaký delegát se má použít (podle toho, zda je to skupina operací nad velikostí, barevným prostorem, nebo pokud do obrázku kreslíme nebo píšeme, nemluvím o situaci, kdy dokonce X obrázků slučujeme na různých pozicích)
- kategorie: v PHP nejsou, daly by se implementovat (zhruba s možnostmi Smalltalku nebo Obj-C ale bez možností např. NewtonScriptu), pokud bych si vytvořil božský Object, z kterého by dědil Image a pokud bych byl schopen za běhu změnit ten božský Object (přidat tam právě ony kategorie). Celé by se to rozbilo, jakmile bych přidal __call a zapomenul volat parent::__call
- dědičnost: zdědit obrázek a přidat resize. Na tomto řešení najde snad každý několik problematických bodů: a) není to skutečný důvod pro dědění, b) pokud bych už chtěl dědit, musím vědět z kterého "potomka" chci dědit. c) Co když budu chtít resizovat 1000 obrázků: vytvořím 1000 objektů s opakujícím se ukazatelem do paměti na metodu resize? d) co když budu chtít přidat ono zkosení, vyrovnání bílé, přidání textu... najednou budu dědit jak blázen bez jakékoliv logiky
- servisní vrstva a přepravka na data: řešení, které jsem vybral já. Byť tím vznikne objekt bez zodpovědnosti, můžu ho poslat do libovolné obrázkové servisní vrstvy, můžu ho poslat na uložení, můžu ho serializovat jako data do CSS, můžu ho poslat jako response atd. Pokud je servisní vrstva za něčím, co mi ty jednotlivé services vrací, můžu dokonce vyměnit implementaci resizovacího algoritmu bez úprav na 1000 místech.
- chain of responsibility (nezmínil jsem) - můžu posílat operaci nad obrázkem nad řetězem filtrů, které obrázek umí upravit. Problém je obdobný jak u dědičnosti: tedy sestavení řetězu, pořadí prvků - u operací jako je odšumování obrázku můžu chtít podle jiných parametrů chtít zavolat ono řetězení v různém pořadí.
- tvé přímočaré řešení (nezmínil jsem) - je jistá cesta k tomu mít časem objekt s N naprosto nesouvisejícími metodami typu: "doostřit, oříznoutu, převést do grayscale, poslat jako HttpResponse" a to je věc, které se od začátku diskuze bráním
Jan Tichý:
Než napíšu něco k samostatnému tématu, tak nejdřív prosím o opravu následujících nepřesností:
* Prosím o přeřazení z druhé skupiny teoretiků do první skupiny teoretiků. S posledním přístupem, kdy se resizer matlá dovnitř Image, nechci mít v rámci této diskuze nic společného, sic to ve zvláštních případech (nikoliv v tomto) kategoricky neodmítám.
* Rozhodně pak tedy v daném příkladu "nenabízím uvedenou alternativu". Že jsem někde ukázal nějaký příklad DI, neznamená, že jsem pro bezhlavé injectování čehokoliv kamkoliv.
* Jak už jsem napsal u předchozího článku, jakákoliv diskuze nad v mém příkladu použitým property injection je zcela podružná a nemá smysl se s ní vůbec zabývat. Prostě si místo @inject představte jakoukoliv jinou formu dependency injection. Čili jakékoliv pozastavování nad nějakou magií je zcela nepatřičné, protože odvádí diskuzi jinam.
Jakub Vrána :
Omlouvám se, omylem jsem tě podezíral z částečného pragmatismu. Přeřadil jsem tě tedy do první skupiny. Alternativu jsem tam nechal, protože i když jsi ji použil v jiném kontextu, na tomhle místě by se dala použít taky (i když ty osobně bys ji tam třeba nepoužil).
Jan Tichý:
Já nepopírám, že to může být pragmatické, zároveň je to ale zanášení zbytečné komplexity a zcela zbytných závislostí do kódu. A to je pro mě osobně větší zlo.
Jan Tichý:
Dále k uvedeným nevýhodám čistého řešení první skupiny teoretiků:
"Pokud se v budoucnu rozhodneme resizer změnit, tak ho musíme vyměnit na všech místech programu."
Pokud Jakube všechno mastíš dovnitř kódu, tak zajisté. Jinak je to ale otázka změny jednoho řádku v konfiguraci DI kontejneru.
"Kvůli dvojnásobné serializaci do formátu PNG a načtení z něj bude kód zbytečně pomalý."
To je ale přece jen problém téhle Tvojí konkrétní implementace třídy Image. Třída Image může mít i jiné rozhraní, možná klidně třeba i getResource() a setResource().
"Kvůli implementaci rozhraní nemůže metoda resize dostat žádné další parametry"
To není pravda. Buďto chceš využívat rozhraní a spoléhat se na ně, nebo nechceš. Pokud ano, tak musíš dané rozhraní respektovat bez ohledu na konkrétní implementaci.
Pokud chceš v konkrétním případě využívat něco nad rámec daného rozhraní, tak tím v daném konkrétním místě přiznáváš, že víš o konkrétní využívané implementaci Resizeru víc, než jen to, že implementuje dané rozhraní. Tedy že víš, že už je to daná konkrétní třída. A v takovém případě Ti nikdo nebrání, aby sis v dané konkrétní třídě nad rámec povinného rozhraní definoval nějakou další jinou metodu, například resizeWithBlur(), kam si dáš všechno, co v tomto specifickém případě potřebuješ. A tuhle specifickou metodu si pak na tomto místě zavoláš - což si můžeš dovolit, protože víš, že se jedná o tuhle konkrétní metodu.
"Kód je poměrně komplikovaný a špatně se používá."
S tím zcela zásadně nesouhlasím, používá se naprosto výborně a snadno, ale to je, jak už jsem pochopil, otázka subjektivního pohledu na věc.
Jakub Vrána :
Skutečně getResource() a setResource()? Zdráhám se tomu od tebe uvěřit, vždyť jde o jasné porušení zapouzdření. Jak bych v budoucnu mohl vyměnit implementaci třídy Image?
Martin Hruška:
Možná si pan Tichý jen není vědom faktu, že v PHP jsou tyhle funkce v rozšířeních udělané naprosto šíleně a neobjektově. Pokud se snažíme roubovat na tuhle prasárnu objekty, tak je logické, že to nemůže dobře dopadnout.
To se pak vyhnout kopírovaní dat sem a tam nedá. To bychom museli mít stream readery a ty zde nemáme, protože je to tam všechno nabastlené bez ladu a skladu.
Jan Tichý:
Nemyslím si, že se nutně musí jednat o porušení zapouzdření, alespoň ho tam zatím nevidím. Abychom to ale mohli posoudit, je potřeba mít třídu Image před sebou úplně celou. Předpokládám, že ani v Tvém prvním případě na začátku článku by třída nebyla autistická s privátním $resource, ke kterému se nelze nijak dostat, ale měla by asi nějaký vstup a výstup. Můžeš tedy prosím nejprve nastínit celou Tvoji třídu Image, jak by vypadala, co do vstupu a výstupu samotných dat?
Jakub Vrána :
Nejjednodušeji takhle:
<?php
class Image {
private $resource;
function __construct($filename) {
$this->resource = imagecreatefromstring(file_get_contents($filename));
}
function save($filename) {
imagepng($this->resource, $filename);
}
}
?>
Opravdu nechápu, proč by třída měla přiznávat, že je implementovaná pomocí GD. (Tedy já to chápu, ale nemůžu uvěřit tomu, že bys něco takového připustil ty.)
Jan Tichý:
Než se pustíme do nějakého přiznávání GD respektive vůbec jakékoliv nutní vazby na GD (což je něco, k čemu se chci dostat), tak ještě chvíli počkejme o krok dříve.
Moje otázka zní: Jak třídu Image použiješ na obrázky tahané například z databáze? Nebo třeba získané nějakou webovou službou ze vzdáleného serveru? Prostě když si je nechci vůbec ukládat na disk?
Jakub Vrána :
Jak jsem psal, je to nejjednodušší možné řešení. Takže vzdálené obrázky půjdou v PHP snadno předáním "http://" v názvu souboru, obrázky z databáze třeba přes data:// wrapper (http://www.php.net/manual/en/wrappers.data.php).
Nějak nevím, kam směřuješ. Když bys to rovnou vybalil, tak bys tím ušetřil práci mě i sobě.
kukulich:
Mě by docela zajímalo, jak chceš při načítání obrázku přes http:// a file_get_contents zajistit, že ti aplikace nezůstane viset, když server nebude odpovídat. Časový limit tahle funkce nemá.
Jan Tichý:
Jde mi hned v první fázi přesně o tohle, směr dalších navazujících kroků je totiž na tom dost závislý. Takže pojďme dořešit nejdřív tohle.
Kromě různých problémů s wrapperem pak budeš muset třeba u toho vzdáleného serveru řešit veškerý protokol pro komunikaci s ním, jako je třeba POST v metodě save(), nedej bože aby tam byl nějaký SOAP nebo jiné zvěrstvo. Tohle všechno chceš dávat do třídy Image? Nebo vyčleníš ukládání a načítání ven do nějaké jiné třídy, kterou budeš nějak injectovat do třídy Image?
A když budeš mít ve zbytku třídy Image nějakou společnou často využívanou funkčnost (klidně zatím třeba ten resize nebo třeba gettery pro rozměry či formát obrázku), tak asi budeš chtít mít třídu Image v nějaké library společné pro všechny Tvé aplikace. Jenže ty aplikace se budou od sebe lišit způsobem uložení dat. Jak to zajistíš? Budeš mít v každé aplikaci jinou instanci Image (třeba poděděnou a překrytou)? Nebo to budeš měnit právě výše zmíněnou injektáží?
A co když budeš chtít změnit ten resize? Tak budeš do Image injektovat další externí Resizer? A co když se nedej bože změní GD na ImageMagick? Budeš měnit celou implementaci třídy Image + tutéž změnu promítnout třeba i do Resizeru? A tak dále...? To pak jako třída Image, místo toho, co by reprezentovala jeden obrázek, bude spíš kontejner/fasáda pro celou řadu dalších injektovaných servis, z nichž v daném okamžiku navíc ani většinu z nich nepoužiješ? Jak tohle všechno uhlídáš, abys v tom u čtvrté, páté, dvanácté aplikace využívající tuhle sdílenou Image třídu neměl bordel?
Jakub Vrána :
Žádných problémů s wrapperem si nejsem vědom. Komunikaci se SOAPem do třídy Image rozhodně dávat nebudu, ani ji tam nebudu injectovat. To vyřeším úplně bokem, data ze třídy Image získám jednoduchým <?php ob_start(); $image->save(null); $data = ob_get_clean(); ?> – to by asi byla první zkratka, kterou bych si spolu se statickou metodou fromString() do Image přidal.
Jak z výše uvedeného plyne, tak žádné dědění, překrytí ani injektáž není nutná.
Když budu chtít změnit resize, tak mám dvě možnosti – vyřešit to úplně bokem bez jakékoliv závislosti na Image nebo poslat patch, který do Image tu jinou variantu přidá (pokud je dostatečně univerzálně použitelná). Žádná injektáž opět není nutná.
Když budu chtít vyměnit GD za ImageMagick, tak jsem úplně v pohodě – okolní kód si toho ani nevšimne.
Mám pocit, že hledáš složitost tam, kde není, a dopouštíš se předčasné nikoliv optimalizace, ale dekompozice.
Jo a samozřejmě pořád nechápu, kde vidíš místo pro getResource() a setResource().
Jan Tichý:
Jakube, vycházíme prostě každý z jiných premis a očekávání. A docházíme proto každý k jiným závěrům.
Tohle, co jsi teď napsal, je důvod, proč si o Tobě spousta lidí myslí, že jsi bastlíř. Já naprosto rozumím tomu, jak to myslíš a proč to takhle používáš, z hlediska momentální rychlosti vývoje a někdy i výpočetní efektivity to samozřejmě dává velký smysl. A z tohoto pohledu je mi to velice sympatické.
Jenom mi to prostě nejde přes srst. Odchytávání přes ob_start, patchování tříd, které by šly reusovat, pro každou aplikaci jinak, to mi prostě subjektivně přijde jako zvěrstvo. Ty zase zjevně moc nemusíš injektování a pokud můžeš, tak se mu vyhneš - pro mě je naopak injektování a skládání symbolem toho nejčistšího možného přístupu, který je v kombinaci s DI kontejnerem navíc i téměř bez práce.
Tvůj přístup bude naprosto bezvadně fungovat, pokud budeš psát jenom jednu aplikaci, u které jde navíc třeba dost o výkon. Myslím ale, že narazíš, když budeš ten svůj Image a věci okolo použít ve sto různých menších nebo středních aplikacích, které budeš chtít dlouhodobě hromadně udržovat a mezi kterými budou plynule přecházet různí programátoři. Tuhle patch, támhle ad hoc obálka, čert aby se v tom vyznal, zorientoval a pochopil...
Jakub Vrána :
Nechápu, jak jsi dospěl k názoru, že se kloním k jakémusi patchování tříd pro každou aplikaci jinak. Já přitom dělám zásadně pravý opak – všude používám standardní Nette, NotORM a Adminer Editor. A o množství centralizovaného kódu, který nemusí každá aplikace psát znovu, si jiné přístupy mohou nechat jenom zdát.
Např. množství kódu, který musím napsat s NotORM je ve srovnání s Doctrine jaký – asi tak desetinový? To je toho výborná ukázka: pro Doctrine musím psát „patche“, které sice nejsou přímo v knihovně (jsou v aplikaci), ale bez nich to prostě nefunguje.
Adminer Editor je ještě názornější příklad – stáhnu aplikaci a ona prostě funguje. Nemusím patchovat vůbec nic. Jediná logika, kterou musím napsat, je ryze specifická pro aplikaci (např. do každé aplikace bude jiný login a heslo). Když chci nějakou nadstavbovou funkčnost, použiju plugin. Když plugin neexistuje, tak ho napíšu. Když vyjde nová verze, tak ji prostě vyměním. Všelijaké generovače administračních rozhraní si o takovéhle centralizaci kódu můžou nechat jenom zdát.
Teď k tomu ob_start() – v PHP jde o zcela standardní mechanismus, bez kterého se často neobejdeš. Nebo víš snad o jiném způsobu, jak z knihovny GD dostat řetězec s JPEGem? Jedinou alternativou je pokud vím dočasný soubor, což je řádově horší řešení.
Teď k té obálce – jak jsem psal, byla by to asi první zkratka, kterou bych si do třídy napsal. Možná jsi úplně nepochopil, co jsem tím myslel. Takže tohle:
<?php
class Image {
// ...
static function fromString($data) {
return new static("data://;base64," . base64_encode($data));
}
function toString() {
ob_start();
$this->save(null);
return ob_get_clean();
}
}
?>
Tuhle zkratku bych napsal nejpozději v okamžiku, když bych podobnou obálku chtěl použít podruhé – já totiž nesnáším programování metodou copy-paste, takže i když mám sklony k psaní lineárního kódu, tak mi nedělá sebemenší potíže společnou funkčnost vyčlenit do samostatné metody (jako v tomto případě) nebo i třídy. V tomto konkrétním případě bych nejspíš zkratku vytvořil hned při jejím prvním použití, protože v ní jde znovupoužitelnost snadno rozpoznat. Ale s největší pravděpodobností bych ji nenapsal ještě před tímto prvním použitím, protože programování „do foroty“ je další věc, kterou nesnáším.
Doufám, že je teď zcela zřejmé, že třída Image může být použita kdekoliv, že rozhodně nemusí mít každá aplikace svoji vlastní nevyměnitelnou verzi. A že to je zároveň způsob, kterým velmi rychle vzniká velmi snadno použitelný kód, který zároveň rychle běží.
Jan Tichý:
Jakube, asi si stojím na vedení, ale já to Tvoje řešení fakt nechápu. Respektive nedovedu si z těch všech dílčích útržků představit a poskládat, jak to vlastně celé myslíš a jak bys to celé http://www.phpguru.cz/clanky/jak-na-praci-s-obrazky konkrétně řešil. Co by přesně kde ve kterém skriptu a třídě bylo, co by se kde v jednotlivých případech a variantách aplikace měnilo. Opravdu mi nejde o to se tu někde veřejně špičkovat, čí řešení je lepší nebo horší nebo kde se dá kdo na čem nachytat. Prostě bych chtěl klidně jen čistě pro sebe vidět, jak bys to celé opravdu konkrétně napsal. Nebudu vůbec vnucovat žádné své postupy nebo názory. Jenom mě prostě zajímá Tvé úplné řešení - nějaký výchozí úplný stav a bod po bodu, jak bys z něj dělal ty jednotlivé změn a úpravy a proč. Napíšeš to? Nebo třeba místo toho klidně někdy zajít do hospody a ukázat si to před sebou?
Jakub Vrána :
V tomto vláknu jsem pochopitelně nic z tvého článku neřešil, řešil jsem pouze diskusi v tomto vláknu. Tvůj článek do tohoto vlákna prosím netahejme, byl by to zbytečný zmatek. Vyřešíme ho bokem – ať už v novém článku nebo v té hospodě.
Trochu mě mrzí, že zcela vyšuměla moje původní otázka z tohoto vlákna – jak obhájíš prosakující abstrakci v podobě metod getResource a setResource? Nikoliv v mém řešení, kterého se pořád chceš dopídit, ale ve svém vlastním řešení.
Jan Tichý:
Ještě k tomu "ob_start() – v PHP jde o zcela standardní mechanismus, bez kterého se často neobejdeš" - to uznávám, že je pravda, GD knihovna je tak špatně udělaná, že u ní se tomu člověk opravdu nevyhne. Takže tady beru svou výhradu zpět.
Jinak k tomu getResource/setResource - v mém aktuálním řešení žádná prosakující implementace není, protože u mě je třída Image objektovou obálkou nad obrázkem v původním formátu, například PNG. Takže mám metody typu getResource a setResource (sic se jmenují jinak), kde se předávají data toho obrázku jako taková, v původním formátu. Výhoda je tohle obecnější rozhraní třídy nevázané na soubory a to, že se nemusí při každém načítání a ukládání obrázku provádět převod do interní GD reprezentace a zpátky. Nevýhoda je, že tenhle převod se pak děje při každé jedné externí operaci s obrázkem, což ve většině případů moc nevadí, občas to ale může být dost špatně - třeba opakovaná ztrátová komprese apod.
Jan Tichý:
:)) Každopádně tak nebo tak neplatí Tvoje výtka "Kvůli dvojnásobné serializaci do formátu PNG a načtení z něj bude kód zbytečně pomalý", od které se to celé odvinulo, nebo ano?
KarelI:
Nesledoval jsem podrobne celou diskuzi, ale mam pocit ze nedorozumeni vznika uz z toho, ze neni jasne, jakeho projektu je trida Image (a predtim Mail, atd.) soucasti. Z teoretickeho hlediska ma byt samozrejme vsude mraky trid a rozhrani, maximalisticky navrh, ktery mysli na 20 let dopredu. Ale v praxi tohle nema smysl a vzdy zalezi na zamereni projektu.
Jinak bude vypadat Image kdyz budu chtit zobrazit ikonku v chatu a jinak kdyz budu psat knihovnu na manipulaci s obrazky. V prvnim pripade to bude jednoucelna trida, ktera asi bude michat vic veci dohromady, proste proto, ze to tak staci, pristich par let to nikomu vadit nebude a nikdo to nezaplati. Pokud cokoli z toho neplati, tak by se to melo prepsat a navrhnout lepe.
Naith:
Přístup typu Jan Tichý je přesně tím důvodem proč i na obyčejný textový editor potřebujeme dvou jádrový CPU a 4GB RAM a jednoduchá aplikace na webu je ukrutně pomalá a potřebuje tuny balastního kódu.
Přece co kdybych potřeboval udělat z Notepadu Word...!
Radek Miček:
V některých programovacích jazycích je možné
1) použít multimetody a odstranit tak rozhodování do jaké třídy metoda patří
2) a místo DI lze (do určité míry) použít dynamic scoping.
Franta:
1) Tohle jsou dost nízkoúrovňové věci a už byly mnohokrát vyřešeny. Možná by bylo zajímavější udělat srovnání, jak se s tímhle úkolem vyrovnaly standardní knihovny v různých jazycích, než se za každou cenu snažit vymyslet něco vlastního.
2) Několikrát jsem tu viděl metody pro uložení do souboru, tak snad jen malá rada: daleko univerzálnější je výstup do proudu dat (bajtů, případně znaků u textových dat).
Pavel Král:
Taky by se u zmensovani nemelo zapomenout na to ze u obrazku neni vzdy stejna velikost x a y a pocitat pokazde pomer neni moc zabavne navic v pripade galerii je treba aby byla stejna i vyska aby se daly nahledy dobre prezentovat a take treba rovnout pripojit prefix (nahled_)ve svem frameworku sem na to myslel a uziti vypada asi takto.
ExtImage:: resizeImage($max_width,$max_height,$img_url,$prefix);
Kirara:
Hmm, ale není to úplně ono. Myslím, že by měli zapracovat na scénáři vracení výsledku. Result by mělo být abstraktní, máme tu přeci SumResult, ProductResult a možné další. A stejně tak z nich můžeme dostat víc než jen numericValue.
blizz:
metóda Resize by mala byť umiestnená do triedy ImageProcessing pričom ako parameter konštruktoru by mala mať triedu Image. trieda ImageProcessing by mala property Resizer kam by sa priraďoval Resizer pričom keby nebola inicializovaná tak by sa k nej priradil defaultný resizer tak ako v riešení od beneho(lazy loading).
niečo podobné som nedávno riešil u adjustačných filtrov: http://pastebin.com/FGBzgcFt trieda HQ.Pixelmap sa používa len ako úložisko pixelov s možnosťou pristupovať k jednotlivým pixelom. na aplikáciu filtrov na pixelmapu sa používa trieda HQ.Adjust
let pixelmap = HQ.Pixelmap.FromBitmap(pictureBox1.Image :?> Bitmap)
(new HQ.Adjust(pixelmap)).Saturation(255.0)
pictureBox1.Image = pixelmap.ToBitmap() :> Image
knyttl:
Podle mě se musí hledat nějaký kompromis mezi tím, co je "objektově správně" a tou úsporností kódu. Kdyby třída Image v Nette byla rozdělena do 20 tříd, protože to je "objektově správně", tak bych ji nikdy nepoužil. To ale závisí na předpokladu, že jí nikdo nebude potřebovat nějak extrémně rozšiřovat a jde hlavně o užitečnost.
Diskuse je zrušena z důvodu spamu.