Rubrika Programming Philosophy

Jak zvládnout gettery, když nemají co vrátit?

Vývoj softwaru často přináší dilema. Například jak řešit situace, kdy getter nemá co vrátit. V tomto článku prozkoumáme tři strategie pro implementaci getterů v PHP, které ovlivňují strukturu a čitelnost kódu, a každá má své specifické výhody i nevýhody. Pojďme se na ně podrobněji podívat.

Univerzální getter s parametrem

Prvním a v Nette používaným řešením je vytvoření jediné getter metody, která, pokud hodnota není dostupná, může dle potřeby vrátit buď null nebo vyhodit výjimku. O chování rozhoduje volitelný parametr. Zde je příklad, jak by mohla metoda vypadat:

public function getFoo(bool $need = true): ?Foo { if (!$this->foo && $need) { throw new Exception("Foo not available"); } return $this->foo; } 

Hlavní výhodou tohoto přístupu je, že eliminuje potřebu mít několik verzí getteru pro různé scénáře použití. Někdejší nevýhodou byla horší srozumitelnost uživatelského kódu používajícího booleovské parametry, ale ta padla s příchodem pojmenovaných parametrů, kdy lze psát getFoo(need: false).

Dále tento přístup může způsobit komplikace v oblasti statické analýzy, jelikož dle signatury se zdá, že getFoo() může vrátit null v každé situaci. Nicméně nástroje jako PHPStan umožňují explicitní dokumentaci chování metody pomocí speciálních anotací, které zlepšují porozumění kódu a jeho správnou analýzu:

/** @return ($need is true ? Foo : ?Foo) */ public function getFoo(bool $need = true): ?Foo { } 

Tato anotace jasně určuje, jaké návratové typy může metoda getFoo() generovat v závislosti na hodnotě parametru $need. Ale například PhpStorm jí nerozumí.

Dvojice metod: hasFoo() a getFoo()

Další možností je rozdělit zodpovědnost na dvě metody: hasFoo() pro ověření existence hodnoty a getFoo() pro její získání. Tento přístup zvyšuje přehlednost kódu a je intuitivně srozumitelný.

