Antes de iniciarmos o tema central do artigo, vamos dar uma passadinha no assunto de herança, pois é comum vermos a associação de uma coisa com a outra.
Na herança as implementações e as especificações descem na “árvore”:
Representando isso em código, temos:
class Animal
{
// TODO
}
class Gato extends Animal
{
// TODO
}
class Coelho extends Animal
{
// TODO
}
class Cachorro extends Animal
{
// TODO
}
Herança é normalmente utilizada para se atingir o reuso de código. Mas, em essência, ela não é sobre reuso. É sobre definir uma hierarquia estrita de tipo. Reuso é a consequência de se usar herança. Quando se pensa em herança apenas com o objetivo de reuso, tendemos a cometer algumas distorções conceituais.
Antes de continuar a leitura desse artigo, recomendo fortemente a leitura de outro artigo aqui do blog: “Devo usar herança ou composição?”. Ele explica em detalhes o que é herança e quando faz sentido utilizá-la.
Traits
Traits, por outro lado, são pura e simplesmente para reuso de código, que pode nos auxiliar nos casos em que fatalmente precisamos cair no comportamento de “copiar e colar” determinados trechos de códigos em diferentes classes.
Alguns autores denominam que Traits são um mecanismo de “herança horizontal”, que seria o contrário da “herança vertical” (a herança clássica, que envolve hierarquia e tipo, comentado anteriormente). Mas, na realidade, se levarmos para um entendimento mais estrito, Traits não possuem nenhuma relação com herança, ademais, elas não se tornam parte e tipo da hierarquia da classe. Entender Traits como “herança horizontal” facilita o entendimento (não é pecado), mas não é conceitualmente fiel.
Então, na prática, Traits podem ser úteis para que compartilhemos determinadas funcionalidades (códigos) entre classes que não são relacionadas (de hierarquias/tipos diferentes).
Curso PHP Avançado
Conhecer o cursoSupondo que estamos trabalhando com duas classes que precisam de um método slug()
para a montagem de URL a partir de um atributo nome
:
class Curso
{
public function slug(): string
{
return strtolower(
preg_replace('/[^A-Za-z0-9-]+/', '-', $this->nome)
);
}
}
class Formacao
{
public function slug(): string
{
return strtolower(
preg_replace('/[^A-Za-z0-9-]+/', '-', $this->nome)
);
}
}
A gente já consegue identificar logo de cara o problema da duplicação de código. O método slug()
é o mesmo nas duas classes.
A primeira solução imaginada quando se dá muita importância para o reuso de código oferecido pela herança, seria criar uma classe base, por exemplo:
class BaseClassWithSlug
{
public function slug(): string
{
return strtolower(
preg_replace('/[^A-Za-z0-9-]+/', '-', $this->nome)
);
}
}
class Curso extends BaseClassWithSlug
{
//
}
class Formacao extends BaseClassWithSlug
{
//
}
Você já deve ter notado que isso não é uma boa ideia. Resolvemos o problema da duplicação, mas criamos outro: herança só faz sentido quando se tem a necessidade de representar um tipo. Se a nossa classe não é do tipo da outra, herança não deve ser utilizada. Quando estendemos uma classe, não herdamos apenas a sua implementação e sua especificação, herdamos também o seu tipo. Uma classe herdada passa a ser do tipo da outra e, a partir dela, novos sub-tipos podem ser compostos.
Essa é uma opção que a grosso modo funciona, mas que nos leva a muitas inconsistências, mais que isso, pode nos levar a um ciclo vicioso de sempre criar uma classe base para ser estendida no intuito de evitar duplicação de código. Como já foi comentado no artigo indicado, composição também tem um grande mecanismo de reuso e deve ser considerado preferencialmente em detrimento à herança (para grande parte dos casos).
O que precisamos nesse caso é uma forma de extrair essa funcionalidade e importar nas classes que precisam dela. É aí que entra um possível caso de uso de Trait:
trait SlugNome
{
public function slug(): string
{
return strtolower(
preg_replace('/[^A-Za-z0-9-]+/', '-', $this->nome)
);
}
}
class Curso
{
use SlugNome;
}
class Formacao
{
use SlugNome;
}
Resolvemos de uma forma um pouco mais elegante, sem precisar definir um tipo genérico e fazê-lo ser herdado. Uma Trait é importada para uma classe usando a sintaxe use
. Mais de uma Trait pode ser importada a partir da mesma declaração use
separando os nomes por vírgulas ou através de mais de uma declaração use
. Instanciando tais objetos você conseguirá acessar o método slug()
.
As opções abaixo de importação são equivalentes:
class TestClass
{
use TraitOne, TraitTwo;
}
// Ou
class TestClass
{
use TraitOne;
use TraitTwo;
}
Mas nem tudo são flores. Quando uma classe passa a usar uma Trait, você precisa garantir que tudo o que for importado vai funcionar corretamente. Por exemplo, a nossa Trait precisa de um atributo nome
para funcionar. Uma possível solução para evitar bugs ao utilizá-la em outras classes é validar se o atributo nome
existe, caso contrário, lançar uma exceção:
trait SlugNome
{
public function slug(): string
{
if ( ! property_exists($this, 'nome')) {
throw new RuntimeException('Faltando o atributo nome para o retorno do slug.');
}
return strtolower(
preg_replace('/[^A-Za-z0-9-]+/', '-', $this->nome)
);
}
}
class Curso
{
use SlugNome;
}
class Formacao
{
use SlugNome;
}
Ou, claro, talvez você tenha bons testes para garantir que todas as classes que importem essa Trait funcionem corretamente.
Uma classe pode importar quantas Traits precisar e as Traits possuem acesso aos membros privados da classe, o que pode ser bom ou ruim, dependendo do ponto de vista de quanto a Trait interage com ela.
Curso Laravel - Fundamentos
Conhecer o cursoConcluindo
Frameworks como Symfony e Laravel, principalmente esse último, fazem um uso bem relevante de Traits, com destaque para a parte que toca testes unitários.
O Laravel, além do caso de uso padrão, tem outro para as Traits, que é o de organizar melhor o código dos seus componentes. Por exemplo, no componente Http
tem um namespace chamado Concerns onde o framework extraiu para Traits comportamentos que estavam todos embutidos na classe Request
, pois estavam tornando-a muito grande e de difícil manutenção. Esse tipo de uso (com o único intuito de organizar melhor o código) deve ser a exceção e não a regra em nossos projetos, senão caímos na “armadilha” de ver as Traits como solução para outro problema: classes “gordas” demais, com muitas responsabilidades que poderiam ser melhor decompostas. No final, é sempre uma questão de bom senso.
Como você deve ter percebido, o objetivo desse artigo não foi o de masterizar o uso de Traits, todas as sintaxes possíveis e tudo mais. Outros detalhes podem ser vistos na própria documentação oficial. O que fizemos aqui foi uma reflexão do que normalmente acontece no desenvolvimento e sobre como Traits podem nos ajudar em alguns casos. Lembrando que nada é solução completa e perfeita para tudo. A comunicação entre objetos, composição, padrões, herança etc, é a integração de todas essas coisas no momento “adequado” que faz a “mágica” de uma aplicação orientada a objetos acontecer.
Até a próxima!