Cechy w PHP - jakieś przykłady z życia wzięte / najlepsze praktyki? [Zamknięte]

148

Cechy były jednym z największych dodatków w PHP 5.4. Znam składnię i rozumiem ideę kryjącą się za cechami, takimi jak poziome ponowne wykorzystanie kodu do typowych rzeczy, takich jak logowanie, bezpieczeństwo, buforowanie itp.

Jednak nadal nie wiem, jak wykorzystałbym cechy w moich projektach.

Czy są jakieś projekty open source, które już używają cech? Jakieś dobre artykuły / materiały do ​​czytania na temat tworzenia struktur architektur przy użyciu cech?

Maks
źródło
8
Oto moja opinia: wpis na blogu na ten temat, który napisałem na ten temat. TL; DR: Zasadniczo obawiam się, że chociaż są potężne i mogą być używane na dobre, większość zastosowań, które zobaczymy, będzie kompletnymi anty-wzorcami i spowoduje znacznie więcej bólu niż rozwiązuje ...
ircmaxell
1
Zapoznaj się ze standardową biblioteką scala, a znajdziesz wiele przydatnych przykładów cech.
dmitry

Odpowiedzi:

89

Osobiście uważam, że podczas pisania czystego kodu zastosowanie cech jest bardzo niewiele.

Zamiast używać cech do włamania kodu do klasy, lepiej jest przekazać zależności za pomocą konstruktora lub ustawiających:

class ClassName {
    protected $logger;

    public function __construct(LoggerInterface $logger) {
        $this->logger = $logger;
    }
    // or
    public function setLogger(LoggerInterface $logger) {
        $this->logger = $logger;
    }
}

Głównym powodem, dla którego uważam, że jest to lepsze niż używanie cech, jest to, że twój kod jest znacznie bardziej elastyczny dzięki usunięciu twardego sprzężenia z cechą. Na przykład możesz teraz po prostu przekazać inną klasę rejestratora. To sprawia, że ​​twój kod jest wielokrotnego użytku i testowalny.

NikiC
źródło
4
Używając cech, możesz również użyć innej klasy rejestratora, prawda? Po prostu edytuj cechę, a wszystkie klasy, które jej używają, zostaną zaktualizowane. Popraw mnie, jeśli się mylę
rickchristie
14
@rickchristie Jasne, możesz to zrobić. Ale musiałbyś edytować kod źródłowy cechy. Więc zmieniłbyś to dla każdej używającej go klasy, a nie tylko dla tej, dla której chcesz mieć inny rejestrator. A co, jeśli chcesz użyć tej samej klasy, ale z dwoma różnymi rejestratorami? A może chcesz zdać próbny rejestrator podczas testów? Nie możesz, jeśli używasz cech, możesz, jeśli używasz zastrzyku zależności.
NikiC,
2
Rozumiem twój punkt widzenia, zastanawiam się również, czy cechy są tego warte, czy nie. Chodzi mi o to, że w nowoczesnych frameworkach, takich jak Symfony 2, masz wszędzie zastrzyk zależności, który wydaje się być wyższy od cech w większości przypadków. W tej chwili widzę cechy niewiele więcej niż „kopiowanie i wklejanie wspomagane kompilatorem”. ;)
Max
11
W tej chwili widzę cechy niewiele więcej niż „kopiowanie i wklejanie wspomagane kompilatorem”. ;) : @Max: Dokładnie tak zaprojektowano cechy, więc jest to całkowicie poprawne. To sprawia, że ​​jest łatwiejszy w utrzymaniu, ponieważ jest tylko jedna definicja, ale w zasadzie jest to po prostu c & p ...
ircmaxell
29
NikiC nie ma sensu: użycie cechy nie zapobiega użyciu Dependency Injection. W tym przypadku cecha po prostu pozwoli każdej klasie, która implementuje logowanie, nie będzie musiała powielać metody setLogger () i tworzyć właściwości $ logger. Cecha zapewniłaby im. setLogger () wpisze podpowiedź w LoggerInterface, tak jak w przykładzie, aby można było przekazać dowolny typ loggera. Pomysł jest podobny do odpowiedzi Gordona poniżej (tylko wygląda na to, że podpowiada on superklasę Loggera zamiast interfejsu Loggera ).
Ethan
205

