Em seu livro “Style Guide for Object Design”, Matthias Noback propõe um template muito interessante (mesmo que trivial, em certa perspectiva) para a implementação de métodos, que é:
Curso BDD - Testes Guiados por Comportamento com Behat PHP
Conhecer o curso[scope] function methodName(type name, ...): void|[return-type]
{
1) [pre-conditions checks]
2) [failure scenarios]
3) [happy path]
4) [post-condition checks]
5) [return void|specific-return-type]
}
Apesar de os exemplos aqui serem escritos em PHP (usando a sintaxe do PHP 7.4), não importa qual linguagem você utiliza, esse modelo e os insights que ele pode gerar são aplicáveis de forma genérica em qualquer linguagem que suporte orientação a objetos.
Na etapa de “Pre-condition checks” devemos verificar se os argumentos recebidos estão corretos, se chegou o que se espera receber. É uma etapa essencial, principalmente quando estamos lidando com dados primitivos (o que é bastante comum em linguagens dinâmicas). É nela que realizamos verificações óbvias e simples, por exemplo, se o valor inteiro a ser recebido tem que ser maior que zero, se o array recebido só possui instâncias de uma determinada classe etc.
Supondo que temos um command method de um service object para disparar o envio de um e-mail. Ele precisa receber o e-mail para o qual se dará o envio. Seguindo a recomendação de pre-condition check faríamos assim:
public function sendEmail(string $email) : void
{
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
throw new InvalidArgumentException('E-mail inválido.');
}
}
Podemos simplificar essa verificação usando alguma assertion library. Por exemplo, usando a assert:
public function sendEmail(string $email) : void
{
Assertion::email($email, 'E-mail inválido.');
}
Não passando, uma exceção do tipo InvalidArgumentException é lançada. O legal desse tipo de library é que já possuem diversas validações e deixam nossas classes menos verbosas (ademais, tais validações também são comuns e úteis nos construtores das classes).
Outro exemplo muito interessante, principalmente no caso do PHP que não implementa uma forma de receber num argumento uma lista de objetos de uma determinada classe:
public function fromEvents(array $events) : void
{
Assertion::allIsInstanceOf($events, EventInterface::class);
}
Essa validação visa garantir que todos os itens do array sejam instâncias de EventInterface. Sem a facilidade da library, o método ficaria mais verboso:
public function fromEvents(array $events) : void
{
foreach ($events as $event) {
if ( ! $event instanceof EventInterface) {
throw new InvalidArgumentException('Class X was expected to be instanceof of Y but is not.');
}
}
}
Voltando ao exemplo do método sendEmail()
, se você implementa algum Value Object em seu projeto, essa etapa de validação do dado estará no construtor dele, não precisando ser feita no método que irá recebê-lo como argumento. Então, vamos supor que no nosso projeto ao invés de recebermos o e-mail como um tipo primitivo, passássemos a recebê-lo como um Value Object:
final class EmailAddress
{
private string $email;
public function __construct(string $email)
{
Assertion::email($email);
$this->email = $email;
}
// ...
}
class SomeService
{
public function sendEmail(EmailAddress $email) : void
{
// ...
}
}
$service = new SomeService();
$service->sendEmail(new EmailAddress('user@domain.com'));
Note que, nesse modelo, nem precisamos fazer a checagem do que estamos recebendo, pois temos a garantia de que estamos recebendo um endereço de e-mail válido.
Curso Laravel - Service Container, Service Provider e Facades
Conhecer o cursoMesmo que o dado recebido tenha passado na primeira checagem, o uso dele pode nos levar à condições de falha. No caso do exemplo acima, só validar o e-mail não garante que o envio dele se dará com sucesso. Alguma condição externa pode influenciar no insucesso da operação. É aqui que entra a etapa de “Failure scenarios”. E alguns métodos costumam fazer muito mais o uso dessa etapa mais que todas as outras.
Supondo que temos um Repository que precisa retornar os dados de um usuário a partir do ID dele:
class UserRepository
{
public function findById(int $id) : array
{
Assertion::greaterThan($id, 0, 'O ID precisa ser maior que zero.');
$row = $this->database->where('id', $id)->first();
if (null === $row) {
throw new RuntimeException("Não foi possível encontrar o usuário de id {$id}");
}
return $row;
}
}
Primeiro, o método verifica se recebeu um inteiro positivo maior que zero, caso contrário uma InvalidArgumentException é lançada. Posteriormente, ele verifica se o registro foi encontrado na base de dados e, não sendo, uma exceção do tipo RuntimeException é lançada (ademais, o problema agora não é relacionado à validez do argumento recebido, uma condição externa ao método que nos levou a ela).
Outro hipotético exemplo:
public function sendEmail(string $email) : void
{
Assertion::email($email, 'E-mail inválido.');
$mail = new Mailer();
$mail->setHtml('body');
$result = $email->send($email);
if ($result === false) {
throw new RuntimeException('Houve um erro ao enviar o e-mail');
}
}
Ao chegar na etapa “happy path”, sabemos que está tudo ocorrendo bem na execução do método. O argumento é válido e nenhuma condição externa nos levou à exceções. É nessa parte que podemos desenvolver alguma operação ou, na maioria das vezes, nem se faz muita coisa nela. É muito comum os métodos de consulta (query methods) serem simples o suficiente ao ponto de já retornarem o que se propõem a retornar e no caso dos métodos de comando (command methods) grande parte do corpo deles se passam nas verificações dos cenários de falhas (failure scenarios).
Na etapa de “Post-condition checks” podemos verificar se o método realmente concluiu o seu propósito. Se for um método de consulta, o valor a ser retornado primeiro pode ser avaliado, por exemplo. Na prática, a maioria dos métodos nem precisam desse tipo de verificação, testes unitários conseguem suprir a previsibilidade do retorno da execução de um método e o tipo retornado (principalmente se você usa strict types para argumentos e retornos).
Um exemplo genérico e prototipado:
public function someMethod(int $someValue) : int
{
// 1) [pre-conditions checks]
// 2) [failure scenarios]
$result = 0; // 3) [happy path]
Assertion::greaterThan(0, $result); // 4) [post-condition checks]
return $result; // 5) [return]
}
Em linhas gerais, as recomendações na construção de métodos são um equilíbrio entre: se você encontrou um cenário falho, lance uma exceção o mais rápido possível. Se você já tem o valor a ser retornado, retorne-o o mais rápido possível, para evitar o aumento da complexidade do método ou até mesmo para não dar chance de alterar erroneamente esse valor antes de retorná-lo.
Se o modelo acima deve ser seguido fielmente? Não, na maioria dos casos 1, 2 ou 3 etapas acima são suficientes para se construir um método. Em vários outros cenários você pode decidir primeiro focar em fazer o retorno o mais rápido possível e lançar uma exceção em caso contrário. Por exemplo:
class ValidateSignature
{
public function handle(Request $request, Closure $next) : Response
{
if ($request->hasValidSignature()) {
return $next($request);
}
throw new InvalidSignatureException;
}
}
Vai depender da sua sensibilidade. O ideal é um equilíbrio entre um método bem desenvolvido, que cheque bem as informações que recebe, que lance exceções o mais rápido quando as coisas claramente não estão indo bem, mas que também não fique enorme e difícil de ser lido. Muitas vezes sacrificar a legibilidade por etapas desnecessárias pode não ser tão frutífero.
Até a próxima!