Este artigo é uma continuação do anterior. Para relembrarmos, abaixo a lista dos assuntos e em negrito os que serão abordados nesse artigo:
- Um nível de indentação por método
- Não use ELSE
- Envolva seus tipos primitivos
- Envolva suas collections em classes
- Uma chamada de método por linha
- Não abrevie
- Mantenha as classes pequenas
- Não tenha classes com mais de duas variáveis de instância
Curso PHP Avançado
Conhecer o curso3. Envolva seus tipos primitivos
Podemos definir esse exercício para os tipos escalares em PHP que são: int, float, bool e string. “Envolver” vem de um significado da programação orientada a objetos, que quer dizer, colocar o tipo “envolta” de uma classe, a fim de trazer mais resultados e funcionalidades do que um tipo comum/escalar.
Essa técnica vêm de uma aplicação do DDD (Domain-Driven Design) chamada de Value Object, onde temos um objeto-valor pequeno, que irá cuidar de um tipo de dado específico. Como o PHP é fracamente tipado, a melhor aplicação será em passagens de parâmetros de métodos ou funções. Veja o código abaixo:
class Customer
{
protected $name;
protected $birthday;
public function __construct(string $name, string $birthday)
{
// Validar aqui???
$this->name = $name;
$this->birthday = $birthday;
}
}
Ambos parâmetros são validáveis e não é legal validarmos no construtor da classe, pelo fato de não podermos reaproveitar as validações. Já o fato de forçarmos os atributos como string na entrada, algo errado pode acontecer se o desenvolvedor que está usando a classe não souber, por exemplo, qual padrão de data utilizado para a entrada $birthday, o que pode gerar um problema lá na frente, possivelmente no banco de dados.
Abaixo um exemplo de bom e outro de mau uso da classe:
// Programador que conhece a classe
$customer = new Customer('John Doe', '1983-02-10');
// Programador que não conhece a classe
// pode gerar um 0000-00-00 no Database
$customer = new Customer('John Doe', '10/02/1983');
Poderíamos então usar duas classes que farão envolvimento no tipo string, por exemplo:
- CustomerName: que cuidará de validação de nome de cliente, pode verificar tamanho, fazer trim, e até limpeza.
- CustomerBithday: Mais importante que o nome, ela vai validar o formato da data ou até mesmo formatar a entrada como, por exemplo, converter 10/02/1983 para 1983-02-10, evitando assim um problema de inconsistência, lembrando que não precisa ser necessariamente uma classe e, seguindo o princípio de inversão de dependência, podemos facilmente trabalhar com interfaces seguindo estratégias.
Como ficaria após a implementação dessas classes:
class Customer
{
protected $name;
protected $birthday;
public function __construct(CustomerName $name, CustomerBirthday $birthday)
{
$this->name = $name;
$this->birthday = $birthday;
}
}
Usando:
// Programador que conhece a classe
$customer = new Customer(
new CustomerName('John Doe'),
new CustomerBirthday('1983-02-10')
);
// A data será formatada internamente
$customer = new Customer(
new CustomerName('John Doe'),
new CustomerBirthday('10/02/1983')
);
Um possível problema dessa abordagem é que ela adiciona complexidade à base de código. Na tradução dos Object Calisthenics, é colocado que todos os tipos primitivos devem ser envolvidos em classes, porém, sabemos que em PHP isso pode se tornar improdutivo e desnecessário, portanto, analise o quanto aquela entrada ou tipo pode sofrer mudança, se precisa de validação, normalização etc, só aplique-o se tiver uma real justificativa.
4. Envolva suas collections em classes
Semelhante ao exercício anterior, devemos envolver nossas coleções. Isso significa que trabalhar com um CustomerList é melhor do que com um array, neste caso, o uso dará uma melhor flexibilidade para o tratamento da coleção.
Abaixo uma usabilidade com e outra sem coleção em classe:
// A lógica fica fora, o que pode trazer problemas futuros
foreach ($customers as $customer) {
if ($customer->isGoldAccount()) {
$customer->addBonus(new Money('R$ 50,00'));
}
}
Nesse exemplo queremos adicionar um bônus aos clientes do tipo gold. $customers é um array e por isso para modificar a coleção, precisamos iterá-la com um foreach e ainda internamente verificar se o tipo do cliente é gold.
Se usarmos uma classe que envolve a coleção, ou seja, uma CustomerCollection ela poderá ter 2 métodos:
- Para filtragem de tipos de clientes: filterGoldAccounts.
- Para adicionar aos clientes o bônus: addBonus.
A usabilidade ficaria assim:
$customersCollection = new CustomersCollection; // Classe com Lazy Loading
// Filtramos os clientes de conta Gold
$goldCustomers = $customersCollection->filterGoldAccounts();
// Adicionamos pela collection o bonus de R$ 50,00
// filtrado pela classe de coleção
$goldCustomers->addBonus(new Money('R$ 50,00'));
// Por fim persistimos
$goldCustomers->persists();
Assim, temos coleções que são específicas e inteligentes o suficiente para melhorar a usabilidade e evitar erros de programação. No PHP temos um conjunto de classes padrão para lidar com listas, a SPL(Standard PHP Library) que tem uma sessão dedicada a iteradores.
5. Uma chamada de método por linha
Devemos sempre fazer uma chamada de método por linha, não se aplicando à bibliotecas que usam do padrão Method Chaining ou DSL(Domain Specific Language).
Para seguir esse exercício não devemos, por exemplo, ao desenvolver um conjunto de Models, relacioná-los com métodos em cadeia, isso pode ser uma péssima ideia, segue um exemplo:
$customer->getById(55988)
->getPurchase(18376)
->getProducts()
->filterById(234);
Queremos resgatar um produto do pedido de um cliente, pode-se parecer muito prático, porém, alguns problemas poderão ocorrer e é muito difícil testar um bloco desses. Como saber se getPurchase encontrou o pedido? E se não encontrou, o que acontece? Nesse caso, vem outra problemática: e se o pedido não contém itens ainda? E temos um retorno null, certamente teremos um erro de método não encontrado.
Por isso, para garantir que tudo ocorreu certo, podemos seguir a Lei de Demeter, ela diz que devemos somente conversar com classes próximas, então, criamos um método para conversar e filtrar o que precisamos, ao invés de percorrer pelos Models que estão distantes. Não focaremos na implementação, porém, o conceito de uso abaixo pode ilustrar essa aproximação:
// Resgatando o model isoladamente
$customer = $customerModel->getById(55988);
// Aproximação: método que pertence a Customer
// sua implementação cuidará de retornos nulls
$product = $customer->getPuchasedProduct(18376, 234);
O principal objetivo desse exercício é não sair percorrendo por objetos retornados em chamadas de métodos, usar chamadas de vários métodos em linha pode gerar muitos problemas de manutenção, dificuldade de entendimento e testes mal escritos. Costuma-se dizer que gera um código que “cheira mal”.
Conclusão
Esses exercícios são mais aprofundados e devem ser estudados com calma, não devemos seguí-los somente por que parece ser o certo, devemos entender a motivação por trás deles, lembrando que nenhuma dessas técnicas são balas de prata e vão servir para toda modelagem, o bom senso é a melhor direção.
Até o próximo artigo da série!