Tratti in PHP - qualche esempio / best practice del mondo reale? [chiuso]


148

I tratti sono stati una delle più grandi aggiunte per PHP 5.4. Conosco la sintassi e capisco l'idea alla base dei tratti, come il riutilizzo del codice orizzontale per cose comuni come registrazione, sicurezza, cache ecc.

Tuttavia, non so ancora come utilizzare i tratti nei miei progetti.

Esistono progetti open source che già utilizzano tratti? Qualche buon articolo / materiale di lettura su come strutturare le architetture usando i tratti?


8
Ecco la mia opinione: un post sul blog sull'argomento che ho scritto sull'argomento. TL; DR: Fondamentalmente, temo che mentre sono potenti e possono essere usati per sempre, la maggior parte degli usi che vedremo saranno anti-schemi completi e causeranno molto più dolore di quello che risolvono ...
ircmaxell

1
Dai un'occhiata alla libreria scala standard e scoprirai molti esempi utili di tratti.
Dmitry,

Risposte:


89

La mia opinione personale è che in realtà c'è pochissima applicazione per i tratti quando si scrive un codice pulito.

Invece di usare i tratti per hackerare il codice in una classe, è meglio passare le dipendenze tramite il costruttore o tramite setter:

class ClassName {
    protected $logger;

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

Il motivo principale per cui trovo che meglio dell'uso dei tratti è che il tuo codice è molto più flessibile rimuovendo l'accoppiamento duro con un tratto. Ad esempio, ora puoi semplicemente passare una classe di logger diversa. Questo rende il codice riutilizzabile e testabile.


4
Usando i tratti, puoi anche usare un'altra classe di logger, giusto? Basta modificare il tratto e tutte le classi che lo utilizzano vengono aggiornate. Correggimi se sbaglio
rickchristie,

14
@Rickchristie Certo, potresti farlo. Ma dovresti modificare il codice sorgente del tratto. Quindi lo cambieresti per ogni classe utilizzandolo, non solo per quello particolare per cui vuoi un logger diverso. E se si desidera utilizzare la stessa classe ma con due logger diversi? O se vuoi passare un finto logger durante il test? Non puoi, se usi tratti, puoi, se usi l'iniezione di dipendenza.
NikiC,

2
Vedo il tuo punto, sto anche meditando se i tratti valgono o meno. Voglio dire, in quadri moderni come Symfony 2 hai un'iniezione di dipendenza dappertutto che sembra superioir sui tratti nella maggior parte dei casi. Al momento, vedo i tratti non molto più di "copia e incolla assistito dal compilatore". ;)
Max

11
Al momento, vedo i tratti non molto più di "copia e incolla assistito dal compilatore". ;) : @Max: Questo è esattamente ciò che i tratti sono stati progettati per essere, quindi è completamente corretto. Lo rende più "mantenibile", dato che esiste solo una definizione, ma in pratica è solo c & p ...
ircmaxell,

29
A NikiC manca il punto: l'uso di un tratto non impedisce l'uso dell'iniezione di dipendenza. In questo caso, un tratto consentirebbe a ogni classe che implementa la registrazione di non dover duplicare il metodo setLogger () e la creazione della proprietà $ logger. Il tratto li avrebbe forniti. setLogger () scriverebbe il suggerimento su LoggerInterface come nell'esempio, in modo che qualsiasi tipo di logger possa essere passato. Questa idea è simile alla risposta di Gordon di seguito (sembra solo che stia scrivendo un suggerimento su una super classe Logger piuttosto che su un'interfaccia Logger ).
Ethan,

205

Immagino che uno dovrebbe cercare nelle lingue che hanno tratti da qualche tempo ormai per imparare le buone / migliori pratiche accettate. La mia attuale opinione su Trait è che dovresti usarli solo per il codice che dovresti duplicare in altre classi che condividono la stessa funzionalità.

Esempio per un tratto Logger:

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;
}

E poi lo fai ( demo )

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

Immagino che la cosa importante da considerare quando si usano i tratti è che sono davvero solo pezzi di codice che vengono copiati nella classe. Ciò può facilmente portare a conflitti, ad esempio, quando si tenta di modificare la visibilità dei metodi, ad es

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

Quanto sopra comporterà un errore ( demo ). Allo stesso modo, tutti i metodi dichiarati nel tratto che sono già dichiarati nella classe using non verranno copiati nella classe, ad es.

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

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

stamperà 2 ( demo ). Queste sono cose che vorresti evitare perché rendono gli errori difficili da trovare. Dovrai anche evitare di mettere le cose in tratti che operano su proprietà o metodi della classe che lo utilizza, ad es

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();

funziona ( demo ) ma ora il tratto è intimamente accoppiato ad A e l'intera idea di riutilizzo orizzontale è persa.

Quando segui il principio di segregazione dell'interfaccia avrai molte piccole classi e interfacce. Ciò rende Traits un candidato ideale per le cose che hai citato, ad esempio le preoccupazioni trasversali , ma non per comporre oggetti (in senso strutturale). Nel nostro esempio di Logger sopra, il tratto è completamente isolato. Non ha dipendenze da classi concrete.

Potremmo usare l' aggregazione / composizione (come mostrato altrove in questa pagina) per ottenere la stessa classe risultante, ma lo svantaggio dell'utilizzo dell'aggregazione / composizione è che dovremo aggiungere manualmente i metodi proxy / delegatore a ciascuna classe quindi dovrebbe essere in grado di accedere. I tratti risolvono bene ciò permettendomi di tenere la piastra della caldaia in un posto e applicarla selettivamente dove necessario.

