Traits w PHP
22 Feb 2008
Kilka dni temu dostałem z PHP Ireland powiadomienie o propozycji włączenia do standardu języka konstrukcji Traits. Traits to mechanizm obchodzący ograniczenia dziedziczenia jednobazowego, umożliwiający zaimportowanie metod do klasy oraz jawne oznaczenie z jakich Traitsów klasa korzysta - jak przy interfejsach. Dla znających UML, Traits to jak Implementation Classes, które są abstrakcyjne tak jak interfejsy, ale posiadają implementację jak zwykłe klasy.
Polecam zapoznać się ze wspomnianym RFC oraz przykładami zanim go obsobaczę. Do czytania marsz.
Na pierwszy rzut oka wydaje się, że Traits to dopust boży dla wszystkich, którzy kiedykolwiek potrzebowali wyposażyć szereg klas we wspólną funkcjonalność. Przyjazna konstrukcja językowa przypominająca nieco CSS również może się podobać. Po chwili zastanowienia przychodzi refleksja, że przecież cały czas radzę sobie z podobnymi problemami bez użycia Traitsów!
Wyobraźmy sobie problem prawie rzeczywisty - potrzeba kompleksowego wyszukiwania po zadanej frazie wśród rozmaitych obiektów trwałych: użytkowników, dokumentów, produktów, itp. Z punktu widzenia projektanta obiektowego, klasa implementująca przede wszystkim trwałość nie jest właściwym miejscem dla systemu szukania. Traits przychodzi z rozwiązaniem: zrób implementację wyszukiwania osobno i włącz ją do klasy. Efekt końcowy jest taki, że mamy implementację szukania w klasie realizującej trwałość, czyli niepoprawnie. Jak zatem zrobić to właściwie?
Można wykorzystując interfejs i kompozycję oraz pewną sprytną technikę programistyczną:
class SearchEngine {
protected $table;
protected $columns;
public function __construct($table, $columns) {
$this->table = $table;
$this->columns = $columns;
}
public search($term) {
$sql = "SELECT * FROM {$this->table} WHERE FALSE";
foreach ($this->columns as $column) {
$sql .= " OR $column ILIKE '%$term%'";
}
// database stuff here
}
}
interface ISearchEngineProvider {
public static function getSearchEngine();
}
class User implements ISearchEngineProvider {
// class implementation here
public static function getSearchEngine() {
return new SearchEngine('users', array('login', 'name'));
}
}
class Product implements ISearchEngineProvider {
// class implementation here
public static function getSearchEngine() {
return new SearchEngine('products_view', array('name', 'category_name', 'make'));
}
}
Widzimy tu czterech uczestników tego teatrzyku (ha, rym!): prosta implementacja wyszukiwania SearchEngine, interfejs dostarczyciela wyszukiwarki ISearchEngineProvider oraz dwie przykładowe klasy użytkownika i produktu.
Klasy User i Product dostarczają informację o implementowanym interfejsie, można więc na nich wywołać statyczną (statyczną dla wygody) metodę getSearchEngine(), która zwróci nam skonfigurowaną wyszukiwarkę. Technika ta nazywa się Dependency Injection i jest niezwykle skuteczna w czynieniu złożonych problemów prostymi - jak w powyższym problemie, gdzie klasy użytkownika i produktu nie mają wspólnego przodka. Element różnicujący zostaje "wstrzyknięty" do kontenera SearchEngine w konstruktorze, ale typ kontenera pozostaje niezmienny.
Możliwa jest dalsza wariacja na temat zwracanego wyniku wyszukiwania. Klasy User i Product mogą zwracać swoje własne wyszukiwarki wyprowadzone z klasy SearchEngine. I tak User::getSearchEngine() może zwracać obiekt klasy UsersSearchEngine, który z kolei zwróci gotowe intancje User zamiast zwyczajnych wierszy z bazy danych, a Product::getSearchEngine() zrobi to analogicznie dla produktów. Typ kontenera nadal pozostaje niezmienny, gdyż zróżnicowane wyszukiwarki są pochodnymi klasy SearchEngine.
Z powyższego rodzi mi się parę luźnych spostrzeżeń:
- problemy rozwiązywane przez Traits da się z łatwością rozwiązać poprzez poprawnie zaprojektowaną strukturę obiektów
- PHP za wszelką cenę chce być bardziej dynamiczny niż Ruby
- dynamic is not the only way to go
- Traits pozwalają kiepskim developerom poczuć się lepiej
- niektórzy developerzy nie odrobili lekcji i gówno wiedzą o interfejsach, kompozycji i delegacji odpowiedzialności do klas usługowych
Dodatkowo gdy czytam o rozwiązywaniu konfliktów w przestrzeni nazw klasy lub Traitsów to wydaje się to dość czytelne, ale gdy wyobrażam sobie kod stworzony w ten sposób przez jakiegoś żółtodzioba to przechodzą mi ciarry.
Wuj Dobra Rada: nie używaj tego gada.
- MySZ
Trochę inaczej to wygląda, gdy używasz dokładnie tak samo działającej metody w różnych klasach. W Twoim przykładzie one się różnią (dla różnych klas jest tworzony obiekt SearchEngine z innymi parametrami), pomijając tutaj każdorazowe tworzenie obiektu przy każdym żądaniu szukiwajki - składam to na karb oszczędności w pisaniu :)
A co jeśli masz w różnych klasach dać dokładnie tą samą funkcję, działającą tak samo etc?
Wtedy pisanie:
class A {
function search () {
return new SearchEngine ();
}
}
class B {
function search () {
return new SearchEngine ();
}
}
Jest nadmiarowością. Powtarzasz kod. Jeszcze gorzej, gdy powinna być tam jakaś konfiguracja etc, która zawsze będzie taka sama. OK, da się to obejść - tworzysz jeszcze jedną funkcję, która robi co trzeba z konfiguracją SearchEngine i zwraca obiekt, a w A::search () wywołujesz tąże funkcję etc etc...
Natomiast Traitsy pozwalają na uproszczenie, ułatwienie zapisu:
trait search {
function search () {
return new SearchEngine ();
}
}
class A {
use search;
}
class B {
use search;
}
IMO ma to sens.- mwd
@MySZ:
"A co jeśli masz w różnych klasach dać dokładnie tą samą funkcję, działającą tak samo etc?"
Wyciągasz klasę nadrzędną, a te dwie różne implementujesz jako specjalizacja.- MySZ
@mwd: to jest dobre tylko przy prostych przypadkach. Owszem, zawsze da się zastosować jakąś _kombinację_, ale to zawsze będzie jakaś nadmiarowość, której wielodziedziczenie, czy też wersja proponowana tutaj (traitsy) pozwalają uniknąć.
- str()
MySZ, jeśli w ogóle nie ma różnic, to po co implementować tą metodę wewnątrz klasy? Lepiej wówczas delegować ją do oddzielnej klasy, bo jej związek z przedmiotową klasą jest i tak dość luźny.
mwd, nie możesz wyciągnąć wspólnej klasy bazowej, bo mówimy tu o klasach z różnych przodków. Nie ma tego w powyższych przykładach, ale takie jest założenie i cel Traitsów.- MySZ
@str(): "MySZ, jeśli w ogóle nie ma różnic, to po co implementować tą metodę wewnątrz klasy?"
Dla zachowania czytelności interfejsu. Razi mnie np pythonowe len() - imo poszczególne struktury powinny mieć metodę .len() zamiast stosowania globalnej funkcji.- str()
MySZ, ależ czytelność interfejsu jest zachowana właśnie przez jawną deklarację interfejsu. Nie proponuję tu wprowadzania globalnych funkcji, a stworzenie klasy usługowej - w tym przypadku wyszukiwarki. Ponadto osobna klasa przyda się, jeśli chcesz w niej mieć więcej niż jedną metodę.
Co do Twojej uwagi w pierwszym komencie o tworzeniu dodatkowej instancji - obiekty w PHP są *cholernie* szybkie, prawie tak szybkie jak prymitywy, a na pewno szybsze niż tablice. Nie oszczędzam czasu procesora - on jest tani, oszczendzam za to czas swój i koderów, którzy w przyszłości będą korzystać/rozbudowywać program. Czas programisty jest bardzo drogi (czego wszystkim życzę ;-)- D4rky
> niektórzy developerzy nie odrobili lekcji i gówno wiedzą o interfejsach, kompozycji i delegacji odpowiedzialności do klas usługowych
Miałbyś jakieś dobre linki do informacji lub tytuły książek na ten temat? Byłbym wdzięczny, bo gówno wiem :P- stronger
Służę: A. Shallowai, J. Trott - Design Patterns Explained: A New Perspective on Object-Oriented Design (jest też jej polskie tłumaczenie, ale nakład został wyczerpany) - bardzo dobra książka. Ponadto klasyczna pozycja Design Patterns: Elements of Reusable Object-Oriented Software - sprawdź w Wikipedii pod hasłem "Gang_of_four_(software)". Na koniec jeszcze coś nowszego: M. Fowler - Patterns of Enterprise Application Architecture. Wszystkie godne polecenia, ponadto na www.dofactory.com znajduje się przystępnie rozpisany katalog wzorców.
- D4rky
Dziekowac.







