woensdag 11 september 2013

Naïeve getters en setters

Snelle websites en snelle webapplicaties zijn essentieel voor een optimale user experience (UX). Eric Higgins, webmaster bij Google, adviseert daarom in objectgeoriënteerd PHP geen naïeve setters en getters te gebruiken. Op het eerste gezicht heeft Eric Higgins gelijk: PHP-klassen zonder naïeve getters en setters zijn bijna twee keer sneller! Als we echter verder kijken, blijkt er op de best practices die Google aanbeveelt nogal wat af te dingen.

Getters en setters

Bij objectgeoriënteerd programmeren (OOP) in PHP gebruiken we de term setter voor een methode die een property of eigenschap van een object instelt. Een getter is een bijbehorende methode die de eigenschap ophaalt of uitleest. Met getters en setters regelen we de operaties voor lezen en schrijven van eigenschappen in een PHP-klasse.

In het artikel ‘PHP performance tips’ adviseert Google-webmaster Eric Higgins om geen naïeve getters en setters te gebruiken. Een naïeve setters is een methode die, nogal naïef, niets anders doet dan een eigenschap instellen. Een naïeve getter is een methode die, al even naïef, niets anders doet dan een waarde retourneren.

Een voorbeeld van een PHP-klasse met naïeve setters en getters is de volgende klasse Pet voor huisdieren. De naïeve setter setName() en de naïeve getter getName() doen hier niets anders dan de eigenschap $Name met de naam van het huisdier instellen en retourneren:

class Pet
{
    private $Name;

    public function getName()
    {
        return $this->Name;
    }

    public function setName($name)
    {
        $this->Name $name;
    }
}

Voor het maken van een object met de klasse Pet, bijvoorbeeld een hond met de naam Bello, kunnen we de naïeve setter en getter als volgt gebruiken:

$hond = new Pet();
$hond->setName('Bello');
echo 'Onze hond heet: ' . $hond->getName();

We kunnen ook een ‘openbare’ of ‘publieke’ eigenschap gebruiken voor de naam van een huisdier. Deze declareren we in PHP met het sleutelwoord public. De klasse wordt hiermee eenvoudiger, want de naïeve methoden setName() en getName() zijn overbodig:

class Pet
{
    public $Name;
}

De eigenschap Pet::$Name is door de declaratie public $Name nu buiten de klasse Pet toegankelijk. De naam van onze hond Bello kunnen we nu rechtstreeks adresseren, zonder setter en getter:

$hond = new Pet();
$hond->Name 'Bello';
echo 'Onze hond heet: ' . $hond->Name;

Tot 100% sneller

Volgens Eric Higgins is het rechtstreeks adresseren van public eigenschappen “up to 100% faster”: tot 100% sneller. Dat snelheidsverschil lijkt ongeveer te kloppen. Ik heb het getest op drie verschillende webservers met andere besturingssystemen en verschillende versies van PHP.

De PHP-klasse met een naïeve getter en setter heeft 1,67 tot 2,44 keer meer tijd nodig. Dat is soms dus een snelheidsverschil van méér dan 100%. Het gemiddelde ligt echter op een factor 1,86 met een standaarddeviatie van 0,18. Dat is minder dan 100%, maar welk een significant en fors snelheidsverschil.

Daarmee lijkt het een uitgemaakte zaak. Google hecht grote waarde aan snelle websites; het artikel van Eric Higgins is dan ook gepubliceerd in de rubriek ‘Best Practices’ van het Google-project Make the Web Faster.

Toch denk ik dat er goede redenen zijn om het advies van Google niet zomaar op te volgen. Sterker nog, snelheid moet geen allesoverheersende gedachte worden, want soms zijn andere best practices beter.

Magische getters en setters

PHP kent nog een derde methode om eigenschappen in een klasse te adresseren. Je kunt de magische methoden __get() en __set() gebruiken. Een magic method of magische methode is een methode die automatisch door PHP wordt aangeroepen. De magische methoden __get() en __set() worden automatisch aangeroepen wanneer je een eigenschap adresseert die is beschermd met private of protected.

Magische methoden hebben een standaardnaam die begint met een dubbele underscore. De bekendste en meest gebruikte is de constructor van een PHP-klasse in de vorm public function __construct(). Met de magische methoden __get() en __set() kunnen we een derde variant van de klasse Pet maken:

class Pet
{
    private $Name;

    public function __get($name)
    {
        if ($name 'Name') {
            return $this->Name;
        }
    }

    public function __set($name$value)
    {
        if ($name 'Name') {
            $this->Name $value;
        }
    }
}

Deze PHP-klasse kunnen we nu hetzelfde gebruiken als de variant met public $Name:

$hond = new Pet();
$hond->Name 'Bello';
echo 'Onze hond heet: ' . $hond->Name;

Daar hebben we het eerste schoonheidsfoutje in het artikel van Eric Higgins. We moeten niet twee maar drie toegangsmethoden vergelijken. Die fout kunnen we de Google-webmaster echter wel vergeven, want de derde variant met een magische __get() en __set() is verreweg de langzaamste van de drie. Hoewel de magische methoden __get() en __set() ongeveer hetzelfde doen, presteren ze meetbaar slechter dan de naïeve methoden getName() en setName().

Van snelheid naar kwaliteit

Naïeve getters en setters zijn traag. Maar vooral een goede setter is zelden naïef. We streven niet slechts naar snelle websites, maar ook naar betrouwbare webapplicaties. En kwaliteit gaat soms ten koste van prestaties, vooral door de extra controles en operaties die in PHP nodig zijn om kwaliteit te waarborgen.

