Blog webdeveloperski Patryk yarpo Jar

Gettery i settery w PHP

Autor wiadomości Kwiecień 20, 2011

Najpopularniejszym paradygmatem programowania jest programowanie zorientowane obiektowo. Obiekt posiada jakieś właściwości, które [IMO] najczęściej powinny być prywatne. Warto by było móc się do nich jakoś odwoływać, czy to podczas zapisywania danych, czy też w celu odczytu. Do tego bardzo przydatne okazać się mogą funkcje popularnie zwane getter ("akcesor" - pobierająca dane) bądź setter ("mutator" - ustawiająca).

W tym wpisie postaram się pokazać kilka możliwych podejść do tego zagadnienia.

Po co mi getter / setter?

Pewna szkoła programistyczna mówi, że żadne właściwości (zmienne obiektu) nie powinny być publiczne.  Jestem wyznawcą tej szkoły (choć pamiętajmy też, że nie każde pole prywatne musi posiadać g/s). Spróbuję pokazać przykład, w którym takie podejście pozwala oszczędzić sporo pracy.

Załóżmy, że mamy klasę `Kwadrat':

class Kwadrat
{
    public $pole;
    public $bok;
    public $obwod;
}

Kiedy chcielibyśmy stworzyć instancję tej klasy:

$kwadrat = new Kwadrat();
$kwadrat->bok = 10;
echo $kwadrat->pole; // brak wartości

Czyż nie lepiej, aby ustawienie nowej wartości dla właściwości `bok' automatycznie ustawiało pozostałe pola, których wartość powinna zostać zaktualizowana?

Jak tego dokonać? Zamiast publicznych właściwości używać prywatnych i odpowiednich funkcji ustawiających i pobierających dane.

getPole, setPole - a'la Java

Jest to metoda znana z Javy. Dla każdego pola (choć nie tylko, zaraz pokażę o co mi chodzi) tworzymy dwie metody: `set{NazwaPola}' oraz `get{NazwaPola}'. Będę zmieniał kod z poprzedniego podpunktu.

Krok 1: pola prywatne i publiczne metody

class Kwadrat
{
    private $pole;
    private $bok;
    private $obwod;

    public function setPole($p) { $this->pole = $p; }
    public function setBok($b) { $this->bok = $b; }
    public function setObwod($o) { $this->obwod = $o; }
    public function getPole() { return $this->pole; }
    public function getBok() { return $this->bok; }
    public function getObwod() { return $this->obwod; }
}

Póki co takie rozwiązanie nie daje zbyt wielu korzyści w porównaniu z poprzednim kodem. Nadal robiąc tak:

$kwadrat = new Kwadrat();
$kwadrat->setBok(10);
echo $kwadrat->getPole(); // brak wartości

Krok 2: kaskadowa aktualizacja wartości

Ustawiając nową wartość dla jednej właściwości, automatycznie zostaną ustawione wartości dla pozostałych.

class Kwadrat
{
... // pola prywatne takie same
    public function setPole($p)
    {
        $this->pole = $p;
        $this->bok = sqrt($p);
        $this->obwod = $p/4;
    }
    public function setBok($b)
    {
        $this->bok = $b;
        $this->obwod = $b*4;
        $this->pole = $b*$b;
    }
    public function setObwod($o)
    {
        $this->obwod = $o;
        $this->bok = $o/4;
        $this->pole = $this->bok*$this->bok;
    }
...// settery takie same
}

Dzięki takiemu zabiegowi, mając coś takiego:

$kwadrat = new Kwadrat();
$kwadrat->setBok(10);
echo $kwadrat->getPole(); // 100

Otrzymamy już poprawny wynik. Ale to jeszcze można polepszyć 🙂

Krok 3: usunięcie redundancji

Tak naprawdę w poprzedniej klasie mamy 3 właściwości, które wynikają jedna z drugiej. Za każdym razem i tak obliczmy pozostałe. Czy nie można zrobić tego lepiej? Zaproponuje rozwiązanie, które w niektórych przypadkach może być szybsze.

class Kwadrat
{
    private $bok;

    public function setPole( $pole )
    {
        $this->bok = sqrt($pole);
    }
    public function setBok( $bok )
    {
        $this->bok = $bok;
    }
    public function setObwod( $obwod )
    {
        $this->bok = $obwod / 4;
    }
    public function getPole()
    {
          return $this->bok*$this->bok;
    }
    public function getBok()
    {
          return $this->bok;
    }
    public function getObwod()
    {
          return $this->bok*4;
    }
}

Dzięki takiemu rozwiązaniu unikamy niepotrzebnych obliczeń przy ustawianiu właściwości. Można by dodać ponownie usunięte prywatne właściwości i w getterze wyliczając je przypisywać do odpowiedniej zmiennej. To już jest kwestia tego w jakich warunkach byłby ten obiekt wykorzystywany. Być może pozwalałoby to na przyspieszenie pracy. TODO: testy wydajnościowe 😉

Dwie funkcje dla wszystkich właściwości + zbiornik na dane

Inny sposobem, oszczędzającym miejsce, choć mniej czytelnym dla programisty, który odziedziczy Twój kod jest jeden globalny setter i jeden globalny getter. Ma to zdecydowanie jedna dużą zaletę - pozwala na zmniejszenie ilości kodu. Ma też wadę - kod jest IMO mniej czytelny.

