Od špagety k objektům (2) - OOP
Objektově orientované programování přináší mnoho výhod, které nemusí být pro začínajícího programátora na první pohled zřejmé a tento článek by měl posloužit jako jejich shrnutí a vysvětlení. V neposlední řadě by mohl být odrazovým můstkem pro ty, kteří nenašli odpověď v mnoha knižních teoretických příkladech.
1. Úvod
Aby bylo možné rozdělit práci na projektu mezi více programátorů a aby se stal kód přenositelný a použitelný napříč aplikacemi, přichází na řadu takzvané objektově orientované programování (OOP) se svými návrhovými vzory a frameworky.
Pokud byla dodržena pravidla popsaná v předchozím článku, tedy že kód je rozdělen na šablony, funkční části a na části pracující s úložištěm, bude přechod na objektově orientované programování opravdu snadný. V podstatě se jen funkce které mají společné pole působnosti (například práce s nákupním košíkem v eshopu) zapouzdří do tříd. Instancím tříd se pak říká objekty a funkcím v těchto třídách se říká metody. Zapouzdření do tříd však přináší nové možnosti.
2. Zapouzření do tříd
Jedním z významů zapouzdření do tříd je sloučení blízkých funkcí do jednoho logického celku. Typickým příkladem může být nákupní košík. Taková třída bude sdružovat funkce pro přidání a odebrání položky košíku, manipulaci s jejich počtem, nebo třeba funkci pro získání celkové ceny.
Nákupní košík si však lze představit i v reálném světě a proto jej mnohdy používají různé knihy jako příklad. Obtížnější je však pochopení práce s třídami právě v situaci, kdy objekt nemá svůj předobraz v reálném světě. Jako příklad může posloužit například jednoduchá funkce pro ukládání logovacích zpráv. Této funkci předáme zprávu a ta ji uloží na disk. V procedurálním programování by se taková funkce zpasala následujícím způsobem.
<?php // common.php function saveLog($message) { file_put_contents('error.log', $message."\n", FILE_APPEND); } ?>
Její použití by vypadalo následovně.
<?php // index.php require('common.php'); saveLog('Some important message to log.'); ?>
Zde je vhodné upozornit na význam značky ?>
umístěné na konci souboru. Z inline PHP lze snadno odvodit, že se jedná o znak pro ukočení práce PHP procesoru a pokračování zpracování běžným způsobem. V případě OOP by celý kód měl být řízen právě procesorem PHP a jeho neustálé vypínání a okamžité zapínání by bylo nežádoucí a proto lze tedy tuto značku ?>
zcela vynechat.
Zapsání předchozího příkladu logovací funkce formou OOP zápisu by mohlo vypadat následujícím způsobem.
<?php // LoggerMessage.php class LoggerMessage { function save($message) { file_put_contents('error.log', $message."\n", FILE_APPEND); } }
Použití takového zápisu by vypadalo následovně.
<?php // index.php require('LoggerMessage.php'); $loggerMessage = new LoggerMessage(); $loggerMessage->save('Some important message to log.');
Na první pohled se může OOP zápis jevit jako rozsáhlejší a tedy horší, ale v následujících částech článku budou dokumentovány právě jeho výhody a možnosti. Příklad tedy slouží jako nutný základ potřebný pro další pochopení problematiky.
Kód souboru LoggerMessage.php lze chápat jako deklaraci třídy s názvem LoggerMessage s jednou metodou s názvem save(), která má jeden vstupní parametr s názvem $message. Zde je vhodné upozornit na doporučení pojmenování souborů stejným jménem jako jméno třidy a na doporučení právě jedné třídy na soubor.
Kód souboru index.php lze chápat jako vytvoření nové instance třídy LoggerMessage, která je reprezentována objektem. Tento objekt je následně uchován v proměnné $loggerMessage. Na tomto objektu je následně zavolána jeho metoda save(), které je předán textový řetězec jako hodnota vstupního parametru $message.
Z předchozího popisu je patrné, že funkce zůstala stejná, ale terminologie se změnila. Je opravdu důležité si tuto terminologii zažít a používat, jelikož se jedná o všeobecně používané názvosloví napříč všemi profesionálními vývojáři ve všech typech objektově orientovaných programovacích jazyků.
V tento moment je také vhodné upozornit na doporučení pojmenování tříd způsobem zvaným "CamelCase", tedy že víceslovný název se zapisuje tak, že první písmena všech slov začínají velkým písmenem a mezera mezi nimi se nepíše. Oproti tomu názvy metod se zapisují způsobem zvaným "pascalCase", který se od předchozího způsobu liší tím, že první písmeno prvního slova je malé.
3. Jmenný prostor
Jmenný prostor slouží pro udržení přehlednosti v kódu, jelikož umožňuje použití více různých tříd se stejným názvem v různém jmenném prostoru. Lze tedy například vytvořit více různých tříd se jménem Message s rozdílným účelem definovaným právě jmenným prostorem.
Deklarace jmenného prostoru je určena zápisem namespace
následovaným pojmenováním jmenného prostoru. Tento kód je umístěn hned na začátku zdrojového kódu, jak dokumentuje následující příklad.
<?php //Message.php namespace Logger; class Message { function save($message) { file_put_contents('error.log', $message."\n", FILE_APPEND); } }
Použití takového zápisu by vypadalo následovně.
<?php // index.php require('./Logger/Message.php'); $loggerMessage = new \Logger\Message(); $loggerMessage->save('Some important message to log.');
Jak je z předchozích ukázek patrné, namespace umožňuje organizovat strukturu tříd podobně jako adresářovou strukturu. Rovněž je doporučeno pro snazší orientaci udržovat tuto adresářovou strukturu identickou se jmenným prostorem.
Jmenný prostor lze libovolně hluboko zanořit. Existuje například doporučení použít jako výchozí jmenný prostor jméno společnosti nebo vývojáře a do něj teprve umístit zbývající kód. Toto doporučení výchází z předpokladu, že v průběhu vývoje přijde potřeba použití kódu třetích stran, které mohou obsahovat své implementace často používaných věcí, jako například právě Logger. Proto by se předchozí příklad mohl upravit následovně.
<?php //Message.php namespace Zeleznypa\Logger; class Message { function save($message) { file_put_contents('error.log', $message."\n", FILE_APPEND); } }
Použití takového zápisu by vypadalo následovně.
<?php // index.php require('./Zeleznypa/Logger/Message.php'); $loggerMessage = new \Zeleznypa\Logger\Message(); $loggerMessage->save('Some important message to log.');
4. Výjimky
V předchozích příkladech se očekává, že soubor "error.log" existuje a že má pro něj uživatel oprávnění zápisu. V OOP ke zpracování těchto výjimečných situací slouží právě výjimky. V případě že se systém dostane do takové situace, může programátor takzvaně „vyhodit výjimku“, kterou lze navíc v jiné části kódu „odchytit“ a zpracovat, jak dokumentuje následující kód.
<?php // exceptionExample.php try { if(date("d.m.") === "24.12.") { echo "Veselé vánoce"; } else { throw new Exception('Unexpected time execution.'); } } catch(Exception $exception) { echo 'Běh skriptu skončil chybou ('. $exception->getMessage() .'). Tento kód se má spouštět pouze o vánocích.'; }
Výhoda tohoto přístupu je, že se může zpracování výjimečného stavu přenést z místa vzniku na jedno místo, kde se následně může chyba standardně zpracovat například zapsáním do logovacího souboru a následného vypsání omluvné zprávy na obrazovku. Další práce s výjimkami bude popsána v dalších částech tohoto článku.
5. Vnitřní stav objektu
Instance třídy může nabývat stavů tím, že se naplní její takzvané stavové proměnné. Tyto proměnné se deklarují na začátku definice třídy. Lze k nim přistupovat pomocí metod zvaných „Gettery a Settery“ nebo přímo. Výhodou přístupu přes „Gettery a Settery“ je možnost dodatečně kontrolovat obsah stavových proměnných a jemněji tak řídit stav objektu.
Předchozí příklad lze upravit tak, že bude možné určit umístění souboru, do kterého budou ukládány logovací zprávy. Zároveň by mohlo být vhodné zkontrolovat, že tento soubor existuje a že má do něj uživatel oprávnění zápisu.
V procedurálním programování by se takový kód dal zapsat následujícím způsobem.
<?php // common.php function saveLog($message,$logFile) { if(file_exists($logFile) === TRUE) { if(is_writeable($logFile) === TRUE) { return file_put_contents($logFile, $message."\n", FILE_APPEND) !== FALSE; } else { echo "Insufficient permission to access log file $logFile."; return FALSE; } } else { echo "Log file does not exist and have to be created before."; return FALSE; } } ?>
Jeho použití by bylo následovné.
<?php // index.php require('common.php'); saveLog('Some important message to log.','error.log'); ?>
Takovýmto přístupem však funkce narůstá do obrovských rozměrů a vyznat se v jejím významu je stále náročnější. Ve větších aplikacích by se kód začal opakovat a v konečném důsledku stal natolik nepřehledným, že by většina práce s ním obnášela zkoumání významu a funkce kódu. Naštěstí OOP přístup přináší řešení této situace, jak dokumentuje následující kód.
<?php //Message.php namespace Zeleznypa\Logger; class Message { var $logFile = ''; function save($message) { return file_put_contents($this->getLogFile(), $message."\n", FILE_APPEND) !== FALSE; } function getLogFile() { if($this->logFile === '') { throw new \RuntimeException('Log file have to be set before use.'); } return $this->logFile; } function setLogFile($logFile) { if(file_exists($logFile) !== TRUE) { throw new \RuntimeException('Log file does not exist and have to be created before.'); } if(is_writeable($logFile) !== TRUE) { throw new \RuntimeException('Insufficient permission to access log file $logFile.'); } $this->logFile = $logFile; } }
Použití takového zápisu by vypadalo následovně.
<?php // index.php require('./Zeleznypa/Logger/Message.php'); $loggerMessage = new \Zeleznypa\Logger\Message(); $loggerMessage->setLogFile('error.log'); $loggerMessage->save('Some important message to log.');
6. Objekt jako kontejner pro data
Nebylo by to právě OOP, kdyby pro vzájemnou komunikaci a výměnu dat nepoužívalo objekty. V některých silně typových programovacích jazicích (Java, Ruby, …) je každá hodnota reprezentovaná objektem. Programovací jazyk PHP není takto dogmatický, ale jekmile metoda vrací nějaká data se složitější strukturou, je vhodné se zamyslet nad tím, je-li důvod, aby nebyla předávána jako objekt.
Objektům, které slouží pouze jako nosič dat se říká "Value object", nebo "POJO object". Dle serveru Wikipedia se zkratka POJO vysvětluje jako "Plain old java object", což pro jazyk PHP poněkud nedává smysl. Existuje logicky odvozené označení POPO object, ale jeho užití se v komunitě jazyka PHP neujalo.
Smyslem použití Value objektů na rozdíl například od polí je ten, že objekt určité třídy má jasně deklarovanou strukturu, co smí a nesmí obsahovat. Pokud tedy metoda přijme jako hodnotu parametru objekt určité třídy, není potřeba validovat, zda obsahuje vyžadované parametry. Pokud má třída navíc ošetřené vstupy formou „setterů“, aby umožnili uložit hodnoty pouze očekávaného datového typu, je možné se v metodách pracujících s objektem této třídy spolehnout i na formální validitu přijímaných dat. Lze rovněž ošetřit i přístup k vlastnostem objektu formou „getterů“, a při pokusu o získání dat z vlastnosti, která nebyla definována při vytváření instance, vyhodit výjimku o neplatném stavu objektu.
Je možné se setkat i s dalšími termíny, které označují třídu obsahující pouze vlastnosti třídy a případné „gettery a settery”. Tyto termíny označují účel takové třídy. Často se vyskytující termín je DTO, který slouží pro standardizování přenášených dat mezi rozhraními aplikací.
Lze tedy říci, že existují dva typy tříd. Jeden typ slouží jako přepravka pro data a druhý slouží pro samotnou práci. Kdyby například předchozí příklad měl umožnit uložit nejen samotnou zprávu, ale i typ zprávy, zapsalo by se to v procedurálním programování následovně.
<?php // common.php function saveLog($message, $logFile, $type = "ERROR") { if(file_exists($logFile) === TRUE) { if(is_writeable($logFile) === TRUE) { return file_put_contents($logFile, '['.$type.'] '.$message."\n", FILE_APPEND) !== FALSE; } else { echo "Insufficient permission to access log file $logFile."; return FALSE; } } else { echo "Log file does not exist and have to be created before."; return FALSE; } } ?>
Jeho použití by bylo následovné.
<?php // index.php require('common.php'); saveLog('Some not so important message to log.','application.log','INFO'); ?>
Z kódu je patrné, že se vstupní funkce při každém dalším požadavku rozrůstá o další parametr. Tento přístup však vede zákonitě k tomu, že bude občas nutné deklarovat i parametry funkce, které není třeba oproti výchozímu nastavení měnit. A to jen proto, že bude potřeba změnit hodnotu parametru, který byl deklarován později.
Tento neduh lze obejít tím, že paramtery které spolu logicky souvisí se spojí do jednoho parametru typu pole. V procedurálním programování by se to zapsalo následovně.
<?php // common.php function saveLog($messageData, $logFile) { $message = is_array($messageData) ? $messageData['text'] : $messageData; $type = (is_array($messageData) && isset($messageData['type'])) ? $messageData['type'] : 'ERROR'; if(file_exists($logFile) === TRUE) { if(is_writeable($logFile) === TRUE) { return file_put_contents($logFile, '['.$type.'] '.$message."\n", FILE_APPEND) !== FALSE; } else { echo "Insufficient permission to access log file $logFile."; return FALSE; } } else { echo "Log file does not exist and have to be created before."; return FALSE; } } ?>
Jeho použití by bylo následovné.
<?php // index.php require('common.php'); $messageData = array( 'text' => 'Some not so important message to log.', 'type' => 'INFO', ); saveLog($messageData,'error.log'); ?>
Takovýto přístup umožňuje volitelně určit typ zprávy, aniž by bylo nutné deklarovat výchozí hodnoty případných předchozích parametrů. Jenže by bylo vhodné takovouto funkci rozšířit ještě o kontrolu existence a formální validity dat uložených v poli. Přidat kontrolu do stejné funkce by znepřehlednilo kód a proto by zkušenější programátor přistoupil k oddělení kontroly do samostatné funkce. Jeden z možných způsobů implementace by vypadal následovně.
<?php // common.php function validateMessageData($messageData) { $output = array( 'text' => NULL, 'type' => 'ERROR', ); if(is_array($messageData) === TRUE) { if(isset($messageData['text']) === TRUE) { if(is_scalar($messageData['text']) === TRUE) { $output['text'] = $messageData['text']; } else { throw new Exception('Message text have to be scalar value.'); } if(isset($messageData['type']) === TRUE) { if(is_string($messageData['type']) === TRUE) { $output['type'] = $messageData['type']; } else { throw new Exception('Message type have to be string.'); } } } else { throw new Exception('MessageData array did not contain "text" key.'); } } elseif(is_scalar($messageData) === TRUE) { $output['text'] = $messageData; } else { throw new Exception('MessageData have to be array or scalar value.'); } return $output; } function saveLog($messageData, $logFile) { $messageData = validateMessageData($messageData); if(file_exists($logFile) === TRUE) { if(is_writeable($logFile) === TRUE) { return file_put_contents($logFile, '[' . $messageData['type'] . '] ' . $messageData['text'] . "\n", FILE_APPEND) !== FALSE; } else { echo "Insufficient permission to access log file $logFile."; return FALSE; } } else { echo "Log file does not exist and have to be created before."; return FALSE; } } ?>
Jeho použití by bylo následovné.
<?php // index.php require('common.php'); $messageData = array( 'text' => 'Some not so important message to log.', 'type' => 'INFO', ); saveLog($messageData,'application.log'); ?>
Pokud by se totéž mělo zapsat v OOP, postupovalo by se následujícím způsobem. Nejprve by se vytvořila třída, která by reprezentovala samotnou zprávu logu a její parametry. Ta by vypadala následovně.
<?php //Message.php namespace Zeleznypa\Logger; class Message { var $text; var $type = 'INFO'; function getText() { if($this->text === NULL) { throw new \RuntimeException('Message text is not defined yet.'); } return $this->text; } function setText($text) { if(is_scalar($text) !== TRUE) { throw new \BadMethodCallException('Message text have to be scalar value.'); } $this->text = $text; } function getType() { return $this->type; } function setType($type) { if(is_string($type) !== TRUE) { throw new \BadMethodCallException('Message type have to be string.'); } $this->type = $type; } }
Následně by se vytvořila služba, která by tuto zprávu uměla uložit a její kód by vypadal následovně.
<?php //LoggerService.php namespace Zeleznypa\Logger; class LoggerService { var $logFile = ''; function save(\Zeleznypa\Logger\Message $message) { return file_put_contents($this->getLogFile(), '[' . $message->getType() . '] ' . $message->getText() . "\n", FILE_APPEND) !== FALSE; } function getLogFile() { if($this->logFile === '') { throw new \RuntimeException('Log file have to be set before use.'); } return $this->logFile; } function setLogFile($logFile) { if(file_exists($logFile) !== TRUE) { throw new \RuntimeException('Log file does not exist and have to be created before.'); } if(is_writeable($logFile) !== TRUE) { throw new \RuntimeException('Insufficient permission to access log file $logFile.'); } $this->logFile = $logFile; } }
Použití takového zápisu by vypadalo následovně.
<?php // index.php require('./Zeleznypa/Logger/Message.php'); require('./Zeleznypa/Logger/LoggerService.php'); $loggerMessage = new \Zeleznypa\Logger\Message(); $loggerMessage->setText('Some not so important message to log.'); $loggerMessage->setType('INFO'); $loggerService = new \Zeleznypa\Logger\LoggerService(); $loggerService->setLogFile('error.log'); $loggerService->save($loggerMessage);
7. Závislosti třídy
Třídy mohou pro svoji funkčnost vyžadovat další třídy nebo dodatečnou konfiguraci. K předání závislosti třídy se obvykle používá speciální metoda známá jako „Constructor“, která se automaticky zavolá při vytváření nové instance třídy a která slouží právě k výchozímu nastavení objektu. V případě jazyka PHP5 se takková metoda jmenuje __construct()
. V dřívějších verzích jazyka PHP i ve většině ostatních jazyků se metoda constructoru pojmenovává stejně jako název třídy včetně „CamelCase” zápisu. Tento způsob zůstal přítomen z důvodu zpětné kompatibility i v aktuálních verzích jazyka PHP, není však doporučený.
Závislost třídy lze chápat tak, že nemá smysl, aby vznikla instance třídy, bez toho aby se něco nastavilo. V případě „value object“ by takovou závislostí mohly být povinné parametry. V případě služeb by se jako závislost dali chápat dílčí služby potřebné pro její správnou funkčnost.
V předchozím příkladu služba LoggerService při zavolání metody save uložila zprávu na pevný disk. Pro vývojové prostředí nebo pro málo navštěvované produkční prostředí by taková služba byla dostačující, ale pro opravdu velké projekty by mohla být přítěží a bylo by vhodné zprávy ukládat například do databáze, nebo skrze webovou službu. Mnohé webové služby jsou však placené, nebo jsou z bezpečnostních důvodů přístupné pouze z produkční sítě. V takový moment vyvstává potřeba mít možnost konfigurovat, jakým způsobem se bude ukládání řešit.
V úvodním článku seriálu bylo napsáno doporučení oddělit aplikační funkce od funkcí, které přímo implementují základní operace s úložištěm avšak toto doporučení nebylo v předchozím příkladu tohoto článku dodrženo. Aby byla logovací služba dobře rozšiřitelná a konfigurovatelná a byla tak připravená i na růst aplikace, bude vhodné ji rozdělit na dvě části. Jedna část, která se v odborné terminologii nazývá „Data mapper“, bude sloužit pro konečnou práci s úložištěm a druhá část, která se v odborné terminologii nazývá „Service“, bude umožňovat vnější nastavení způsobu ukládání a bude sloužit pro zbytek aplikace jako dosavadní výše popisovaná logovací služba. Podoba popisované části „Data Mapper” by mohla být implementována následujícím způsobem.
<?php //FileDataMapper.php namespace Zeleznypa\Logger; class FileDataMapper { var $logFile = ''; function __construct($logFile) { $this->setLogFile($logFile); } function save(\Zeleznypa\Logger\Message $message) { return file_put_contents($this->getLogFile(), '[' . $message->getType() . '] ' . $message->getText() . "\n", FILE_APPEND) !== FALSE; } function getLogFile() { if($this->logFile === '') { throw new \RuntimeException('Log file have to be set before use.'); } return $this->logFile; } function setLogFile($logFile) { if(file_exists($logFile) !== TRUE) { throw new \RuntimeException('Log file does not exist and have to be created before.'); } if(is_writeable($logFile) !== TRUE) { throw new \RuntimeException('Insufficient permission to access log file $logFile.'); } $this->logFile = $logFile; } }
Implementace výše popisované servisní části by pak mohla být následující.
<?php //LoggerService.php namespace Zeleznypa\Logger; class LoggerService { var $dataMapper; function __construct(\Zeleznypa\Logger\FileDataMapper $dataMapper) { $this->setDataMapper($dataMapper); } function save(\Zeleznypa\Logger\Message $message) { return $this->getDataMapper()->save($message); } function getDataMapper() { if($this->dataMapper === NULL) { throw new \RuntimeException('DataMapper is not set yet.'); } return $this->dataMapper; } function setDataMapper(\Zeleznypa\Logger\FileDataMapper $dataMapper) { return $this->dataMapper; } }
Zároveň by mohlo být vhodné stejným způsobem vylepšit i třídu reprezentující zprávu logu, aby nebylo možné vytvořit její instanci bez vyplnění povinných údajů. Toto by se dalo zapsat násldujícím způsobem.
<?php //Message.php namespace Zeleznypa\Logger; class Message { var $text; var $type = 'INFO'; function __construct($text) { $this->setText($text); } function getText() { if($this->text === NULL) { throw new \RuntimeException('Message text is not defined yet.'); } return $text; } function setText($text) { if(is_scalar($text) !== TRUE) { throw new \BadMethodCallException('Message text have to be scalar value.'); } $this->text = $text; } function getType() { return $this->type; } function setType($type) { if(is_string($type) !== TRUE) { throw new \BadMethodCallException('Message type have to be string.'); } $this->type = $type; } }
Použití takto upraveného kódu by vypadalo následovně.
<?php // index.php require('./Zeleznypa/Logger/Message.php'); require('./Zeleznypa/Logger/FileDataMapper.php'); require('./Zeleznypa/Logger/LoggerService.php'); $loggerMessage = new \Zeleznypa\Logger\Message('Some not so important message to log.'); $loggerMessage->setType('INFO'); $loggerDataMapper = new \Zeleznypa\Logger\FileDataMapper('error.log'); $loggerService = new \Zeleznypa\Logger\LoggerService($loggerDataMapper); $loggerService->save($loggerMessage);
8. Obecné rozhraní a implementace
Pozornější čtenář si v předchozím příkladu jistě všiml, že služba LoggerService vyžaduje pro svoji práci přímo instanci třídy FileDataMapper, což přesně neodpovídá požadované konfigurovatelnosti. Bylo by jistě vhodnější mít možnost určit, jak má vypadat obecný logovací „Data Mapper“ a následně kontrolovat, že předávaná závislost splňuje tento předpis. K tomuto slouží v jazyce PHP technologie zvaná „Interface“ a její zápis vypadá následovně.
<?php //IDataMapper.php namespace Zeleznypa\Logger; class IDataMapper { function save(\Zeleznypa\Logger\Message $message); }
Takovýmto předpisem se říká, že DataMapper má mít metodu save, která přijímá logovací zprávu, ale co s touto zprávou udělá a jakým způsobem je úplně jedno. Může tedy existovat „Data Mapper“, který uloží logovací zprávu na disk nebo jiný, který ji uloží do databáze. Třída která splňuje zadané rozhraní může obsahovat i další metody, které v interface zapsány nejsou. Dokonce může třída spňovat více rozhraní najednou. Vzhledem k efektivitě zpracování kódu je však nutné explicitně říct, že třída dané rozhraní splňuje. Výhodu tohoto zápisu je rovněž fakt, že při spuštění tohoto kódu se provede kontrola, že daná třída rozhraní doopravdy splňuje a pokud tomu tak není, běh skončí výjimkou obsahující informaci o nedostatku. Stačí tedy upravit dosavadí „Data Mapper“ následujícím způsobem.
<?php //FileDataMapper.php namespace Zeleznypa\Logger; class FileDataMapper implements \Zeleznypa\Logger\IDataMapper { var $logFile = ''; function __construct($logFile) { $this->setLogFile($logFile); } function save(\Zeleznypa\Logger\Message $message) { return file_put_contents($this->getLogFile(), '[' . $message->getType() . '] ' . $message->getText() . "\n", FILE_APPEND) !== FALSE; } function getLogFile() { if($logFile === '') { throw new \RuntimeException('Log file have to be set before use.'); } return $this->logFile; } function setLogFile($logFile) { if(file_exist($logFile) !== TRUE) { throw new \RuntimeException('Log file does not exist and have to be created before.'); } if(is_writeable($logFile) !== TRUE) { throw new \RuntimeException('Insufficient permission to access log file $logFile.'); } $this->logFile = $logFile; } }
Stejným způsobem, jakým se zapíše že metoda očekává jako hodnotu vstupního parametru objekt určité třídy, je možné také zapsat i možnost, že metoda očekává jako hodnotu objekt jakékoliv třídy, která splňuje zapsané rozhraní. Takováto úprava logovací služby se zapíše následujícím způsobem.
<?php //LoggerService.php namespace Zeleznypa\Logger; class LoggerService { var $dataMapper; function __construct(\Zeleznypa\Logger\IDataMapper $dataMapper) { $this->setDataMapper($dataMapper); } function save(\Zeleznypa\Logger\Message $message) { return $this->getDataMapper()->save($message); } function getDataMapper() { if($this->dataMapper === NULL) { throw new \RuntimeException('DataMapper is not set yet.'); } return $this->dataMapper; } function setDataMapper(\Zeleznypa\Logger\IDataMapper $dataMapper) { $this->dataMapper = $dataMapper; } }
Použití takto upraveného kódu zůstává stejné a vypadalo by tedy následovně.
<?php // index.php require('./Zeleznypa/Logger/Message.php'); require('./Zeleznypa/Logger/FileDataMapper.php'); require('./Zeleznypa/Logger/LoggerService.php'); $loggerMessage = new \Zeleznypa\Logger\Message('Some not so important message to log.'); $loggerMessage->setType('INFO'); $loggerDataMapper = new \Zeleznypa\Logger\FileDataMapper('error.log'); $loggerService = new \Zeleznypa\Logger\LoggerService($loggerDataMapper); $loggerService->save($loggerMessage);
9. Přetěžování
Do tohoto momentu se mohlo OOP jevit jen jako komplikovanější zápis procedurálního kódu. Jedním ze smyslů, proč se vůbec používá je možnost opakovaného použití a modularita kódu. V předchozím příkladu bylo napsáno, že je nutné explicitně zapsat, že třída dané rozhraní splňuje. Pokud by se pro logování měla použít komponenta třetí strany, bylo by tedy nutné upravit v ní tuto část kódu. Tím by se však ztratila možnost v budoucnu použít novější verzi této komponenty, respektive by bylo nutné při každém nasazení nové verze zjistit rozdíly oproti originálu a tyto opakovaně použít na novou verzi. Takto by ale modularita a znovupoužitelnost vypadat neměla. Zde přichází na řadu technologie, které se říká přetěžování.
Vytvoří se tedy třída, která bude obalovat komponentu třetí strany a která bude obsahovat informaci o implementaci rozhraní a ve zbytku aplikace se bude pracovat s touto nově vytvořenou třídou. Originální třída se tak může libovolně měnit a aktualizovat a při tom nebude nutné zapracovávat do ní potřebné změny. Pokud by se tedy měla pro logování použít například třída třetí strany \ThirdParty\Logger
, vytvořil by se následující obal.
<?php //ThirdPartyLogger.php namespace Zeleznypa\Logger; class ThirdpartyLogger extends \ThirdParty\Logger implements \Zeleznypa\Logger\IDataMapper { }
Tímto zápisem se říká, že nově vzniknutá třída \Zeleznypa\Logger\ThirdpartyLogger
obaluje původní třídu \ThirdParty\Logger
a zároveň tato třída splňuje rozhraní \Zeleznypa\Logger\IDataMapper
. Použití by pak s využitím kódu z předchozího příkladu vypadalo následovně.
<?php // index.php require('./libs/ThirdParty/Logger.php'); require('./Zeleznypa/Logger/Message.php'); require('./Zeleznypa/Logger/ThirdPartyLogger.php'); require('./Zeleznypa/Logger/LoggerService.php'); $loggerMessage = new \Zeleznypa\Logger\Message('Some not so important message to log.'); $loggerMessage->setType('INFO'); $loggerDataMapper = new \Zeleznypa\Logger\ThirdpartyLogger(); $loggerService = new \Zeleznypa\Logger\LoggerService($loggerDataMapper); $loggerService->save($loggerMessage);
Tím však schopnosti přetěžování nekončí. Opravdová síla přetěžování je v možnosti přiohnout jednotlivé metody přetěžované třídy. Logovací služba z p;předchozího příkladu očekává objekt třídy \Zeleznypa\Logger\Message
, ale těžko lze očekávat, že třída třetí strany bude očekávat totéž. Je velice pravděpodobné, že bude dokonce očekávat instanci vlatní třídy. Logovací služba z předchozího příkladu umí pracovat například s typem zprávy. Pokud by se však měla použít služba, která takovéto chování neumožňuje, mohlo by být požadováno, aby se typ uložil jako počátek zprávy. Takového chování by bylo možné dosáhnout následujícím způsobem.
<?php //ThirdPartyLogger.php namespace Zeleznypa\Logger; class ThirdPartyLogger extends \ThirdParty\Logger implements \Zeleznypa\Logger\IDataMapper { public function save(\Zeleznypa\Logger\Message $message) { parent::save('[' . $message->getType() . '] - '. $message->getText()); } }
Tímto způsobem se přetížila původní metoda save novou implementací, ale zároveň se při tom využila díky přístupu přes deklaraci parent::metoda()
jelikož se přetěžovaná třída stává předkem přetěžující třídy. Samozřejmě je také možné deklarovat metodu, která pro svou funkci využije zcela jinak pojmenovanou metodu nebo i více metod předka a rozšířit tak možnosti originální třídy, aniž by ji bylo nutné upravit.
Velice častým důvodem pro přetížení je potřeba rozšíření třídy o nové funkce. Zde je asi největší kámen úrazu správného objektového návrhu. Přetížení má sloužit pro specializaci obecné třídy. Jako příklad může posloužit obecná logovací služba oproti logovací službě se schopností volat události. Obecná třída může, ale nemusí dané očekávání splňovat a tím je obecnou. Specializovaná třída oproti tomu očekávání splnit musí a tím je konkrétnější.
Nejčastějším příkladem specializace obecné třídy je vytváření specifičtějších výjimek, které lépe dokumentují nastalou chybu, jak dokumentuje následující ukázka.
<?php //InvalidStateException.php namespace Zeleznypa\Exceptions; class InvalidStateException extends \RuntimeException { }
Častou chybou v návrhu je obrácený přístup, neboli zobecňování specializované služby. Jako příklad může posloužit snaha rozšířit třídu pro práci s firemním rozharaním o schopnost práce s rozhraním další aplikace. Tedy ze specializované firemní vrstvy se stává vrstva universální. Ačkoliv se to ze začátku může jevit jako praktická a jednoduchá cesta k cíli, z dlouhodobého hlediska je chybná. Jelikož objekty, které se předávají jiným třídám by měli být chápány jako závislost třídy. Při návrhu objektového modelu je proto potřeba se zamyslet nad otázkou, zda-li všechny třídy vyžadující pro svoji práci možnost komunikace s firemním rozhraním budou rovněž vždy potřebovat pracovat s rozhraním další aplikace. Může se stát, že takové chování je přesto potřeba. Pro tyto případy je však vhodnější vytvořit nadřazenou zobecňující třídu, která bude mít závislost na obou rozhraních a jejíž instance bude následně využívána v kódu stejně jako obecná logovací služba v předchozích příkladech.
10. Veřejné rozhraní třídy
Moderní vývojové nástroje a profesionálí IDE umožňují programátorům napovídat dostupné metody na aktuálním objektu. Metody objektu a jeho stavové proměnné, které jsou přístupné vně tohoto objektu se nazývají veřejným rozhraním objektu. Některé metody nebo stavové proměnné by však neměli být dostupné odkudkoliv. V&předchozích příkladech je příklad třídy \Zeleznypa\Logger\FileDataMapper
na jejímž objektu se mimo tento objekt volá pouze metoda save(). Ostatní metody a stavové proměnné slouží pouze internímu účelu a nemá tak smysl, aby je vývojová prostředí navrhovala programátorovi k užití. V některých případech je to dokonce nežádoucí, jako například přístup ke stavové proměnné bez použití setteru, který validuje její hodnotu. Pro tento účel disponuje OOP možností řízení přístupu ke stavovým proměnným a metodám deklarací jednoho z typů přístupu před deklarací. Dostupné možnosti jsou public
, protected
a final
. V případě stavových proměnných třídy se tímto slovem nahrazuje původní označení var
, které funguje stejně jako public
. Označení var
by se nemělo používat a v následujících verzích jazyka PHP je označeno jako „deprecated“.
Metody a stavové proměnné, které jsou označeny jako public
jsou dostupné odkudkoliv a je tedy možné je využít nebo v případě stavových proměnných i změnit jejich hodnotu. Metody nebo stavové proměnné označené jako protected
je možné použít unitř třídy, ale nejsou dostupné a tedy ani změnitelné z vnějšku, což se hodí právě pro případ interních metod nebo stavových proměnných pro které existuje řízení přístupu přes getter nebo setter. Rozdíl mezi oprávněními protected
a private
je v možnosti práce s metodou nebo stavovou proměnou uvnitř třídy. To co je označeno jako private
není možné využít nebo změnit ani v předcích nebo potomcích dané třídy.
Kdy použít protected
a kdy použít private
závisí na osobním přístupu k celkové bezpečnosti kódu a na firmní politice. Někdy je potřeba opravdu časté přetěžování, kde je používání private
přítěží. Jinde je zase dbáno na důslednou bezpečnost a umožnění přetížit metodu nebo hodnotu stavové proměnné musí projít schvalovacím procesem. Druhý přístup má navíc tu výhodu, že zároveň dává najevo, že daná metoda je, nebo se alespoň očekává že může být někde přetížena.
Zároveň je také možné zabránit přetížení celé třídy. K tomu slouží deklarace final
na začátku definice třídy, jak dokumentuje následující kód.
<?php //LoggerService.php namespace Zeleznypa\Logger; final class LoggerService { private $dataMapper; public function __construct(\Zeleznypa\Logger\IDataMapper $dataMapper) { $this->setDataMapper($dataMapper); } public function save(\Zeleznypa\Logger\Message $message) { return $this->getDataMapper()->save($message); } protected function getDataMapper() { if($this->dataMapper === NULL) { throw new \RuntimeException('DataMapper is not set yet.'); } return $this->dataMapper; } private function setDataMapper(\Zeleznypa\Logger\IDataMapper $dataMapper) { $this->dataMapper = $dataMapper; } }
V některých případech může být přetížení vyžádáno. Tedy že třída obsahuje určitou logiku, ale není přímo využitelná. Takového přístupu využívá například FacebookSDK, který vyžaduje přetížením určit, jakým způsobem bude řešena persistence stavových proměnných. Za tímto účelem existuje deklarace abstract
uvedená na začátku definice třídy, jak dokumentuje následující kód.
<?php //FileDataMapper.php namespace Zeleznypa\Logger; abstract class UniversalFileDataMapper implements \Zeleznypa\Logger\IDataMapper { private $logFile = ''; public function __construct($logFile) { $this->setLogFile($logFile); } public function save(\Zeleznypa\Logger\Message $message); public function getLogFile() { if($logFile === '') { throw new \RuntimeException('Log file have to be set before use.'); } return $this->logFile; } public function setLogFile($logFile) { if(file_exist($logFile) !== TRUE) { throw new \RuntimeException('Log file does not exist and have to be created before.'); } if(is_writeable($logFile) !== TRUE) { throw new \RuntimeException('Insufficient permission to access log file $logFile.'); } $this->logFile = $logFile; } }
Jak je z kódu patrné, metoda save nebyla v abstraktní třídě implementována a je tedy vyžadováno, aby byla v reálné implementaci přetížena a doimplementována. Tímto způsobem je možné vytvořit například práci se souborem na lokálním file systému nebo přes síťové spojení na vzdáleném smb systému. Přesto všechny implementace budou vždy disponovat metodami pro nastavení a přístup k nastavenému názvu souboru. Tento přístup je vhodné používat s opravdovou rozvahou.
11. Reference na objekt
Jakmile se do proměnné nastavuje hodnota, alokuje se místo v paměti kam se nastavovaná hodnota uloží a do proměnné se uloží takzvaný „pointer“ neboli adresa na toto místo v paměti. Když se však do proměnné nastavuje hodnota z jinné proměnné nebo metody, nemusí být vždy žádoucí, aby se alokovala další paměť, do které se zkopíruje něco, co už je uložené jinde. Popisovanou situaci dokumentuje následující příklad.
<?php $users['admins']['foo'] = 'Foo'; $admins = $users['admins']; $admins['foo'] = 'Bar'; echo $users['admins']['foo']; // 'Foo'
Na tomto příkladu je vidět, že proměnná $admins
pracuje se zcela jiným polem než proměnná $users
a tedy se zcela jiným prostorem v paměti. Jsou však případy, kdy je toto chování nežádoucí a kdy se hodí pracovat opravdu se stejnými daty aniž by se v paměti neustále kopírovali. K tomu slouží technologie zvaná „reference“, která namísto vytváření nové alokace v paměti přiřadí do proměnné ukazatel neboli „pointer“ na již alokované místo. Ukázku práce s referencí dokumentuje následující příklad.
<?php $users['admins']['foo'] = 'Foo'; $admins &= $users['admins']; $admins['foo'] = 'Bar'; echo $users['admins']['foo']; // 'Bar'
Objekty obvykle obsahují složitější a tedy i větší datové struktury jejichž opakované rozkopírovávání by bylo opravdu nehospodárné. Proto se při práci s objekty předává ukazatel, pokud není explicitně určeno, že se má vytvořit kopie neboli klon. Toho se využívá v různých ORM systémech, kde se při změně hodnoty stavové proměnné objektu označí objekt jako změněný („Dirty“) a při následném zavolání ukládací funkce se díky tomuto označení ví, které objekty se ukládat nemusí. Ukázku práce s objekty a referencemi dokumentuje následující kód.
<?php $user = (object) array('name' => 'Foo'); $bar = $user; $foo = clone $user; $bar->name = 'Bar'; echo $bar->name; // 'Bar' echo $user->name; // 'Bar' echo $foo->name; // 'Foo'
Detailnější vysvětlení problematiky referencí je možné nalézt v oficiální dokumentaci k PHP nebo na blogu Jakuba Vrány.
12. Dokumentace pomocí PhpDoc
Kód lze navíc opatřit dokumentací, která může pomoci ostatním kolegům pochopit danou problematiku (například vysvětlení použitého workaroundu). Názory na obsáhlost dokumentace se různí od dogmatického dokumentování každé třídy a metody až po opačný extrém v podobě tvrzení, že kód má být samopopisný. Správná dokumentace zdrojového kódu má svoji podobu deklarovanou standardem phpDoc. Pokud je kód dobře zdokumentován, lze použít různých generátorů, které umí na základě tohoto zápisu vygenerovat HTML dokumentaci. Mezi nejznámější generátory patří phpDocumentor nebo ApiGen. Ukázka zdokumentovaného kódu je v následujícím příkladu.
<?php //LoggerService.php namespace Zeleznypa\Logger; /** * Service for saving log message * @author Pavel Železný <[email protected]> */ final class LoggerService { /** @var \Zeleznypa\Logger\IDataMapper $dataMapper */ private $dataMapper; /** * Constructor * @author Pavel Železný <[email protected]> * @param \Zeleznypa\Logger\IDataMapper $dataMapper */ * @return void */ public function __construct(\Zeleznypa\Logger\IDataMapper $dataMapper) { $this->setDataMapper($dataMapper); } /** * Handle saving log message * @author Pavel Železný <[email protected]> * @param \Zeleznypa\Logger\Message $message * @return bool Is saving success? */ public function save(\Zeleznypa\Logger\Message $message) { return $this->getDataMapper()->save($message); } /** * DataMapper getter * @author Pavel Železný <[email protected]> * @return \Zeleznypa\Logger\IDataMapper $dataMapper * @throws RuntimeException */ protected function getDataMapper() { if($this->dataMapper === NULL) { throw new \RuntimeException('DataMapper is not set yet.'); } return $this->dataMapper; } /** * DataMapper setter * @author Pavel Železný <[email protected]> * @param \Zeleznypa\Logger\IDataMapper $dataMapper * @return void */ private function setDataMapper(\Zeleznypa\Logger\IDataMapper $dataMapper) { return $this->dataMapper; } }
Dalším z důvodů pro použití dokumentace formou phpDoc je integrace ve vývojových prostředích, které umějí číst data z dokumentace a následně napovídat dostupné stavové proměnné a metody objektu. V neposlední řadě některé frameworky využívají takzvaných „anotací“, neboli částí dokumentace odpovídající jednomu řádku začínajícímu na znak @, pro usnadnění práce s kódem. Příkladem tak může být persistování stavové proměnné v Nette frameworku.