Traços em PHP - existem exemplos / práticas recomendadas do mundo real? [fechadas]

148

Características foram uma das maiores adições para o PHP 5.4. Conheço a sintaxe e entendo a ideia por trás de características, como a reutilização de código horizontal para coisas comuns, como log, segurança, cache etc.

No entanto, ainda não sei como utilizaria as características de meus projetos.

Existem projetos de código aberto que já usam características? Algum bom artigo / material de leitura sobre como estruturar arquiteturas usando características?

Máx.
fonte
8
Aqui está a minha opinião: um post sobre o assunto que escrevi sobre o assunto. TL; DR: Basicamente, temo que, embora sejam poderosos e possam ser usados ​​para o bem, a maioria dos usos que veremos serão anti-padrões completos e causam muito mais dor do que solucionam ...
ircmaxell
1
Dê uma olhada na biblioteca padrão da scala e você encontrará muitos exemplos úteis de características.
Dmitry

Respostas:

89

Minha opinião pessoal é que, na verdade, existe muito pouca aplicação de características ao escrever código limpo.

Em vez de usar características para hackear código em uma classe, é melhor passar as dependências pelo construtor ou pelos setters:

class ClassName {
    protected $logger;

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

A principal razão pela qual acho que melhor do que usar características é que seu código é muito mais flexível removendo o acoplamento rígido a uma característica. Por exemplo, você pode simplesmente passar uma classe de logger diferente agora. Isso torna seu código reutilizável e testável.

NikiC
fonte
4
Usando características, você também pode usar outra classe de logger, certo? Apenas edite a característica e todas as classes que a utilizam são atualizadas. Corrija-me se eu estiver errado
rickchristie
14
@ Rickchristie Claro, você poderia fazer isso. Mas você precisaria editar o código fonte da característica. Então, você o alteraria para todas as classes que o usassem, não apenas para a classe em que deseja um logger diferente. E se você quiser usar a mesma classe, mas com dois registradores diferentes? Ou se você deseja passar por um mock-logger durante o teste? Você não pode, se usar traços, pode, se usar injeção de dependência.
NikiC 25/10
2
Percebo o seu ponto, também estou pensando se as características valem a pena ou não. Quero dizer, em estruturas modernas como o Symfony 2, você tem injeção de dependência em todo o lugar, o que parece superador de características na maioria dos casos. No momento, vejo características como não muito mais do que "copiar e colar assistido por compilador". ;)
Max
11
No momento, vejo características como não muito mais do que "copiar e colar assistido por compilador". ;) : @Max: Isso é exatamente o que as características foram projetadas para serem, então isso é completamente correto. Torna-se mais "sustentável", uma vez que há apenas uma definição, mas é basicamente apenas c & p ...
ircmaxell
29
O NikiC está perdendo o ponto: o uso de uma característica não impede o uso de Injeção de Dependência. Nesse caso, uma característica deixaria que todas as classes que implementam o log não precisassem duplicar o método setLogger () e a criação da propriedade $ logger. A característica os forneceria. setLogger () digitaria dica no LoggerInterface como no exemplo, para que qualquer tipo de logger possa ser transmitido. Essa ideia é semelhante à resposta de Gordon abaixo (apenas parece que ele está digitando uma superclasse do Logger em vez de uma interface do Logger )
Ethan
205

Acho que seria preciso procurar em idiomas que possuam Traits há algum tempo para aprender as Boas / Melhores práticas aceitas. Minha opinião atual sobre o Trait é que você deve usá-los apenas para códigos que precisaria duplicar em outras classes que compartilham a mesma funcionalidade.

Exemplo para uma característica do criador de logs:

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 então você faz ( demo )

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

Eu acho que o importante a considerar ao usar traços é que eles realmente são apenas pedaços de código que são copiados para a classe. Isso pode facilmente levar a conflitos, por exemplo, quando você tenta alterar a visibilidade dos métodos, por exemplo,

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

O exemplo acima resultará em um erro ( demo ). Da mesma forma, quaisquer métodos declarados na característica que também já estão declarados na classe using não serão copiados para a classe, por exemplo

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

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

imprimirá 2 ( demo ). Essas são as coisas que você deseja evitar, pois dificultam a localização dos erros. Você também deve evitar colocar coisas em características que operam nas propriedades ou métodos da classe que a utiliza, por exemplo,

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

funciona ( demo ), mas agora a característica está intimamente ligada a A e toda a idéia de reutilização horizontal é perdida.

Ao seguir o Princípio de Segregação de Interface, você terá muitas classes e interfaces pequenas. Isso faz da Traits um candidato ideal para as coisas que você mencionou, por exemplo , preocupações transversais , mas não para compor objetos (em um sentido estrutural). No exemplo do Logger acima, a característica é completamente isolada. Não possui dependências de classes concretas.

Poderíamos usar agregação / composição (como mostrado em outro lugar nesta página) para obter a mesma classe resultante, mas a desvantagem de usar agregação / composição é que teremos que adicionar os métodos proxy / delegador manualmente a cada classe, o que deve ser poder fazer logon. As características resolvem isso muito bem, permitindo-me manter o padrão em um só lugar e aplicá-lo seletivamente quando necessário.