Wydaje mi się, że przez jakiś czas należałoby przyjrzeć się językom, które mają cechy, aby poznać przyjęte dobre / najlepsze praktyki. Moja obecna opinia na temat Trait jest taka, że ​​powinieneś używać ich tylko do kodu, który musiałbyś powielać w innych klasach, które mają tę samą funkcjonalność.

Przykład cechy Loggera:

interface Logger
{
    public function log($message, $level);    
}

class DemoLogger implements Logger
{
    public function log($message, $level)
    {
        echo "Logged message: $message with level $level", PHP_EOL; 
    }
}

trait Loggable // implements Logger
{
    protected $logger;
    public function setLogger(Logger $logger)
    {
        $this->logger = $logger;
    }
    public function log($message, $level)
    {
        $this->logger->log($message, $level);
    }
}

class Foo implements Logger
{
    use Loggable;
}

A potem robisz ( demo )

$foo = new Foo;
$foo->setLogger(new DemoLogger);
$foo->log('It works', 1);

Myślę, że ważną rzeczą do rozważenia podczas używania cech jest to, że tak naprawdę są to tylko fragmenty kodu, które są kopiowane do klasy. Może to łatwo prowadzić do konfliktów, na przykład przy próbie zmiany widoczności metod, np

trait T {
    protected function foo() {}
}
class A { 
    public function foo() {}
}
class B extends A
{
    use T;
}

Powyższe spowoduje błąd ( demo ). Podobnie, żadne metody zadeklarowane w trait, które są już zadeklarowane w klasie using nie zostaną skopiowane do klasy, np.

trait T {
    public function foo() {
    return 1;
}
}
class A { 
    use T;
    public function foo() {
    return 2;
}
}

$a = new A;
echo $a->foo();

wypisze 2 ( demo ). Są to rzeczy, których będziesz chciał uniknąć, ponieważ utrudniają one znalezienie błędów. Będziesz także chciał uniknąć przypisywania rzeczy cechom operującym na właściwościach lub metodach klasy, która z nich korzysta, np

class A
{
    use T;
    protected $prop = 1;
    protected function getProp() {
        return $this->prop;
    }
}

trait T
{
    public function foo()
    {
        return $this->getProp();
    }
}

$a = new A;
echo $a->foo();

działa ( wersja demonstracyjna ), ale teraz cecha jest ściśle powiązana z A i cała idea ponownego wykorzystania w poziomie zostaje utracona.

Postępując zgodnie z zasadą segregacji interfejsów , będziesz mieć wiele małych klas i interfejsów. To sprawia, że ​​cechy są idealnym kandydatem do rzeczy, o których wspomniałeś, np. Przekrojów , ale nie do komponowania obiektów (w sensie strukturalnym). W powyższym przykładzie Loggera cecha jest całkowicie izolowana. Nie ma zależności od konkretnych klas.

Moglibyśmy użyć agregacji / kompozycji (jak pokazano w innym miejscu na tej stronie), aby osiągnąć tę samą wynikową klasę, ale wadą korzystania z agregacji / kompozycji jest to, że będziemy musieli ręcznie dodać metody proxy / delegatora do każdej klasy, a następnie powinno móc się logować. Cechy dobrze to rozwiązują, pozwalając mi trzymać szablon w jednym miejscu i selektywnie go stosować w razie potrzeby.

Uwaga: biorąc pod uwagę, że cechy są nową koncepcją w PHP, wszystkie opinie wyrażone powyżej mogą ulec zmianie. Nie miałem jeszcze zbyt wiele czasu, aby samemu ocenić koncepcję. Mam jednak nadzieję, że wystarczy, aby dać Ci coś do przemyślenia.

