OOP Toepassen
Inhoud |
Inleiding
Dit artikel staat in het teken van Object Oriented Programming (OOP). Veel artikelen die dit onderwerp behandelen nemen onrealistische en slecht toepasbare voorbeelden als grondslag. Een klassiek voorbeeld daarvan is de nietszeggende klasse ‘Voertuig’. Met dit artikel wil ik daar verandering in brengen, met realistische en bruikbare voorbeelden. Dit artikel is vooral bedoeld voor programmeurs die bekend zijn met OOP, maar het nog niet volledig kunnen uitbuiten. Er wordt vanuit gegaan dat de lezer enige kennis heeft van OOP. Voor meer informatie kun je [1] raadplegen. We zullen gebruik maken van PHP5.
OOP vs procedure
Voor beginnende programmeurs is de stap naar OOP een moeilijke stap. De meerwaarde van object oriëntatie wordt niet begrepen en men ziet classes slechts als een ‘collectie van functies’. Meeste voorbeelden kunnen ook gemakkelijk naar PP (Procedure Programming) vorm geschreven worden, waardoor de meerwaarde van OOP niet gezien wordt. Bovendien vergt OOP een totaal andere denkwijze dan PP. Bij PP werk je van event tot event: ‘als dit gebeurt, dan doe ik dat en als dat gebeurt doe ik dit’. OOP daarentegen vergt meer overzicht: ‘Welke objecten ga ik nodig hebben, wat zijn de eigenschappen van het object en wat moet het object kunnen?’
Het meest zichtbare verschil is hoe er met de code wordt omgesprongen. Functies worden gebruikt in een veelvoud aan bestanden en daardoor ontstaan er afhankelijkheden. Dat komt de schaalbaarheid van je applicatie niet ten goede. Object oriëntatie zorgt juist voor zo min mogelijk afhankelijkheden, omdat alle verantwoordelijkheden plaatsvinden binnen objecten.
Static
Vanaf PHP5 heb je static properties en methods. Je kunt deze gebruiken zonder een instantie van een object.
class validateInput { // Setting voor debugging doeleinde static public $debug = FALSE; public static function onlyCharacters($str) { return(preg_match("/^[a-z]+$/i", $str)); } public static function onlyDigits($digits) { return(preg_match("/^[0-9]+$/", $digits)); } }
Alle functies van de class validateInput zijn static. Je kunt deze functies nu aanroepen zonder een instantie van het object.
if (validateInput::onlyCharacters('abcde')) { // geldige input } else { if (validateInput::$debug === TRUE) { // debuggen! } }
Static methods werken niet met een instantie van een object. Daarom kun je ook niet in je static method $this gebruiken. Er is immers geen object waarmee je kan werken. PHP zal ook niet tijdelijk een object voor je aanmaken. Een static method aanroepen gaat dus net zo te werk als normale functies. Static methods en members roep je dus aan met :: (Paamayim Nekudotayim) niet met ->.
Je kunt dus geen $this gebruiken en -> notatie kan ook niet. Om de methods en members te gebruiken binnen een class gebruik je self.
class validateInput { // Setting voor debugging doeleinde static public $debug = FALSE; public static function onlyCharacters($str) { if (self::$debug) { // Doe iets extra's } return(preg_match("/^[a-z]+$/i", $str)); } public static function onlyDigits($digits) { return(preg_match("/^[0-9]+$/", $digits)); } // Alias voor onlyDigits() public static function onlyNumbers($input) { return self::onlyDigits($input); } }
Static properties zijn overal in je scripts bereikbaar. Je hoeft dus niet de hele tijd instanties van objecten door te geven aan andere objecten omdat je een bepaalde waarde wilt weten. Het is ook eleganter dan een global.
Het volgende voorbeeld illustreert dit.
class Database { private static $resource = NULL; public function __construct($parameters) { if (self::$resource == NULL) { self::$resource = $this->connectDB($parameters); } else { // Hergebruik verbinding self::$resource } } } $db = new Database($db_credits); // Voer wat queries uit // Er wordt GEEN nieuwe verbinding gemaakt, omdat self::$resource // al een resource bevat. $db2 = new Database($db_credits);
Interfaces en Abstracte classes
Interfaces zijn een raamwerk voor classes. Het kan alleen functionaliteit defineren, maar nooit implementeren. Ze lijken op classes, maar interfaces bevatten alleen functie prototypes.
Laten we een interface maken voor een webshop. Een interface die we gebruiken is voor producten.
interface Product { // functie prototypes public function getName(); public function getDescription(); public function getPrice(); }
Elke class die een interface implementeert moet alle functie prototypes defineren. Is dat niet het geval, dan krijgen we een fatal error. Onze webshop heeft verschillende producten die aan deze interface voldoen. In dit voorbeeld implementeert de class Book de interface.
class Book implements Product { public function getName() { return 'Naam boek'; } public function getDescription() { return 'Beschrijving boek'; } public function getPrice() { return 'Prijs van het boek'; } }
Zo kunnen we dit ook toepassen voor andere producten, zoals CD's en DVD's.
De relaties vind je terug in het UML diagram.
Je kunt gewoon een instantie maken de class Book. Er kan geen instantie gemaakt worden van Product, dit is een interface en bevat dus geen werkende code. Wat is dan precies het nut van interfaces? Dit heeft te maken met type hinting. Met type hinting forceer je dat een parameter aan een method van een bepaalde class is. Het volgende voorbeeld illustreert dit.
class Summary { public function getSummary(Product $product) { // geef samenvatting van product $summary = 'Titel: '. $product->getName() ."\n"; $summary .= 'Beschrijving: '. $product->getDescription() ."\n"; $summary .= 'Prijs: '. $product->getPrice() ."\n"; return $summary; } }
De parameter aan de method getSummary() moet van type Product zijn. Bij incorrecte types krijg je een fatal error. De method getSummary() weet zeker welke functionaliteit de parameter heeft, namelijk de methods getName(), getDescription() en getPrice(). Ongeacht het product een boek, CD of DVD is. Deze methods staan immers gedefineerd in de interface.
$boek = new Book(); $dvd = new DVD(); $love = new Love(); // niet een product // We gaan er vanuit dat de objecten nu gegevens bevatten // zoals de naam en prijs voor de producten $summary = new Summary(); // Geef samenvattingen weer van beide producten print $summary->getSummary($boek); print $summary->getSummary($dvd); print $summary->getSummary($love); // fatal error
Type hinting en interfaces gaan hand in hand. Interfaces hebben weinig nut wanneer je geen type hinting gebruikt.
Een variant op interface is de abstracte class. Deze defineert ook prototype methods, maar kan ze ook implementeren.
abstract class SaveProduct { protected $products = array(); public function addProduct(Product $product) { $this->products[] = $product; } abstract public function save(); }
Met een abstracte method verzeker je dat de child classes deze methode hebben geimplementeerd. Is dat niet het geval, dan krijg je een fatal error. De child class erft gegevens van de parent (overerving). De implementatie kan de properties van lezen van de parent, dus $this->products kan gelezen worden.
Laten we een implementatie in elkaar zetten.
class XmlSaveProduct extends SaveProduct { public function save() { foreach ($this->products as $product) { // sla gegevens van $product op in een XML bestand } } } class DatabaseSaveProduct extends SaveProduct { public function save() { foreach($this->product as $product) { // sla gegevens van $product op in een database } } }
We hebben hier twee implementaties, een die gegevens opslaat in een XML bestand en de ander slaat gegevens op in een database.
Interfaces/abstract classes zijn belangrijk voor hergebruik en overerving. Dit bestaat niet in procedure programming. De child classes kunnen nieuwe functionaliteit toevoegen en bestaande functionaliteit wijzigen. Stel dat we het SaveProduct zonder een object orienterend aanpak hadden gedaan. We moeten de gegevens kunnen opslaan in XML formaat of in een database kunnen opslaan. We kunnen een functie saveProduct() aanmaken en daar lange switch maken om te controleren welke opslag methode we moeten gebruiken.Zoals je misschien al vooruit denkt, dit lever slechte code op en veel afhankelijkheden. het is niet schaalbaar en zeker niet vatbaar voor hergebruik.
Met een OOP aanpak, hoeven we alleen een subclass te schrijven. De afhankelijkheden zijn een stuk minder, wat het hergebruik ten goede komt. Je kunt nu gemakkelijk een andere opslag methode toevoegen door alleen een subclass te schrijven. Hoe pas je dit precies toe? Met polymorphism! Dit wordt uitgelegd in het volgende hoofdstuk.
Polymorphism
Polymorphism is een moeilijk woord voor een simpel concept: Achter een interface hebben subclasses dezelfde functies, elk met een eigen implementatie. Hierbij spelen interfaces en abstract classes een belangrijke rol, zoals je in het vorige hoofdstuk zag.
We hebben een project waarbij we configuratie bestanden uitlezen en wegschrijven. Dit kan in verschillende formaten, namelijk in text en XML formaat. We hebben twee functies:
- readConfig()
- writeConfig()
Als je procedure code zou hebben, zou dit ongeveer zo uit kunnen zien.
/** * Haal configuratie op en stop ze in een array. * @return array */ function readConfig($file) { $extension = substr($file, -4); if ($extension == '.txt') { // lees config en stop de data in een array } else { // geen text, dus xml // lees data en return array } } /** * Schrijf en save configuratie. * @param array Config array * @return boolean */ function writeConfig($data, $file) { $extension = substr($file, -4); if ($extension == '.txt') { // schrijf config weg naar $file } else { // geen text, dus xml // schrijf data in xml formaat weg } }
Wat we zien is dat in beide functies gecontroleerd wordt of het inputbestand een tekstbestand is. Stel nu dat we in de toekomst een andere databron willen gebruiken, zoals een database of een bestand in .INI formaat. In dat geval zullen de twee beschreven functies aangepast en uitgebreid moeten worden. We moeten dus zoeken naar de plek waar we die functies geïmplementeerd hebben. Eenmaal gevonden rest ons de aanpassing zelf: een niet al te elegante (misschien ook heel lange) switch-case of if-else constructie, om nog maar niet te spreken over de verschillende manieren van lezen en schrijven die elk formaat met zich meebrengt.
Laten we het probleem met de configuratie bestand oplossen met object orienterende code.
// Dit is onze interface abstract class ConfigHandler { /** * Configuration filename */ private $file; public function __construct($filename) { $this->file = $filename; } /** * Deze twee functies moeten gedefineerd worden door * de subclasses. */ abstract public function readConfig(); abstract public function writeConfig($parameters); /** * Deze functie geeft de correcte object terug waarmee gewerkt wordt. */ static public function getInstance($configfile) { $extension = substr($configfile, -4); if ($extension == '.txt') { return new TxtConfigHandler($configfile); } else { return new XmlConfigHandler($configfile); } } }
We gebruiken een abstract class en geen interface class. De reden is dat je bij interface classes geen functies mag aanmaken die iets doen, je mag alleen prototype functies maken. Bij abstract classes is dat niet het geval.
We maken twee abstracte functies aan. De subclasses moeten deze twee functies implementeren, anders krijgen we een fatal error. Daarnaast hebben we de functie getInstance($configfile) die een configuratie bestand als parameter heeft. Hier gebeurt het polymorphism. Aan de hand van de extensie, geven we het correcte object terug. De objecten hebben zeker twee functies gemeen, namelijk de functies readConfig() en writeConfig(). Daarnaast behoren ze tot dezelfde interface, maar ieder subclass heeft haar eigen implementatie. Laten we de code afmaken.
class TxtConfigHandler extends ConfigHandler { public function readConfig() { // Lees data met $this->file als bestandsnaam. } public function writeConfig($data) { // Schrijf data weg in text formaat. } } class XmlConfigHandler extends ConfigHandler { public function readConfig() { // Parse XML // Lees data met $this->file als bestandsnaam. } public function writeConfig($data) { // Schrijf data weg in XML formaat. } }
Het volgende UML diagram laat de relaties zien.
Beide classes hebben dezelfde functies, maar ieder met een eigen implementatie. Laten we de client code maken.
// We weten niet welke implementatie we krijgen $config = ConfigHandler::getInstance($configfile); // We weten niet welke implementatie we gebruiken $config_data = $config->readConfig(); $settings = array('password' => 'abracadabra'); // We weten niet welke implementatie we gebruiken $config->writeConfig($settings);
Doordat we de verantwoording code centraal hebben gemaakt, is het schaalbaarder geworden. De functie getInstance() geeft de correcte object terug aan de hand van de extensie. Mochten we later een ander formaat willen toevoegen, dan hoeven we maar op een plek wijzigingen door te voeren. Daarnaast moet je nog een class aanmaken met een eigen implementatie van readConfig() en writeConfig().
Een goed voorbeeld van wat we tot nu toe hebben behandeld is PEAR::DB, dit is een database abstraction layer (DAL). Een DAL verbergt je onderliggende database. Je code is dan portable omdat je gemakkelijk van database software kunt wijzigen, mits is correcte SQL gebruikt. De structuur van PEAR::DB ziet er ongeveer zo uit.
Aan de hand van de DSN (Data Name Source) geeft DB::connect() de correcte implementatie terug van DB_Common (interface).
$dsn = 'mysql://tri:secret@localhost/db_name'; // In dit geval zal DB::connect() de implementatie van MySQL teruggeven. $db = DB::connect($dsn);
Bij een ander project zou je PostgreSQL kunnen gebruiken. Het enige wat je hoeft te veranderen is de DSN. Door het goede ontwerp van PEAR::DB kun je gemakkelijk uitbreiden en het is herbruikbaar. Dat is de kracht van OOP!
Conclusie
Enkele triviale voorbeelden hebben laten zien hoe je een web applicatie met object oriented programming aanpakt. De static keyword is behandeld, daarnaast ook interfaces en polymorphism.
Het vergt veel programmeren om de technieken te kunnen toepassen. Heb je het eenmaal onder de knie, dan worden je toekomstige web applicaties steeds beter ontwikkeld, herbruikbaar en onderhoudbaar.
De volgende stap is design patterns aanleren en toepassen. In de toekomst zal ik ook een artikel daarover schrijven.
Veel succes!
Zie ook: Object Oriented Programming
Auteur
Tri Pham is een freelance web developer, met kennis van verschillende programmeertalen. Hij heeft interesses in computer- en netwerkbeveiliging, cryptografie en Semantic Web.






