PHP: Go home static, you’re drunk
|
Kilka powodów dlaczego static
powinien zniknąć, bądź przynajmniej stracić na popularności.
Zmienne statyczne
Na tapetę idzie taki przykład:
class A {
private static $var;
public function var($val = null) {
if($val !== null) {
static::$var = $val;
}
return static::$var;
}
}
Mamy klasę A
ze statyczną zmienną $var
. Owa zmienna jest wspólna między wszystkimi instancjami, tj. każdy obiekt klasy A
może dowolnie modyfikować jej wartość. Mamy więc obiekty, które nie posiadają pełnej kontroli nad swoim stanem - enakpsulacja właśnie dostała w twarz.
Drugi przykład:
class A {
protected static $var;
public function var($val = null) {
if($val !== null) {
static::$var = $val;
}
return static::$var;
}
}
class B extends A {
}
Krótki ale jakże wymowny - teraz nie tylko obiekty klasy A
mogą modyfikować stan $var
ale i obiekty klasy B
. Enkapsulacja dostała ponownie w twarz i cichutko chlipie w kącie.
Ile frameworków potraficie wymienić z podobnymi klasami? A jak zmienimy nazwę zmiennej na $db
a klasy na Database
?
Dalej będzie tylko gorzej.
class A {
public static $a = 'foo';
}
A::$a = 'bar';
$A = new A();
var_dump($A::$a);
var_dump(A::$a);
Robiąc $var
publiczną, dajemy możliwość zmiany stanu każdemu w dowolnym momencie, we wszystkich instancjach jednocześnie. I nawet nie trzeba się odwoływać do żadnej instancji.
Metody statyczne
Metody statyczne są dostępne globalnie, w każdym miejscu, klasie, funkcji. Wszędzie możemy napisać:
$result = Database::query('...');
Jeśli taki zapis pojawi się w klasie, to wprowadzamy ukrytą zależność. Oczywiście nikt o niej nie wie, nawet autor - bo już zapomniał. Powiązaliśmy klasy między sobą, nie poprzez interfejs czy prototyp ale przez nazwę, zmieniliśmy Object Oriented Programming w Class Oriented Programming.
Kolejny przykład:
class Database {
public static $con;
public static function connect($server, $database, $username, $password) {
self::$con = new mysqli($server, $username, $password, $database);
}
public static function query($query) {
return self::$con->query($query);
}
public static function close() {
return self::$con->close();
}
}
Metoda query
musi być poprzedzona wywołaniem connect
. Gdzieś na początku skryptu, czy może każdorazowo sprawdzamy $con
? Chyba to drugie, bo przecież $con
może być modyfikowany przez inne instancje Database
. W sumie, jeżeli są inne instancje, to za cholerę nie wiadomo z czym właściwie jesteśmy połączeni.
Rozwiązaniem jest singleton. Nie będę pisał jak się go implementuje - zakładam, że czytelnik jakąś tam wiedzę posiada.
$result = Database::getInstance()->query(...)
Proste wywołanie, a w zamian każdorazowo gdy chcemy wywołać query
, getInstance
sprawdzi, czy już nawiązaliśmy połączenie, czy dopiero trzeba to zrobić. W efekcie mamy jedną klasę globalną zapewniającą wszędzie, w każdej warstwie abstrakcji połączenie do bazy danych. Singleton rozwiązał też problem z modyfikowaniem zmiennych statycznych przez inne instancje - nie ma innych instancji.
Problem w tym, że gdzieś trzeba podawać dane potrzebne do nawiązania połączenia. Zaszyć je w deklaracji klasy. Podawać je każdorazowo jako parametry getInstance
? Może gdzieś na początku programu pilnować się i wywoływać connect
? Z innego obiektu statycznego? Czy wspominałem o tym że drugie połączenie do bazy wymaga osobnej klasy, nawet jeśli różni się tylko danymi dostępowymi?
Dlaczego w ogóle static?
Dla wygody. Krótkotrwałej i kończącej się czkawką.
Metody statyczne często są też używane przez ludzi przyzwyczajonych do programowania proceduralnego. Stają się tzw. helperami, podpięte pod jakąś klasę-kontener grupujący, świadczą funkcjonalność, która nie zawsze powinna być dostępna poza konkretną warstwą.
Mniej doświadczeni programiści, by uniknąć trudu przepychania instancji przez kolejne warstwy abstrakcji, omamieni łatwością implementacji tworzą klasy pełne statycznych metod. Później, łatwość wywołań dodatkowo zachęca do korzystania gdzie się chce, niezależnie czy klasa ma rację bytu na tym etapie czy nie.
Przykład z dokumentacji Laravela:
Route::get('/', function() {
return 'Hello World';
});
W imię wygody, autorzy ukryli całkiem niezły kontener zależności pod postacią statycznej metody. Tak! Route
jest tylko fasadą, za którą stoi wywołanie komponentu z kontenera.
Z dokumentacji Fuela
if (\Fuel::$env == \Fuel::DEVELOPMENT)
{
return \Request::forge('secret/mystuff/keepout', false)->execute();
}
Zmienne i metody statyczne. Ktoś kto nie będzie potrafił przekazać potrzebnych instancji między warstwami, odwoła się bezpośrednio - bo może. Zaś ci, którzy potrafią - nie potrzebują takiej wygody.
W starszych frameworkach jak Kohana, Codeigniter czy stary Zend, static
jest znacznie więcej. Można im jednak wybaczyć, powstawały w czasach gdy PHP nie oferował obecnych narzędzi.
Dziś, static
może pójść w kąt i być używany wtedy, gdy jest konieczny, a nie kiedy jego jedyną zaletą jest wygoda.