Nota: dato che i tratti sono un nuovo concetto in PHP, tutte le opinioni espresse sopra sono soggette a modifiche. Non ho ancora avuto molto tempo per valutare il concetto da solo. Ma spero che sia abbastanza buono per darti qualcosa a cui pensare.


41
Quello è un caso d'uso interessante: usa un'interfaccia che definisce il contratto, usa la caratteristica per soddisfare quel contratto. Buona
Max

13
Mi piace questo tipo di veri programmatori, che propongono un vero esempio di lavoro con una breve descrizione per ciascuno. Thx
Arthur Kushman

1
Cosa succede se invece si utilizza una classe astratta? Sostituendo l'interfaccia e il tratto, si può creare una classe astratta. Inoltre, se l'interfaccia è così necessaria per l'applicazione, la classe astratta può anche implementare l'interfaccia e definire i metodi come ha fatto il tratto. Quindi puoi spiegare perché abbiamo ancora bisogno di tratti?
Sumanchalki,

12
@sumanchalki La classe astratta segue le regole dell'ereditarietà. E se avessi bisogno di una classe che implementa Loggable e Cacheable? Avresti bisogno della classe per estendere AbstractLogger che deve estendere quindi AbstractCache. Ciò significa che tutti i loggable sono cache. Questo è un accoppiamento che non vuoi. Limita il riutilizzo e rovina il grafico dell'eredità.
Gordon,

1
Penso che i collegamenti demo siano morti
Pmpr

19

:) Non mi piace teorizzare e discutere su cosa si dovrebbe fare con qualcosa. In questo caso tratti. Ti mostrerò a cosa trovo utili i tratti e puoi imparare da esso o ignorarlo.

Tratti : sono grandiosi per applicare strategie . I modelli di progettazione strategica, in breve, sono utili quando si desidera che gli stessi dati vengano gestiti (filtrati, ordinati, ecc.) In modo diverso.

Ad esempio, hai un elenco di prodotti che desideri filtrare in base ad alcuni criteri (marchi, specifiche, qualunque cosa) o ordinati con mezzi diversi (prezzo, etichetta, qualunque cosa). È possibile creare un tratto di ordinamento che contiene diverse funzioni per diversi tipi di ordinamento (numerico, stringa, data, ecc.). È quindi possibile utilizzare questo tratto non solo nella propria classe di prodotto (come indicato nell'esempio), ma anche in altre classi che richiedono strategie simili (per applicare un ordinamento numerico ad alcuni dati, ecc.).

Provalo:

<?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>';
?>

Come nota di chiusura, penso a tratti come gli accessori (che posso usare per modificare i miei dati). Metodi e proprietà simili che possono essere tagliati fuori dalle mie lezioni e messi in un unico posto, per una facile manutenzione, codice più breve e più pulito.


1
Sebbene ciò mantenga pulita l'interfaccia pubblica, quella interna potrebbe diventare davvero complessa con questo, specialmente se lo estendi ad altre cose, come ad esempio i colori. Penso che funzioni semplici o metodi statici per migliorare qui.
Sebastian Mach,

Mi piace il termine strategies.
Rannie Ollit,

4

Sono entusiasta di Traits perché risolvono un problema comune durante lo sviluppo di estensioni per la piattaforma di e-commerce Magento. Il problema si verifica quando le estensioni aggiungono funzionalità a una classe principale (ad esempio il modello Utente) estendendola. Questo viene fatto puntando il caricatore automatico Zend (tramite un file di configurazione XML) per utilizzare il modello Utente dall'estensione e fare in modo che il nuovo modello estenda il modello principale. ( esempio ) Ma cosa succede se due estensioni hanno la precedenza sullo stesso modello? Si ottiene una "condizione di gara" e ne viene caricata solo una.

La soluzione in questo momento è modificare le estensioni in modo che una estenda la classe di sostituzione del modello dell'altra in una catena, quindi imposta la configurazione dell'estensione per caricarle nell'ordine corretto in modo che la catena di ereditarietà funzioni.

Questo sistema causa spesso errori e durante l'installazione di nuove estensioni è necessario verificare la presenza di conflitti e modificare le estensioni. Questo è un problema e interrompe il processo di aggiornamento.

Penso che usare Traits sarebbe un buon modo per ottenere la stessa cosa senza che questo fastidioso modello prevalga sulle "condizioni di gara". È vero che potrebbero esserci ancora conflitti se più Tratti implementano metodi con gli stessi nomi, ma immagino che qualcosa come una semplice convenzione dello spazio dei nomi possa risolvere questo per la maggior parte.

TL; DR Penso che i tratti potrebbero essere utili per la creazione di estensioni / moduli / plugin per pacchetti software PHP di grandi dimensioni come Magento.


0

Potresti avere un tratto per l'oggetto di sola lettura come questo:

  trait ReadOnly{  
      protected $readonly = false;

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

È possibile rilevare se viene utilizzato quel tratto e determinare se dovresti scrivere quell'oggetto in un database, in un file, ecc.


Quindi la classe che avrebbe usequesto tratto avrebbe chiamato if($this -> getReadonly($value)); ma questo genererebbe un errore se non hai usequesta caratteristica. Pertanto questo esempio è imperfetto.
Luceos,

Bene, devi prima verificare se il tratto è in uso. Se il tratto ReadOnly è definito su un oggetto, è possibile verificare se è di sola lettura o meno.
Nico,

Ho fatto una dimostrazione generica del concetto per tale tratto in gist.github.com/gooh/4960073
Gordon

3
Dovresti dichiarare un'interfaccia per ReadOnly a tale scopo
Michael Tsang,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.