public function hasFoo(): bool { return (bool) $this->foo; } public function getFoo(): Foo { return $this->foo ?? throw new Exception("Foo not available"); 

Hlavním problémem je redundance, zvláště v případech, kdy je kontrola dostupnosti hodnoty sama o sobě náročným procesem. Pokud hasFoo() provádí složité operace k ověření, zda je hodnota dostupná, a tato hodnota je poté opět získávána pomocí getFoo(), dojde k jejich opětovnému provedení. Hypoteticky může být stav objektu nebo dat změněn mezi voláním hasFoo() a getFoo(), což může vést k nesrovnalostem. Z uživatelského pohledu může být tento přístup méně pohodlný, protože nás nutí volat dvojici metod s opakujícím se parametrem. A nemůžeme využít například null-coalescing operátor.

Výhodou je, že některé nástroje pro statickou analýzu umožňují definovat pravidlo, že po úspěšném volání hasFoo() nedojde v getFoo() k vyhození výjimky.

Metody getFoo() a getFooOrNull()

Třetí strategií pro je rozdělení funkcionality na dvě metody: getFoo() pro vyhození výjimky, pokud hodnota neexistuje, a getFooOrNull() pro vrácení null. Tento přístup minimalizuje redundanci a zjednodušuje logiku.

public function getFoo(): Foo { return $this->getFooOrNull() ?? throw new Exception("Foo not available"); } public function getFooOrNull(): ?Foo { return $this->foo; } 

Alternativou je dvojice getFoo() a getFooIfExists(), ale v tomto případě nemusí být zcela intuitivní pochopit, která metoda vyhazuje výjimku a která vrací null. O trošku výstižnější by byla dvojice getFooOrThrow() a getFoo(). Další možností je getFoo() a tryGetFoo().

Každý z představených přístupů k implementaci getterů v PHP má své místo v závislosti na specifických potřebách projektu a preferencích vývojářského týmu. Při výběru vhodné strategie je důležité zvážit, jaký dopad bude mít na čitelnost, údržbu a výkon aplikace. Volba by odrážet snahu o co nejsrozumitelnější a nejefektivnější kód.

První kroky v OOP v PHP: Základy, které musíte znát

Chcete se ponořit do světa objektově orientovaného programování v PHP, ale nevíte, kde začít? Mám pro vás nového stručného průvodce OOP, který vás seznámí se všemi těmi pojmy, jako class, extends, private atd.

V průvodci se dozvíte, co je to:

Průvodce si neklade za cíl udělat z vás mistry v psaní čistého kódu nebo podat zcela vyčerpávající informace. Jeho cílem je vás rychle seznámit se základními koncepty OOP v současném PHP a dát vám fakticky správné informace. Tedy poskytnout pevný základ, na kterém můžete dále stavět. Třeba aplikace v Nette.

Jako navazující čtení doporučuji podrobný průvodce světem správného návrhu kódu. Ten je přínosný i pro všechny, co PHP a objektově orientované programování ovládají.

Tabulátory místo mezer jako projev ohleduplnosti

Určitě už jste někdy narazili debatu „tabulátory vs. mezery“ pro odsazování. Polemika probíhá od nepaměti a oba tábory vyzdvihují své argumenty:

Tabulátory:

  • odsazování je jejich účel
  • menší soubory, protože odsazení zabírá jeden znak
  • můžete si nastavit vlastní šířku odsazení (? k tomu se vrátíme)

Mezery:

  • kód bude vypadat všude stejně a konzistence je klíčová
  • vyhnete se možným problémům v prostředích citlivých na bílé znaky

Co když jde ale o víc než o osobní preference? ChaseMoskal nedávno zveřejnil na Redditu velmi podnětný příspěvek s názvem Nikdo nezmínil skutečný důvod, proč používat tabulátory místo mezer, který vám otevře oči.

Stěžejní důvod, proč používat tabulátory

Chase ve svém příspěvku popisuje zkušenost se zaváděním mezer na svém pracovišti a negativní dopady, které to mělo na spolupracovníky se zrakovým postižením.

Jeden z nich byl zvyklý používat šířku tabulátoru 1, aby se vyhnul velkým odsazením při použití obřího písma. Druhý používá šířku tabulátoru 8, protože mu nejlépe vyhovuje na ultraširokém monitoru. Pro oba však představuje kód s mezerami vážný problém, musí je převádět na tabulátory před čtením a zase zpátky na mezery před komitováním.

Pro nevidomé programátory, kteří používají braillské displeje, představuje každá mezera jednu braillskou buňkou. Pokud je tedy výchozí odsazení 4 mezery, odsazení 3. úrovně plýtvá 12 cennými braillskými buňkami ještě před začátkem kódu. Na 40buňkovém displeji, který se u notebooků používá nejčastěji, je to více než čtvrtina dostupných buněk, které jsou promrhány bez jakékoliv informace.

Nám se může přizpůsobení šířky odsazení zdát jako zbytečnost, jsou ale mezi námi programátoři, pro které je naprosto nezbytné. A to prostě nemůžeme ignorovat.

Tím, že budeme v našich projektech používat tabulátory, dáváme jim možnost tohoto přizpůsobení.

Nejdříve přístupnost, pak osobní preference

Jistě, nejde přesvědčit každého, aby se přiklonil na jednu či druhou stranu, jde-li o preference. Každý má své. A měli bychom být rádi za možnost volby.

Zároveň však musíme dbát na to, abychom zohledňovali všechny. Abychom respektovali odlišnosti a používali přístupné prostředky. Jakým je například znak tabulátor.

Myslím, že Chase to vystihl dokonale, když ve svém příspěvku uvedl, že „…neexistuje žádný protiargument, který by se jen blížil k tomu převážit potřeby přístupnosti našich spolupracovníků“.

Accessible first

Stejně jako při navrhování webů se vžila metodika „mobile first“, kdy se snažíme zajistit, aby každý, bez ohledu na zařízení, měl s vaším produktem skvělou user experience – měli bychom usilovat o „accessible first“ prostředí tím, že zajistíme, aby každý měl stejnou možnost pracovat s kódem, ať už v zaměstnání nebo na opensource projektu.

Pokud se tabulátory stanou výchozí volbou pro odsazování, odstraníme jednu bariéru. Spolupráce pak bude příjemná pro každého, bez ohledu na jeho schopnosti. Pokud budou mít všichni stejné možnosti, můžeme maximálně využít společný potenciál ❤️


Článek vychází z Default to tabs instead of spaces for an ‚accessible first‘ environment. Podobně přesvědčivý post jsem si přečetl v roce 2008 a ještě ten den změnil ve všech svých projektech mezery na tabulátory. Zůstala po tom stopa v Gitu, ale samotný článek už zmizel v propadlišti dějin.

Nejsi ve vleku cargo kultů?

Před mnoha lety jsem si uvědomil, že když v PHP ve funkci používám proměnnou obsahující předdefinovanou tabulku dat, tak při každém volání funkce musí být pole znovu „vytvořené“, což je překvapivě dost pomalé. Příklad:

function isSpecialName(string $name): bool {	$specialNames = ['foo' => 1, 'bar' => 1, 'baz' => 1, ...];	return isset($specialNames[$name]); } 

A přišel jsem na jednoduchý trik, který znovuvytváření zabránil. Stačilo proměnnou definovat jako statickou:

function isSpecialName(string $name): bool {	static $specialNames = ['foo' => 1, 'bar' => 1, 'baz' => 1, ...];	return isset($specialNames[$name]); } 

Zrychlení, pokud pole bylo trošku větší, se pohybovalo v několika řádech (jako třeba klidně 500×).

Takže od té doby jsem u konstantních polí vždy používal static. Je možné, že tento zvyk někdo následoval, a třeba ani netušil, jaký má skutečný důvod. Ale to nevím.


Před pár týdny jsem psal třídu, která nesla v několika properties velké tabulky předdefinovaných dat. Uvědomil jsem si, že to bude zpomalovat vytváření instancí, tedy že operátor new bude pokaždé „vytvářet“ pole, což jak víme je pomalé. Tudíž musím properties změnit na statické, nebo možná ještě lépe použít konstanty.

A tehdy jsem si položil otázku: Hele a nejsi jen ve vleku cargo kultu? Opravdu pořád platí, že bez static je to pomalé?

Těžko říct, PHP prošlo revolučním vývojem a staré pravdy nemusí být platné. Připravil jsem proto testovací vzorek a udělal pár měření. Samozřejmě jsem si potvrdil, že v PHP 5 použití static uvnitř funkce nebo u properties přineslo zrychlení o několik řádů. Ale pozor, v PHP 7.0 už šlo jen o jeden řád. Výborně, projev optimalizací v novém jádře, ale stále je rozdíl podstatný. Nicméně u dalších verzí PHP rozdíl dál klesal a až postupně téměř vymizel.

Dokonce jsem zjistil, že použití static uvnitř funkce v PHP 7.1 a 7.2 běh zpomalovalo. Zhruba 1,5–2×, tedy z pohledu řádů, o kterých se tu celou dobu bavíme, zcela zanedbatelně, ale byl to zajímavý paradox. Od PHP 7.3 rozdíl zmizel zcela.

Zvyklosti jsou dobrá věc, ale je nutné jejich smysl stále validovat.


Zbytečný static v těle funkcí už používat nebudu. Nicméně u oné třídy, která držela velké tabulky předdefinovaných dat v properties, jsem si řekl, že je programátorsky správné konstanty použít. Za chvíli jsem měl refaktoring hotový, ale už jak vznikal jsem naříkal nad tím, jak se kód stává ošklivým. Místo $this->ruleToNonTerminal nebo $this->actionLength se v kódu objevovalo řvoucí $this::RULE_TO_NON_TERMINAL a $this::ACTION_LENGTH a vypadalo to fakt hnusně. Zatuchlý závan ze sedmdesátých let.

Až jsem zaváhal, jestli vůbec chci koukat na tak hnusný kód, a jestli raději nezůstanu u proměnných, případně statických proměnných.

A tehdy mi to došlo: Hele nejsi jen ve vleku cargo kultu?

No jasně že jsem. Proč by měla konstanta řvát? Proč by měla na sebe upozorňovat v kódu, být vyčnívajícím elementem v toku programu? Fakt, že struktura slouží jen ke čtení, není důvod PRO ZASEKNUTÝ CAPSLOCK, AGRESIVNÍ TÓN A HORŠÍ ČITELNOST.

TRADICE VELKÝCH PÍSMEN POCHÁZÍ Z JAZYKA C, KDE SE TAKTO OZNAČOVALY MAKROKONSTANTY PREPROCESORU. BYLO UŽITEČNÉ NEPŘEHLÉDNUTELNĚ ODLIŠIT KÓD PRO PARSER OD KÓDU PRO PREPROCESOR. V PHP SE ŽÁDNÉ PREPROCESORY NIKDY NEPOUŽÍVALY, TAKŽE NENÍ ANI DŮVOD psát konstanty velkými písmeny.

Ještě ten večer jsem je všude zrušil. A stále nemohl pochopil, proč mě to nenapadlo už před dvaceti lety. Čím větší blbost, tím tužší má kořínek.

Jak souhrnně nazývat třídy a rozhraní?

Názvoslovný oříšek: jak souhrnně označovat třídy a rozhraní? Jak třeba nazvat proměnnou, která může obsahovat jak název třídy, tak rozhraní? Co zvolit místo $class?

Dá se tomu říkat type ($type), nicméně to je zase příliš obecné, protože typem je i řetězec nebo pole. Z pohledu jazyka jím může být i něco komplikovanějšího, třeba ?array. Navíc je sporné, co je v případě objektu jeho typ: je jím název třídy, nebo je to object?

Nicméně souhrnné označení pro třídy a rozhraní skutečně existuje: je jím slovo třída.

Cože?

  1. Z pohledu deklarace je interface hodně ořezaná třída. Může obsahovat jen veřejné abstraktní metody. Což také implikuje nemožnost vytvářet objekty. Rozhraní jsou tedy podmnožinou tříd. A pokud je něco podmnožinou, tak to můžeme označovat názvem nadmnožiny. Člověk je savec, stejně jako rozhraní je třída.
  2. Nicméně je tady ještě pohled užití. Třída může dědit jen od jedné třídy, ale může implementovat vícero rozhraní. Nicméně tohle je omezení týkající se tříd, samotné rozhraní za to nemůže. Obdobně: třída nemůže dědit od final třídy, ale přitom final třídu pořád vnímáme jako třídu. A také pokud třída může implementovat víc rozhraní (tj. tříd, viz 1.), stále je vnímejme jako třídy.

A co traity? Ty sem vůbec nepatří, z hlediska OOP jednoduše neexistují.

Tedy problém se společným pojmenováním tříd a rozhraní je vyřešen. Říkejme jim prostě třídy.

classes + interfaces = classes 

No jo, ale vznikl tady problém nový. Jak říkat třídám, které nejsou rozhraní? Tedy jejich doplňku. Tomu, co se ještě na začátku článku nazývalo třídy. Nerozhraní? Nebo implementations? 🙂

To je ještě větší oříšek. To je pořádný ořech. Víte co, raději zapomeňme na to, že rozhraní jsou také třídy, a tvařme se opět, že každý OOP identifikátor je buď třída, nebo rozhraní. Bude to snazší.

Psát isomorfní webové aplikace?

Isomorfní webové aplikace jsou takové, které sdílejí kód mezi serverovou a klientskou stranou. Jinými slovy, jsou obě strany psané v JavaScriptu. I když tak to vůbec nemuselo být, historie je zajímavější.

Úplně poprvé jsem se s touto koncepcí setkal před (fíha, to je neuvěřitelné) takřka 20 lety. Vlastně JavaScript přímo vznikl jako client-side i server-side jazyk, serverové prostředí se jmenovalo Netscape LiveWire a kód vypadal nějak takto. Šlo tedy o mix HTML a JavaScriptu, jen s tím rozdílem, že skript se vykonával na serveru. JavaScript byl zamýšlený jako jazyk pro amatérské programátory, jako konkurent tehdejšího PHP a Microsoftího ASP, zatímco pro profesionály tu byla client-side a server-side Java.

Nic nedopadlo podle očekávání. Kvůli soudním sporům Java z prohlížečů zmizela, ke konci se držela už jen v porno chatech a bankovnictví, a dnes je z ní jeden velký bezpečnostní kráter distribuovaný jako adware, který je nutno v prohlížečích vypínat. Neuspěl ani JavaScript na serveru, protože byl příliš nezralý a nevhodný na takové nasazení a serverové řešení upadajícího Netscape nezískalo popularitu.

Vývoj webů na mnoho let zbrzdilo šílenství okolo specifikací začínajících na X a monopol Internet Exploreru, ale pak došlo k jejich svržení a máme tu hromadu nových technologií. A s tím se pochopitelně vrací i otázka jednoho jazyka na obou stranách. Odstartoval to zejména výkonnostně nadupaný interpret JavaScriptu z Google Chrome a platforma Node.js.

Situace je dosti jiná, než před 20 lety:

  • server-side technologie ušly obrovský kus cesty a vyzrály
  • client-side prožívá pubertu
  • v průniku jazyků je pouze JavaScript

Tvorba webů pomocí serverových frameworků se stává komoditou, na řadu složitých otázek odpovídají zažité návrhové vzory. Na straně klienta to naopak bují, dnešní novinky nejspíš brzy nahradí novinky jiné, a to se ještě několikrát zopakuje. Tenhle stav je fajn, dohání se dlouhé zpoždění a máte šanci se zapojit a odvětvím pohnout.

Dohání také JavaScript, leč jeho skutečnou pozici nejlépe charakterizuje potřeba a popularita nejrůznějších nadstaveb, ať už jde o CoffeeScript, Google Closure Compiler nebo TypeScript. Pomocí nich už dnes lze z JavaScriptu udělat něco celkem robustního, což ale ve skutečnosti stále není. Přičemž jazyky s ambicí jej nahradit existují.

Osobně mi cesta k izomorfním aplikacím připadá přirozená a správná. U klientského skriptování jsem začínal a stále hledal různé spojnice, například Nette má dosud poměrně ojedinělou vlastnost, že pravidla pro validaci formulářů zapsaná na straně serveru vám automaticky překlopí na stranu prohlížeče. Isomorfní validace formulářů od roku 2008.

Ale v žádném případě bych si isomorfně nenechal naprogramovat třeba e-shop. Zatím.

Příliš mladé prostředí znamená absenci zažitých návrhových vzorů a různá rizika. Když si Dan Steigerwald, který pro mě částečně pochopitelně odmítá jakékoliv problémy této technologie připouštět, si tuhle posteskl, že čeští vývojáři jsou pozadu za frikulíny ze San Francisca a stále se drží serverových technologií, rozjela se diskuse o výhodách a nevýhodách jednotlivých přístupů a Dan jako odpověď na jednu námitku poslal příklad webu (tuším jeho kolegů) iodine.com psaný v React.js. Čímž poskytl pěkný příklad neduhů SPA/isomorfních aplikací:

  • na webu nefunguje správně tlačítko zpět
  • na mnoha různých URL se nachází identický obsah
  • jeho výroba byla násobně dražší

Zdůrazňuji, že z jeho stany nešlo o ukázkový příklad, nicméně tím lépe demonstruje hlavní problém SPA/isomorfních aplikací: udělat je dobře je stále velmi těžké a potažmo drahé. Přičemž tentýž web za použití server-side frameworku, jako je například Nette, zvládne napsat i průměrný a levný programátor. A podobných hrubek se přitom nedopustí.

Izomorfním aplikacím se nevyhýbejte, zkoušejte si novinky, zavčasu odhalujte slepé cesty, rozšiřujte si obzory. Ale s ostrým nasazením se držte jen u typů aplikací, kde je to skutečně nutné a výhodné. Není jich zase tolik.

Navíc nemáte v žádné žhavé technologii jistotu. Tvrdit opak, třeba proto, že za nějakou z nich stojí obří firma, znamená být slepý k historii posledních 20 let.

Nevěřím statistikám, které si sám nezfalšuji

Václav Novotný připravil infografiku porovnávající aktivitu vývojářů v Nette a Symfony. Rád a zvědavě se podívám, leč bez vysvětlení metriky umí být čísla krutě zrádná. S nadsázkou: při určitém workflow a naivním měření mohu ve statistikách vyjít jako autor 100 % kódu, aniž bych naprogramoval jedinou řádku.

I při přímočarých workflow je poměřování množství komitů zákeřné. Není komit jako komit. Pokud přidáte pětici důležitých komitů a zároveň deset lidí vám opraví překlepy v komentářích, jste co do počtu komitů autorem třetiny kódu. Což ale není pravda, jste autorem celého kódu, opravy překlepů se za autorství (jak ho obvykle vnímáme) nepovažují.

V GITu dále věc komplikují „merge-commits“. Pokud někdo připraví zajímavý komit a vy ho schválíte (tedy vznikne ještě merge-commit), jste autorem poloviny komitů. Ale jaký je vlastně skutečný podíl? Obvykle nulový, schválení je otázka jednoho kliknutí v GitHubu, byť někdy diskusí strávíte víc času, než kdybyste si kód napsal sám, ale neuděláte to, protože potřebujete vývojáře vychovávat.

Proto místo počtu komitů je vhodnější analyzovat jejich obsah. Nejjednodušší je brát v úvahu počet změněných řádek. Ale i to může být zavádějící: pokud vytvoříte 100 řádkovou třídu a někdo jiný soubor s ní jen přejmenuje (či rozdělí na dva), „změnil“ vlastně 200 řádků a opět jste autorem třetiny.

Pokud týden ladíte u sebe několik komitů a až potom je pošlete do repozitáře, jste v počtu změněných řádek v nevýhodě oproti tomu, kdo je pošle hned a teprve poté dolaďuje následujícími komity. Nebylo by tedy od věci analyzovat třeba až souhrny za celý den. Je třeba odfiltrovat i údržbové komity, zejména ty, které mění u všech souborů letopočet nebo verzi v hlavičce.

Do toho přicházejí ještě situace, kdy se automatizovaně kopírují komity z jedné větve do jiné, nebo do jiného repozitáře. Což de facto znemožňuje dělat jakékoliv globální statistiky.

Analýza jednoho projektu je věda, natož ta srovnávací. Docela mi to připomíná skvělý analytický kvíz od Honzy Tichého.


Související: Jak se počítá „Hall of fame“ na nette.org

Dokumentační úchylnosti

Dobře udržovaný software má mít kvalitní API dokumentaci. Jistě. Ovšem stejným prohřeškem, jakým je absence dokumentace, je i její přebytečnost. U psaní dokumentačních komentářů je totiž potřeba, podobně jako u návrhu API nebo uživatelského rozhraní, přemýšlet.

Přičemž přemýšlením bych nenazýval proces, který se udál v hlavě vývojáře, když doplnil konstruktor tímto komentářem:

class ChildrenIterator {	/** * Constructor. * * @param array $data * @return \Zend\Ldap\Node\ChildrenIterator */	public function __construct(array $data)	{	$this->data = $data;	} 

Šest řádků, které nepřidaly ani jednu jedinou informaci. Místo toho roste

  • vizuální šum
  • duplicita informací
  • objem kódu
  • možnost chybovosti

Nesmyslnost uvedeného komentáře vám možná připadá evidentní, pak jsem rád. Občas totiž dostávám pull requesty, které se snaží podobné smetí do kódu propašovat. Někteří programátoři dokonce používají editory, které takto znečišťují kód automaticky. Au.

Nebo jiný příklad. Zkuste se zamyslet, zda vám komentář prozradil něco, co by bez něj nebylo zřejmé:

class Zend_Mail_Transport_Smtp extends Zend_Mail_Transport_Abstract {	/** * EOL character string used by transport * @var string * @access public */	public $EOL = "\n"; 

S výjimkou anotace @return lze pochybovat o přínosnosti i v tomto případě:

class Form {	/** * Adds group to the form. * @param string $caption optional caption * @param bool $setAsCurrent set this group as current * @return ControlGroup */	public function addGroup($caption = null, $setAsCurrent = true) 

Pokud používáte výmluvné názvy metod a parametrů (což byste měli), pokud ty ještě navíc mají výchozí hodnoty nebo typehinty, nedá vám tento komentář takřka nic. Buď bych ho zredukoval o informační duplicity, nebo naopak rozšířil.

Ale pozor na opačný extrém, jakým jsou romány v phpDoc:

/** * Performs operations on ACL rules * * The $operation parameter may be either OP_ADD or OP_REMOVE, depending on whether the * user wants to add or remove a rule, respectively: * * OP_ADD specifics: * * A rule is added that would allow one or more Roles access to [certain $privileges * upon] the specified Resource(s). * * OP_REMOVE specifics: * * The rule is removed only in the context of the given Roles, Resources, and privileges. * Existing rules to which the remove operation does not apply would remain in the * ACL. * * The $type parameter may be either TYPE_ALLOW or TYPE_DENY, depending on whether the * rule is intended to allow or deny permission, respectively. * * The $roles and $resources parameters may be references to, or the string identifiers for, * existing Resources/Roles, or they may be passed as arrays of these - mixing string identifiers * and objects is ok - to indicate the Resources and Roles to which the rule applies. If either * $roles or $resources is null, then the rule applies to all Roles or all Resources, respectively. * Both may be null in order to work with the default rule of the ACL. * * The $privileges parameter may be used to further specify that the rule applies only * to certain privileges upon the Resource(s) in question. This may be specified to be a single * privilege with a string, and multiple privileges may be specified as an array of strings. * * If $assert is provided, then its assert() method must return true in order for * the rule to apply. If $assert is provided with $roles, $resources, and $privileges all * equal to null, then a rule having a type of: * * TYPE_ALLOW will imply a type of TYPE_DENY, and * * TYPE_DENY will imply a type of TYPE_ALLOW * * when the rule's assertion fails. This is because the ACL needs to provide expected * behavior when an assertion upon the default ACL rule fails. * * @param string $operation * @param string $type * @param Zend_Acl_Role_Interface|string|array $roles * @param Zend_Acl_Resource_Interface|string|array $resources * @param string|array $privileges * @param Zend_Acl_Assert_Interface	$assert * @throws Zend_Acl_Exception * @uses Zend_Acl_Role_Registry::get() * @uses Zend_Acl::get() * @return Zend_Acl Provides a fluent interface */	public function setRule($operation, $type, $roles = null, $resources = null, $privileges = null,	Zend_Acl_Assert_Interface $assert = null) 

Vygenerovaná API dokumentace je pouhá referenční příručka, nikoliv kniha, kterou by si člověk četl před spaním. Litanie sem skutečně nepatří.

Asi nejoblíbenějším místem, kde se lze dokumentačně vyřádit, jsou hlavičky souborů:

<?php /** * Zend Framework * * LICENSE * * This source file is subject to the new BSD license that is bundled * with this package in the file LICENSE.txt. * It is also available through the world-wide-web at this URL: * http://framework.zend.com/license/new-bsd * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to license@zend.com so we can send you a copy immediately. * * @category Zend * @package	Zend_Db * @subpackage Adapter * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com) * @license	http://framework.zend.com/license/new-bsd New BSD License * @version	$Id: Abstract.php 25229 2013-01-18 08:17:21Z frosch $ */ 

Kolikrát se zdá, že záměrem je hlavičku natáhnout tak, aby po otevření souboru vůbec nebyl vidět kód. K čemu je 10řádková informace o licenci New BSD, obsahující klíčové zvěsti, jako že její znění najdete v souboru LICENSE.txt, že je dostupná přes world-wide-web a pokud náhodou nedisponujete moderními výstřelky, jako je tzv. webový prohlížeč, máte odeslat email na license@zend.com a oni vám ji okamžitě pošlou? Navíc v balíku zopakovaná 4400×. Schválně jsem žádost zkusil poslat, ale odpověď nepřišla 🙂

Též uvedení letopočtu v copyrightu vede k vášni dělat komity jako update copyright year to 2014, které změní všechny soubory, což komplikuje porovnávání verzí.

Je vůbec potřeba uvádět v každém souboru copyright? Z právního hlediska to potřeba není, nicméně pokud open source licence dovolují uživatelům používat části kódu s tím, že musí zachovat copyrighty, je vhodné je tam mít. Stejně tak je užitečné v každém souboru uvádět, z jakého produktu pochází, pomůže to lidem v orientaci, když na něj jednotlivě narazí. Dobrým příkladem je třeba:

/** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ 

Přemýšlejte proto prosím nad každým řádkem, jestli skutečně má pro uživatele přínos. Pokud ne, jde o smetí, které nemá v kódu co dělat.

(Prosím případné komentátory, aby článek nevnímali jako souboj frameworků, tím rozhodně není.)

Objeví Rails Dependency Injection?

Málokdo má takovou potřebu zdůrazňovat svou domnělou nadřazenost, jako právě Railisti. Abyste mě nechápali špatně, jde o dobrou marketingovou strategii. Nepříjemné je, když jí podlehnete do té míry, že zbytek světa vnímáte jen jako upachtěné kopírovače bez šance se vám někdy přiblížit. Svět takový totiž není.

Příkladem je Dependency Injection. Zatímco lidé kolem PHP nebo JavaScriptu objevili DI se zpožděním, Ruby on Rails zůstávají dosud nepolíbené. Bylo mi záhadou, proč framework s tak pokrokovou image zůstává kdesi pozadu a začal v tom pátrat. Odpověď mi dala řada zdrojů na Google nebo karmiq a zní:

Ruby je tak dobrý jazyk, že vůbec Dependency Injection nepotřebuje.

Fascinující argument, který je navíc v elitářském prostředí sebepotvrzující. A je skutečně pravdivý? Nebo jde jen o zaslepení pýchou, stejné zaslepení, jaké způsobilo nedávno přetřásané bezpečnostní díry v Rails?

Říkal jsem si, že je možné, že Ruby znám natolik málo, aby mi nějaký klíčový aspekt unikl, a že skutečně jde o jazyk, který DI nepotřebuje. Jenže primárním smyslem Dependency Injection je zřejmé předávání závislostí, aby byl kód srozumitelný a předvídatelný (a je pak i lépe testovatelný). Jenže když se podívám do dokumentace Rails na tutoriál „blog za pár minut“, vidím tam třeba:

def index @posts = Post.all end 

Tedy pro získání blogpostů používají statickou metodu Post.all, která odněkud (!) vrátí seznam článků. Z databáze? Ze souboru? Vyčaruje je? Nevím, protože se tu nepoužívá DI. Místo toho se tu vaří nějaké statické peklo. Ruby je bezesporu šikovný jazyk, ale DI nenahrazuje.

V Ruby lze za běhu přepisovat metody (Monkey patch; obdobně jako v JavaScriptu), což je forma Inversion of control (IoC), která třeba pro potřeby testů dovolí podstrčit jinou implementaci statické metody Post.all. Tohle ale nenahrazuje DI, kód to zřejmější neudělá, spíše naopak.

Mimochodem, zaujala mě i třída Post tím, že reprezentuje jak jeden článek na blogu, tak funguje jako repozitář (metoda all), což je porušení Single Responsibility Principle jako vyšité.

Jako odůvodnění, proč Ruby nepotřebují DI, se často odkazuje na článek LEGOs, Play-Doh, and Programming. Důkladně jsem ho pročetl, sledoval, jak autor párkrát zaměňuje „DI“ s „DI frameworkem“ (tedy něco jako zaměňovat „Ruby“ s „Ruby on Rails“) a nakonec zjistil, že k závěru, že Ruby Dependency Injection nepotřebují, vůbec nepřišel. Psal, že nepotřebují DI frameworky, jaké zná z Javy.

Jeden mylně interpretovaný závěr, pokud pohladí ego, dokáže zcela pobláznit obrovskou skupinu inteligentních lidí. Nakonec mýtus, že špenát obsahuje neobyčejné množství železa, se taky drží od roku 1870.

Ruby je velmi zajímavý jazyk, vyplatí se v něm jako v každém jiném používat DI a existují pro něj i DI frameworky. Rails je zajímavý framework, který zatím neobjevil DI. Až ho objeví, půjde o velké téma některé z příštích verzí.

(Po pokusu diskutovat o DI s Karmim, kterého pokládám za nejinteligentnějšího Railistu, nechávám komentáře uzavřené, omlouvám se.)

Viz také: Dependency injection není pouze o jednoduším testování

DI a property injection

Dependency Injection je zřejmé předávání závislostí, tedy že se každá třída otevřeně hlásí ke svým závislostem, místo toho, aby je někde pokoutně získávala. Co kdyby se závislosti předávaly přímo do proměnných? Proberu úskalí a výhody property injection.

Property injection má jednu podstatnou výhodu: stručnost. Srovnejte:

class Foobar {	/** @var HttpRequest */	private $httpRequest;	/** @var Router */	private $router;	function __construct(HttpRequest $httpRequest, Router $router)	{	$this->httpRequest = $httpRequest;	$this->router = $router;	} } 

versus

class Foobar {	/** @var HttpRequest @inject */	public $httpRequest;	/** @var Router @inject */	public $router; } 

Proměnné musíme definovat tak jako tak. Zpravidla u nich uvádíme i anotaci @var a příslušný datový typ. Je lákavé si ušetřit práci a místo psaní rutinního kódu konstruktoru nebo metody inject() doplnit prosté @inject. Property injection kromě minimálního režijního kódu navíc parádně řeší problém s předáváním závislostí a dědičností.

Použití anotace představuje jinou konvenci pro předání závislostí. Zdůrazňuji slovo jinou, protože ať už vyjmenujeme závislosti jakožto argumenty metody nebo anotováním, jde o ekvivalentní činnost. Čímž oponuji názoru, že použití anotace představuje závislost na kontejneru. To v žádném případě není pravda, jde jen o konvenci, koneckonců dosud o kontejnerech nepadla řeč a ukázky dávají smysl.

Stejně tak se nedívejte na anotaci @inject jako nějakou odpornou magii, kterou musíte nastudovat, abyste ji mohli používat. Žádná magie tu není. Jde o obyčejné veřejné proměnné a anotace je jen doplňující informace pro programátora, říkající, že objekt vyžaduje tyto proměnné naplnit. (Nutno dodat, že Jakub Vrána reagoval na použití anotací u private proměnných, což magie je.)

V článku o předávání závislostí jsem se používání proměnných širokým obloukem vyhnul, protože mají vážné nedostatky:

  • public proměnné nezajistí typovou kontrolu
  • public proměnné nezajistí neměnnost
  • private proměnné nelze naplnit žádnou jazykovou konstrukcí
  • private proměnné nejsou součástí veřejného API – nejde tedy o deklaraci závislosti!
  • pro protected proměnné platí nevýhody obou

Ještě bych přidal, že anotace nejsou nativní součástí jazyka PHP a jde tedy o nestandardní konvenci, oproti třeba injektáži přes konstruktor.

Poznámka: vstřikování závislostí do privátních proměnných posvětila třeba Java EE 6 a je to skutečně ee. Třída své závislosti tají (private = neveřejný) a nelze ji instancovat jinak, než kontejnerem (závislost na kontejneru). Jde zcela proti smyslu Dependency Injection, jak je popsán v perexu tohoto článku, a také proti základnímu principu OOP, zapouzdření. Označil bych to jako „Inversion of Dependency Injection.“

Pro properly property injection bychom potřebovali once-write-only veřejnou proměnnou s typovou kontrolou. Kdyby tohle PHP umělo, nic by nebránilo je používat. Jenže PHP to neumí.

Emulace inject property

PHP to neumí, ale lze to emulovat!

Emulaci zajistíme pomocí magických metod __set a __get. Jak ale dosáhnout toho, aby se k public proměnné přistupovalo skrze tyto metody? Použijeme trik: v konstruktoru ji unsetneme. Proměnná zmizí a při přístupu k ní se již použijí magické metody.

Příklad implementace ve formě základní třídy Object by mohl vypadat třeba takto:

class Object {	private $injects = array();	function __construct()	{	// následující analýza proměnných by se mohla kešovat	$rc = new ReflectionClass($this);	foreach ($rc->getProperties() as $prop) {	if ($prop->isPublic() && strpos($prop->getDocComment(), '@inject')	&& preg_match('#@var\s+(\S+)#', $prop->getDocComment(), $m)	) {	// unset property to pass control to __set() and __get()	unset($this->{$prop->getName()});	$this->injects[$prop->getName()] = array('value' => null, 'type' => $m[1]);	}	}	}	function __set($name, $value)	{	if (!isset($this->injects[$name])) {	throw new Exception("Cannot write to an undeclared property $$name.");	} elseif ($this->injects[$name]['value']) {	throw new Exception("Property $$name has already been set.");	} elseif (!$value instanceof $this->injects[$name]['type']) {	throw new Exception("Property $$name must be an instance of {$this->injects[$name]['type']}.");	} else {	$this->injects[$name]['value'] = $value;	}	}	function __get($name)	{	if (!isset($this->injects[$name])) {	throw new Exception("Cannot read an undeclared property $$name.");	}	return $this->injects[$name]['value'];	} } 

pak stačí deklarovat výše uvedenou třídu Foobar jako potomka Object a vše bude fungovat standardně podle očekávání:

class Foobar extends Object {	/** @var HttpRequest @inject */	public $httpRequest;	/** @var Router @inject */	public $router; } $fb = new Foobar; $fb->router = new Router; 

Navíc však máme zajištěnou neměnnost a typovou kontrolu:

$fb->router = new Router; // Exception: Property $router has already been set. $fb->httpRequest = new Router; // Exception: Property $httpRequest must be an instance of HttpRequest."); 

Čistá cesta nebo prasárna?

Zkusme se zamyslet nad tím, co vlastně anotace @inject představuje: hint pro programátora, že proměnnou má při vytváření objektu nastavit a že ji později nesmí měnit. Anotace @var pak nařizuje typ.

Je na programátorovi, aby dodržel kontrakt. Stejně jako v případě anotace @private v PHP 4 nebo JavaScriptu, či anotace @return v současném PHP. Jde o pravidla, u nichž se předpokládá, že je programátor dodrží, aniž to lze na úrovni interpreteru ověřit.

Třída Object rozšiřuje PHP o schopnost kontroly za běhu, usnadní tedy identifikaci chyb. Je to vychytávka navíc. Z mého pohledu tedy akceptovatelná cesta k použití property injection v PHP. Možná by se dalo uvažovat nad zařazením do Nette\Object a legitimizace této injektáže v Nette.



phpFashion © 2004, 2026 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.