Nota: dado que os traços são um novo conceito em PHP, todas as opiniões expressas acima estão sujeitas a alterações. Ainda não tive muito tempo para avaliar o conceito. Mas espero que seja bom o suficiente para lhe dar algo em que pensar.

Gordon
fonte
41
Esse é um caso de uso interessante: use uma interface que defina o contrato, use a característica para satisfazer esse contrato. Um bom.
Max
13
Eu gosto desse tipo de programadores verdadeiros, que propõem exemplos reais de trabalho com descrições curtas para cada um. Thx
Arthur Kushman
1
E se alguém usar uma classe abstrata em vez disso? Substituindo a interface e a característica, pode-se criar uma classe abstrata. Além disso, se a interface é tão necessária para o aplicativo, a classe abstrata também pode implementar a interface e definir os métodos como a característica. Então, você pode explicar por que ainda precisamos de traços?
precisa saber é o seguinte
12
@sumanchalki A classe abstrata segue as regras da herança. E se você precisasse de uma classe que implementasse Loggable e Cacheable? Você precisaria da classe para estender o AbstractLogger, que precisa estender o AbstractCache. Mas isso significa que todos os Loggables são caches. Esse é um acoplamento que você não deseja. Isso limita a reutilização e atrapalha seu gráfico de herança.
Gordon
1
Eu acho que as ligações de demonstração estão mortos
PMPR
19

:) Não gosto de teorizar e debater sobre o que deve ser feito com alguma coisa. Nesse caso, traços. Mostrarei a você o que acho útil para as características e você pode aprender com ela ou ignorá-la.

Traços - eles são ótimos para aplicar estratégias . Em resumo, os padrões de design da estratégia são úteis quando você deseja que os mesmos dados sejam manipulados (filtrados, classificados etc.) de maneira diferente.

Por exemplo, você tem uma lista de produtos que deseja filtrar com base em alguns critérios (marcas, especificações, qualquer que seja) ou classificados por diferentes meios (preço, etiqueta, qualquer que seja). Você pode criar uma característica de classificação que contenha funções diferentes para diferentes tipos de classificação (numérico, sequência, data etc.). Em seguida, você pode usar essa característica não apenas na sua classe de produto (conforme fornecido no exemplo), mas também em outras classes que precisam de estratégias semelhantes (para aplicar uma classificação numérica a alguns dados, etc.).

Tente:

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

Como observação final, penso em características como acessórios (que posso usar para alterar meus dados). Métodos e propriedades semelhantes que podem ser cortados das minhas aulas e colocados em um único local, para manutenção fácil, código mais curto e limpo.

D. Marti
fonte
1
Embora isso mantenha a interface pública limpa, a interna pode se tornar realmente complexa com isso, especialmente se você estender isso para outras coisas, como cores, por exemplo. Eu acho que funções simples ou métodos estáticos para melhorar aqui.
Sebastian Mach
Eu gosto do termo strategies.
Rannie Ollit 17/09/19
4

Estou empolgado com o Traits, porque eles resolvem um problema comum ao desenvolver extensões para a plataforma de comércio eletrônico Magento. O problema ocorre quando as extensões adicionam funcionalidade a uma classe principal (como o modelo do usuário) estendendo-a. Isso é feito apontando o carregador automático do Zend (por meio de um arquivo de configuração XML) para usar o modelo de usuário da extensão e fazer com que esse novo modelo estenda o modelo principal. ( exemplo ) Mas e se duas extensões substituírem o mesmo modelo? Você obtém uma "condição de corrida" e apenas uma é carregada.

A solução agora é editar as extensões para que uma amplie a classe de substituição de modelo da outra em uma cadeia e, em seguida, defina a configuração da extensão para carregá-las na ordem correta para que a cadeia de herança funcione.

Esse sistema freqüentemente causa erros e, ao instalar novas extensões, é necessário verificar se há conflitos e editar extensões. Isso é doloroso e interrompe o processo de atualização.

Eu acho que usar Traits seria uma boa maneira de realizar a mesma coisa sem esse modelo irritante substituir a "condição de corrida". É verdade que ainda pode haver conflitos se várias características implementarem métodos com os mesmos nomes, mas eu imagino que algo como uma simples convenção de namespace possa resolver isso na maior parte do tempo.

TL; DR Acho que o Traits pode ser útil para criar extensões / módulos / plugins para grandes pacotes de software PHP como o Magento.

thaddeusmt
fonte
0

Você poderia ter uma característica para um objeto somente leitura como este:

  trait ReadOnly{  
      protected $readonly = false;

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

Você pode detectar se essa característica é usada e determinar se deve ou não gravar esse objeto em um banco de dados, arquivo etc.

Nico
fonte
Portanto, a classe que useessa característica chamaria if($this -> getReadonly($value)); mas isso geraria um erro se você não tivesse useessa característica. Portanto, este exemplo é falho.
Luceos 23/05
Bem, você precisa verificar se a característica está em uso primeiro. Se a característica ReadOnly estiver definida em um objeto, você poderá verificar se é somente leitura ou não.
Nico
Fiz uma prova genérica de conceito para essa característica em gist.github.com/gooh/4960073
Gordon
3
Você deve declarar uma interface para ReadOnly para o efeito
Michael Tsang