Od zmniejszania ilości kodu, jest odpowiednio przeprowadzona refaktoryzacja. Jeśli chcemy, aby nasze programy zajmowały mniej linii, zawsze możemy napisać je nie używać enterów :). Jeśli kod staje się za długi, to znaczy, że wymaga poważniejszych zmian, niż tylko skrócenie nazw zmiennych. Ale to jest temat na osobny artykuł ;).

Takie rozwiązanie jest szczególnie przydatne w klasach "konfiguracyjnych" - zbiornikach na dane:

class Config
{
    private $params = array();
    public function get( $k )
    {
        return $this->params[$k];
    }
    public function set($key, $value)
    {
        $this->params[$key] = $value;
    }
}

Dla czytelności przykładu zrezygnowałem z jakiejkolwiek walidacji. Warto choćby sprawdzić, czy istnieje oczekiwany klucz w `get' i jeśli nie, rzucić jakiś zgrabny wyjątek.

Wykorzystanie kodu:

$conf = new Config();
$conf->set('pole', 12);
$conf->set('bazadanych', new bazaDanych());
...
echo $conf->get('pole');
$bd = $conf->get('bazadanych');

Automagia

PHP w swoich nowszych (5 i nowszych) wersjach posiada także sporo magii. Przykładami na takie automagiczne metody są `__get' i `__set':

class MagicConfig
{
    private $params = array();

    public function __set( $key, $value )
    {
        $this->params[$key] = $value;
    }
    public function __get( $key )
    {
        return $this->params[$key];
    }
}

Sam kod zmienił się bardzo nieznacznie. Można go jednak "czytelniej" używać:

$conf = new MagicConfig();
$conf->pole = 12;
$conf->bazadanych = new bazadanych();
...
echo $conf->pole;

Jak widać na powyższym listingu, kodu jest trochę mniej. I być może sprawia on wrażenie bardziej czytelnego. Ja jednak jestem zwolennikiem braku magi [takiego typu] w kodzie. Nigdy ten rodzaj g/s mnie do siebie nie przekonał.

Podobnie jak wcześniejszy przykład, może to być dobre rozwiązanie dla "worków z danymi" . Jeśli jednak chcemy uniemożliwić nadpisania z zewnątrz jakiejś wartości pojawiają się nieciekawe warunki, walidacje itp. Moim zdaniem lepiej i przejrzyściej jest ręcznie zdefiniować jakie właściwości klasy w jaki sposób mają się zmieniać oraz jak zmiana ta ma wpływać na cały stan obiektu (czytaj g/s a'la Java).

Jedna funkcja dla get i set

Jest to moje "autorskie" rozwiązanie. Piszę w cudzysłowie, gdyż nie jest to mój patent, czy też na pewno nie ja pierwszy w programowaniu takie coś wymyśliłem. Jest to używane powszechnie w jQuery oraz AFAIK przez pearlowców. Sam wykorzystuje to też w swoich skryptach JS [bliźniaczy przykład w JS].

Skoro tam się to sprawdza, dlaczego nie wykorzystać tego w PHP?

Idea

Chodzi o to, aby zamiast dwóch metod - jednej pobierającej, drugiej ustawiającej dane, stworzyć jedną metodę, która będzie zachowywać się odpowiednio do potrzeb. Czynnikiem sterującym będzie tu przekazanie [bądź nie] parametru do metody.

<?php
class Kwadrat
{
    private $bok;

    public function pole()
    {
        // przekazano argument => setter
        if (1 == func_num_args())
        {
            $this->bok = sqrt(func_get_arg(0));
            return;
        }
        // getter
        return $this->bok*$this->bok;
    }

    public function bok()
    {
        if (1 == func_num_args())
        {
            $this->bok = func_get_arg(0);
            return;
        }
        return $this->bok;
    }

    public function obwod()
    {
        if (1 == func_num_args())
        {
            $this->bok = func_get_arg(0) / 4;
            return;
        }
        return $this->bok*4;
    }
}

$kwadrat = new Kwadrat();
$kwadrat->pole(10);
echo $kwadrat->obwod(); // 12.64
$kwadrat->bok(2);
echo $kwadrat->pole(); // 4

W kodzie tym zastosowałem dwie funkcje:

  • func_num_args - zwraca liczbę przekazanych do funkcji argumentów
  • func_get_arg - zwraca argument przekazany do funkcji. Parametrem jest indeks argumentu. 0 oznacza "pierwszy przekazany do funkcji argument

Takie podejście podoba mi się najbardziej. Dodatkowo można tu w prosty sposób zrobić wzorzec łańcuchowy, co pokazałem kiedyś na przykładzie JS.

Przykład użycia tego rozwiązania - JSCacher.

Komentarze (2) Trackbacks (0)
  1. Po to mamy do wyboru private, protected, public. Aby to co było private ma być private. Po co mi do tego publiczny getter ?

  2. Myślę, że przykład z kwadratem pokazuje po co.

    Zmienna to zmienna. Settter i getter zwraca pewien stan obiektu, a niekoniecznie wartość zmiennej.


Leave a comment

 

Brak trackbacków.