Gordon
źródło
41
To interesujący przypadek użycia: użyj interfejsu, który definiuje kontrakt, użyj cechy, aby spełnić tę umowę. Dobry.
Max
13
Lubię takich prawdziwych programistów, którzy proponują prawdziwe przykłady pracy z krótkim opisem dla każdego. Dzięki
Arthur Kushman,
1
A jeśli ktoś zamiast tego użyje klasy abstrakcyjnej? Zastępując interfejs i cechę, można stworzyć klasę abstrakcyjną. Również jeśli interfejs jest tak niezbędny dla aplikacji, klasa abstrakcyjna może również implementować interfejs i definiować metody, takie jak trait. Czy możesz więc wyjaśnić, dlaczego nadal potrzebujemy cech?
sumanchalki
12
@sumanchalki Klasa abstrakcyjna jest zgodna z zasadami dziedziczenia. A jeśli potrzebujesz klasy, która implementuje Loggable i Cacheable? Potrzebowałbyś klasy, aby rozszerzyć AbstractLogger, która musi wtedy rozszerzyć AbstractCache. Ale to oznacza, że ​​wszystkie Loggables to Cache. To jest połączenie, którego nie chcesz. Ogranicza ponowne użycie i psuje wykres dziedziczenia.
Gordon,
1
Myślę, że linki demo są martwe
Pmpr
19

:) Nie lubię teoretyzować i debatować o tym, co z czymś zrobić. W tym przypadku cechy. Pokażę ci, do czego są przydatne cechy i możesz się z tego nauczyć lub zignorować.

Cechy - świetnie nadają się do stosowania strategii . Krótko mówiąc, wzorce projektowania strategii są przydatne, gdy chcesz, aby te same dane były obsługiwane w inny sposób (filtrowane, sortowane itp.).

Na przykład masz listę produktów, które chcesz odfiltrować na podstawie pewnych kryteriów (marki, specyfikacje, cokolwiek) lub posortować według różnych środków (cena, etykieta, cokolwiek). Możesz utworzyć cechę sortowania, która zawiera różne funkcje dla różnych typów sortowania (numeryczne, ciąg, data itp.). Możesz następnie użyć tej cechy nie tylko w swojej klasie produktu (jak podano w przykładzie), ale także w innych klasach, które wymagają podobnych strategii (aby zastosować sortowanie numeryczne do niektórych danych itp.).

Spróbuj:

<?php
trait SortStrategy {
    private $sort_field = null;
    private function string_asc($item1, $item2) {
        return strnatcmp($item1[$this->sort_field], $item2[$this->sort_field]);
    }
    private function string_desc($item1, $item2) {
        return strnatcmp($item2[$this->sort_field], $item1[$this->sort_field]);
    }
    private function num_asc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] < $item2[$this->sort_field] ? -1 : 1 );
    }
    private function num_desc($item1, $item2) {
        if ($item1[$this->sort_field] == $item2[$this->sort_field]) return 0;
        return ($item1[$this->sort_field] > $item2[$this->sort_field] ? -1 : 1 );
    }
    private function date_asc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 < $date2 ? -1 : 1 );
    }
    private function date_desc($item1, $item2) {
        $date1 = intval(str_replace('-', '', $item1[$this->sort_field]));
        $date2 = intval(str_replace('-', '', $item2[$this->sort_field]));
        if ($date1 == $date2) return 0;
        return ($date1 > $date2 ? -1 : 1 );
    }
}

class Product {
    public $data = array();

    use SortStrategy;

    public function get() {
        // do something to get the data, for this ex. I just included an array
        $this->data = array(
            101222 => array('label' => 'Awesome product', 'price' => 10.50, 'date_added' => '2012-02-01'),
            101232 => array('label' => 'Not so awesome product', 'price' => 5.20, 'date_added' => '2012-03-20'),
            101241 => array('label' => 'Pretty neat product', 'price' => 9.65, 'date_added' => '2012-04-15'),
            101256 => array('label' => 'Freakishly cool product', 'price' => 12.55, 'date_added' => '2012-01-11'),
            101219 => array('label' => 'Meh product', 'price' => 3.69, 'date_added' => '2012-06-11'),
        );
    }