Laten we om te beginnen aannemen dat een huisdier meestal een naam heeft. Bij het maken van een object van de klasse Pet zouden we daarom meteen een naam kunnen meegeven. Hiervoor kunnen we een constructor met een optioneel argument toevoegen. Deze constructor laten we vervolgens de methode setName() aanroepen voor het instellen van de naam:

class Pet
{
    private $Name;

    public function __construct($name null)
    {
        $this->setName($name);
    }

    public function getName()
    {
        return $this->Name;
    }

    public function setName($name)
    {
        $this->Name $name;
    }
}

De setter setName() is nog steeds naïef. Maar met deze aanpassing hebben we de klasse licht verbeterd én besparen we ergens anders één PHP-expressie. We kunnen nu namelijk meteen een huisdier met een naam maken:

$hond = new Pet('Bello');
echo 'Onze hond heet: ' $hond->getName();

Een public $Name is minder flexibel en vereist altijd een extra PHP-expressie voor het instellen van de eigenschap:

$hond = new Pet();
$hond->Name 'Bello';
echo 'Onze hond heet: ' . $hond->Name;

Een goede setter is zelden naïef. We kunnen de klasse Pet verbeteren door alleen een string voor de naam te accepteren. Na het verwijderen van eventuele spaties en andere whitespace (witruimte) mag de string bovendien niet leeg zijn. Als we hiervoor controlestructuren toevoegen, is de setter setName() niet langer naïef:

class Pet
{
    private $Name;

    public function __construct($name null)
    {
        $this->setName($name);
    }

    public function getName()
    {
        return $this->Name;
    }

    public function setName($name)
    {
        // De naam moet een string zijn
        if (is_string($name)) {
            // Spaties en andere witruimte verwijderen
            $name trim($name);
            // De naam mag geen lege string zijn
            if (!empty($name)) {
                $this->Name $name;
            }
        }
    }
}

Hadden we echter het advies uit de ‘best practices’ volgens Google opgevolgd, dan zouden we nu een groot probleem hebben. Overal waar we de public eigenschap $Name van een object rechtstreeks met $object->Name gebruiken, moeten we alsnog code toevoegen om te controleren of er wel een geldige string wordt doorgegeven. Of we moeten alle code herschrijven, zodat deze alsnog de setter setName() gebruikt.

Dat is onnodig veel werk, want je moet alle applicaties die de klasse Pet gebruiken onderhanden nemen. De tijd die daaraan verloren gaat, staat in geen verhouding tot de milliseconden die we in eerste instantie hadden bespaard. Bovendien werkt het fouten in de hand: vergeet je ergens een verandering door te voeren, dan gedraagt een webapplicatie zich niet zoals het hoort.

Dat is een tweede punt van kritiek op het artikel van Eric Higgins. Een naïeve setter of een naïeve getter kun je later nog aanpassen. Naïeve methoden hoeven niet naïef te blijven. Ga je echter in eerste instantie uit van public eigenschappen, dan wordt PHP-code verbeteren veel lastiger.

We vergelijken zo appels met peren. Als je netjes objectgeoriënteerd programmeert, leg je de verantwoordelijkheid voor eigenschappen van een klasse zoveel mogelijk bij de klasse zelf. Daarvoor gebruik je getters en setters. Werk je echter met public eigenschappen, dan verplaats je die verantwoordelijkheid naar alle applicaties die een klasse gebruiken. Dat zijn twee verschillende manieren van programmeren: appels en peren.

Naïeve PHP-gebruikers

Dat we de best practices volgens Google niet zomaar opvolgen, spreekt ondertussen voor zich. Maar er is nog een goede reden om er nog eens kritisch naar te kijken: veiligheid en beveiliging.

Stel, we willen een class User gebruiken voor gebruikers of gebruikersaccounts. Voor maximale prestaties zouden we dan als volgt moeten beginnen:

class User
{
    public $Username;
    public $Password;
}

Dit werkt programmeerfouten door naïeve PHP-gebruikers in de hand. De declaratie public $Password maakt het wachtwoord van een gebruiker overal openbaar toegankelijk. We kunnen in een team van webdevelopers natuurlijk wel afspreken dat de eigenschap User::$Password nooit mag worden gelezen. En we kunnen zo’n fout er bij een review van applicaties ook wel uithalen. Maar wat we veel beter kunnen doen, is de fout bij voorbaat uitsluiten en géén public $Password maar private $Password gebruiken.

In de praktijk zie ik dat naïeve PHP-gebruikers ook naïeve getters en setters nogal eens verkeerd gebruiken. Een eerste opzet van een class User maken ze dan zo:

class User
{
    private $Username;
    private $Password;

    public function setUsername($username)
    {
        $this->Username $username;
    }

    public function getUsername()
    {
        return $this->Username;
    }

    public function setPassword($password)
    {
        $this->Password $password;
    }

    public function getPassword()
    {
        return $this->Password;
    }
}

Op de automatische piloot wordt elke private eigenschap van de PHP-klasse hier voorzien van een naïeve setter en een naïeve getter. Prima, zou je misschien denken, met deze flexibele opzet kunnen we later alle kanten op. Voor de beveiliging is dit helaas verre van ideaal. Met de laatste naïeve getter public function getPassword() hebben we de klasse namelijk een achterdeur gegeven die even ver openstaat als bij een public $Password.

Kortom, gebruik gerust naïeve getters en setters, maar gebruik ze nooit naïef.

1 opmerking:

  1. Spreek je dan binnen de klasse zelf de properties aan of de getters/setters?

    $this->foo = 'bar';
    $this->setFoo('bar');

    BeantwoordenVerwijderen