    public function sort_by($by = 'price', $type = 'asc') {
        if (!preg_match('/^(asc|desc)$/', $type)) $type = 'asc';
        switch ($by) {
            case 'name':
                $this->sort_field = 'label';
                uasort($this->data, array('Product', 'string_'.$type));
            break;
            case 'date':
                $this->sort_field = 'date_added';
                uasort($this->data, array('Product', 'date_'.$type));
            break;
            default:
                $this->sort_field = 'price';
                uasort($this->data, array('Product', 'num_'.$type));
        }
    }
}

$product = new Product();
$product->get();
$product->sort_by('name');
echo '<pre>'.print_r($product->data, true).'</pre>';
?>

Na koniec myślę o cechach, takich jak akcesoria (których mogę użyć do zmiany moich danych). Podobne metody i właściwości, które można wyciąć z moich klas i umieścić w jednym miejscu, dla łatwej obsługi, krótszego i czystszego kodu.

D. Marti
źródło
1
Chociaż utrzymuje to publiczny interfejs w czystości, wewnętrzny może stać się naprawdę skomplikowany, zwłaszcza jeśli rozszerzysz to na inne rzeczy, na przykład kolory. Myślę, że proste funkcje lub metody statyczne są tutaj lepsze.
Sebastian Mach,
Podoba mi się ten termin strategies.
Rannie Ollit,
4

Jestem podekscytowany cechami, ponieważ rozwiązują one częsty problem podczas tworzenia rozszerzeń dla platformy e-commerce Magento. Problem występuje, gdy rozszerzenia dodają funkcjonalność do klasy podstawowej (na przykład modelu użytkownika), rozszerzając ją. Odbywa się to poprzez wskazanie autoloaderowi Zend (poprzez plik konfiguracyjny XML), aby używał modelu użytkownika z rozszerzenia, a nowy model rozszerza model podstawowy. ( przykład ) Ale co, jeśli dwa rozszerzenia zastępują ten sam model? Otrzymujesz „stan wyścigu” i tylko jeden jest załadowany.

Obecnie rozwiązaniem jest edycja rozszerzeń, tak aby rozszerzyć klasę przesłaniania modelu drugiej strony w łańcuchu, a następnie ustawić konfigurację rozszerzenia tak, aby ładowała je we właściwej kolejności, tak aby łańcuch dziedziczenia działał.

Ten system często powoduje błędy, a podczas instalowania nowych rozszerzeń konieczne jest sprawdzenie konfliktów i edycja rozszerzeń. To jest uciążliwe i przerywa proces aktualizacji.

Myślę, że użycie cech byłoby dobrym sposobem na osiągnięcie tego samego bez tego irytującego modelu zastępującego „stan wyścigu”. To prawda, że ​​nadal mogą występować konflikty, jeśli wiele cech zaimplementuje metody o tych samych nazwach, ale wyobrażam sobie, że coś takiego jak prosta konwencja przestrzeni nazw może rozwiązać ten problem w większości.

TL; DR Myślę, że cechy mogą być przydatne do tworzenia rozszerzeń / modułów / wtyczek dla dużych pakietów oprogramowania PHP, takich jak Magento.

thaddeusmt
źródło
0

Możesz mieć cechę dla obiektu tylko do odczytu, taką jak ta:

  trait ReadOnly{  
      protected $readonly = false;

      public function setReadonly($value){ $this->readonly = (bool)$value; }
      public function getReadonly($value){ return $this->readonly; }
  }

Możesz wykryć, czy ta cecha jest używana i określić, czy powinieneś zapisać ten obiekt w bazie danych, pliku itp.

Nico
źródło
Tak więc klasa, która będzie miała usetę cechę, zadzwoni if($this -> getReadonly($value)); ale spowodowałoby to błąd, gdybyś nie miał usetej cechy. Dlatego ten przykład jest błędny.
Luceos
Cóż, musisz najpierw sprawdzić, czy cecha jest używana. Jeśli cecha ReadOnly jest zdefiniowana w obiekcie, możesz następnie sprawdzić, czy jest on tylko do odczytu, czy nie.
Nico,
Zrobiłem ogólny dowód koncepcji takiej cechy na gist.github.com/gooh/4960073
Gordon
3
W tym celu należy zadeklarować interfejs dla ReadOnly
Michael Tsang