DEV Community

Daniel Reis for He4rt Developers

Posted on

Criando Exceptions para impressionar no Teste Técnico

Exceptions sempre vai ser um assunto constante quando o tópico for Orientação à Objetos e hoje vamos descobrir como criá-las como um artesão de software!

Tabela de Conteúdo

1. Prólogo

Quando eu comecei a estudar programação, um dos assuntos que sempre me assustava eram "erros" ou qualquer coisa que se relaciona à isso, porém após começar a estudar com mais frequencia, eu entendi que os erros e/ou Exceptions são muito mais amigas do que inimigas. Mas é claro que pra isso, você precisa entender como utilizar de um jeito interessante pro seu projeto.

No meu caso, acabava usando o trecho throw new Exception() pra literalmente qualquer coisa e me perdia facilmente na codebase, por conta de uma Exception genérica espalhada no meio de tantas outras. No início não tinha problema, eu ainda não trabalhava com time que tinha observabilidade, então tá tudo certo.

Passado o tempo, entrei em mais empresas FODAS e me deparei com excelentes implementações de Exceptions, principalmente o Factory Pattern. Esse método me deixou maravilhado em como as coisas podem ser simples e elegantes, mesmo quando se trata de erros.

E hoje vou mostrar pra vocês como criar um certo gosto em escrever exceções elegantes pra não encher seu código com 2km de mensagem de erro dentro da regra de negócio.

2. O que gostariamos de EVITAR

Vamos começar dando um pouco de contexto para esse tutorial: imagine que você tá desenvolvendo um sistema de RPG e nele você precisa criar um inventário simples pro seu personagem.

src
├── Item
│   └── Item.php
└── Player
    ├── Inventory.php
    └── Player.php
Enter fullscreen mode Exit fullscreen mode

Dentro desse contexto, imagine que você está tentando equipar um item no seu personagem. Porém, é lógico que nós vamos colocar algumas regras de validação com suas devidas Exceptions.

namespace DanielHe4rt\Player;

use DanielHe4rt\Item\Item;

class Player {

    public function __construct(
        public string $username,
        public int $level,
        protected Inventory $inventory,
    ) {

    }

    public function equipItem(Item $item): void
    {

        if ($this->inventory->hasItem($item)) {
            throw new \Exception(
                'Você não possui o item "' . $item->name . '". '
            );
        }

        if ($item->minLevel > $this->level) {
            throw new \Exception(
                'Você não pode equipar o item ' . $item->name . 'pois o nível minimo é ' . $item->minLevel 
            );
        }

        $this->setupItem($item);
    }


    private function setupItem(Item $item): void 
    {
        // faça coisas iradas
    }
}
Enter fullscreen mode Exit fullscreen mode

Vimos que existem duas regras de validação que jogam exceções diferentes pro nosso cliente. E pasmem: isso funciona (num cenário que o código tá completinho) e cumpre o papel de validação... MASSSSS, depois que eu aprendi que em testes de emprego o que é visto é a QUALIDADE DA ENTREGA e não a agilidade que foi criado, meu mundo deu uma leve mudada para entender como transformar coisas que parecem "estranhas e feias" em coisas "simples e elegantes".

Nesse caso, eu gostaria muito de evitar duas coisas:

  • Exceptions genéricas;
  • Exceptions que tomam conta de locais para regra de negócio.

Não entenda errado, as exceptions vão continuar onde elas estão, porém vamos melhorar a legibilidade do código.

3. Refatoração 1 : Criando Exceptions

src
├── Item
│   └── Item.php
└── Player
    ├── Exceptions
    │   ├── PlayerException.php
    │   └── PlayerInventoryException.php
    ├── Inventory.php
    └── Player.php
Enter fullscreen mode Exit fullscreen mode

Beleza, agora passamos para parte que criamos nossa primeira Exception "customizada", onde só estendemos a Exception base para uma nova classe. Não é nada de outro mundo, mas já melhora nossa legibilidade e entendimento do código em alguns vários pontos.

namespace DanielHe4rt\Player; 

class PlayerException extends \Exception
{}

class PlayerInventoryException extends \Exception
{}
Enter fullscreen mode Exit fullscreen mode

E faremos uma refatoração simples na nossa função equipItem(), reposicionando as exceptions padrões pela nova exception que criamos.


namespace DanielHe4rt\Player;

use DanielHe4rt\Item\Item;
use DanielHe4rt\Player\PlayerException;
use DanielHe4rt\Player\PlayerInventoryException;

class Player {

    public function __construct(
        public string $username,
        public int $level,
        protected Inventory $inventory,
    ) {

    }

    public function equipItem(Item $item): void
    {
        if (!$this->inventory->hasItem($item)) {
            throw new PlayerInventoryException(
                'Você não possui o item "' . $item->name . '". '
            );
        }

        if ($item->minLevel > $this->level) {
            throw new PlayerException(
                'Você não pode equipar o item ' . $item->name . 'pois o nível minimo é ' . $item->minLevel 
            );
        }

        $this->setupItem($item);
    }


    private function setupItem(Item $item): void 
    {
        // faça coisas iradas
    }
}
Enter fullscreen mode Exit fullscreen mode

Com as novas Exceptions, agora sabemos exatamente do que se trata e principalmente onde buscar na nossa codebase quando essa Exception estourar. É literalmente um CTRL + ALT + F e pesquisar o nome "PlayerInventoryException". Facilita sua vida, a vida do DevOps que vai meter isso num NewRelic/DataDog da vida e assim seguimos.

Porém algo ainda me incomoda muito... Por quê essas mensagens gigantes estão no meio da regra de negócio? Misturar pt-br com en desse jeito é triste d+ pra mim desculpa amigos!! Vamos aprender um jeito de por isso debaixo dos panos, porém antes precisamos passar num tópico de Design Pattern chamado Factory!

4. Design Patterns: Factory Pattern

Se você já ouviu falar de Design Patterns, provavelmente já entende um pouco sobre o que isso resolve. Mas caso não, eu te explico!

"Design Patterns são soluções genéricas para problemas genéricos." - Alguém por ai

Nessa ideia de problemas genéricos, uma galera se reuniu e começou a criar alguns principios de Design de Software pra você resolver problemas do dia a dia com uma certa agilidade. Os Design Patterns são divididos em três tipos:

  • Padrões Comportamentais;
  • Padrões Criacionais;
  • Padrões Estruturais.

e você pode ler mais sobre eles no site https:/refactoring.guru e eu recomendo MUITO pra qualquer pessoa desenvolvedora explorar essa documentação e se auto desenvolver. Ok, mas vamos focar nele, o tal do Criacional de Fábrica (ou Factory Method).

A ideia desse padrão é você criar objetos sem ter que instaciar mil coisas em classes diferentes, você literalmente fabricar alguma instância de algo e só receber numa chamada simples de alguma função. No mundo da programação existem centenas de milhões de chamadas como Models::make(), Exception::create(), ApiQualquer::factory() pra você não ter que acessar o método construtor de uma respectiva classe.

Dando o exemplo de um Client de API, onde deixamos o construtor modular pra caso precisemos trocar a chave e segredo PORÉM ainda damos a possibilidade de uma chamada rápida fabricando o objeto final:


class GithubClient {

    public function __construct(string $clientId, string $clientSecret) 
    {
        $this->client = new Client([
            'clientId' => $clientId,
            'clientSecret' => $clientSecret,
        ]);
    }

    public static function make(): self
    {
        return new self(
            env('github.client_id'),
            env('github.client_secret'),
        );
    }

    public function getUserByUsername(string $username = 'danielhe4rt'): array 
    {
        // faça uma chamada pro github..
    }
}

// Chamando sem fabricar o objeto

$client = (new GithubClient('client-id-foda', 'client-secret-foda'))
    ->getUserByUsername('danielhe4rt');

// Chamando usando o Factory 

$client = GithubClient::make()
    ->getUserByUsername('danielhe4rt');

Enter fullscreen mode Exit fullscreen mode

Nós fizemos uma chamada estática fabricando todos os parâmetros de um jeito sucinto. Esse "make/factory" ou o que você quiser chamar, pode ser um método bem extenso dependendo do que você for injetar mas isso não chega a ser problema.

Mas de qualquer forma, vimos que a legibilidade usando o Factory Pattern foi melhorada, claro que você pode colocar melhores nomes pras funções mas na base é isso. Agora voltemos para nossas exceptions!

5. Refatoração 2: Refinando as Exceptions

Show, aprendemos um pouquinho sobre o factory, agora vamos aplicar.

Criaremos um método de factory para nossa exception que faça sentido com o contexto do que tá acontecendo. Pois é, nada de usar "make" ou "create" nesses momentos. Exception precisam contar minimamente uma história pro usuário ou pro desenvolvedor do quê tá acontecendo e vamos focar nisso.

Depois de uma leve refatoração na nossa PlayerInventoryException, temos o resultado de:

class PlayerInventoryException extends \Exception 
{
    public static function itemNotFound(string $itemName): self
    {
        $message = sprintf('Você não possui o item "%s".', $itemName);
        return new self(
            message: $message,
            code: 403 // Forbidden
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

E após chamarmos essa factory em nosso código, podemos perceber uma melhora de leitura e mantenibilidade já que isolamos as informações da Exception dentro da mesma.

public function equipItem(Item $item): void
{

    if (!$this->inventory->hasItem($item)) {
        throw PlayerInventoryException::itemNotFound($item->name);
    }

    if ($item->minLevel > $this->level) {
        throw new PlayerException(
            'Você não pode equipar o item ' . $item->name . 'pois o nível minimo é ' . $item->minLevel 
        );
    }

    $this->setupItem($item);
}
Enter fullscreen mode Exit fullscreen mode

Agora refatorando a próxima, temos a mesma ideia de trocar a PlayerException.

class PlayerException extends \Exception 
{
    public static function lowLevelForThisEquipment(string $itemName, int $itemLevel): self
    {

        $message = sprintf(
            'Você não pode equipar o item %s pois o nível minimo é %s.',
            $itemName,
            $itemLevel
        );

        return new self(
            message: $message,
            code: 403 // Forbidden
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

e agora nossa equipItem() tá ó, uma maravilha!

public function equipItem(Item $item): void
{
    if (!$this->inventory->hasItem($item)) {
        throw PlayerInventoryException::itemNotFound($item->name);
    }

    if ($item->minLevel > $this->level) {
        throw PlayerException::lowLevelForThisEquipment($item->name, $item->minLevel);
    }

    $this->setupItem($item);
}
Enter fullscreen mode Exit fullscreen mode

Tá uma maravilha? Tá. Mas ainda tem algo me incomodando... Por quê passar os tipos primitivos sendo que essas exceptions estão se "comunicando" com classes?

Ficaria bem mais limpo se passarmos a referência do objeto inteiro pra Exception e lá dentro ela resolve o que precisa usar. Afinal, vai que precisamos de mais algo num futuro e não custa nada já deixar bonitinho, né?

namespace DanielHe4rt\Player;

use DanielHe4rt\Item\Item;

class PlayerException extends \Exception 
{
    public static function lowLevelForThisEquipment(Item $item): self
    {

        $message = sprintf(
            'Você não pode equipar o item %s pois o nível minimo é %s.',
            $item->name,
            $item->minLevel
        );

        return new self(
            message: $message,
            code: 403 // Forbidden
        );
    }
}

class PlayerInventoryException extends \Exception 
{
    public static function itemNotFound(Item $item): self
    {

        $message = sprintf('Você não possui o item "%s".', $item);

        return new self(
            message: $message,
            code: 403 // Forbidden
        );
    }
}

Enter fullscreen mode Exit fullscreen mode

E o resultado final do nosso método fica só o charme, tendo exceptions encapsuladas e ainda vai te gerar ótimos feedbacks na sua entrevista de emprego.

namespace DanielHe4rt\Player;

use DanielHe4rt\Item\Item;
use DanielHe4rt\Player\PlayerException;
use DanielHe4rt\Player\PlayerInventoryException;

class Player {

    public function __construct(
        public string $username,
        public int $level,
        protected Inventory $inventory,
    ) {

    }

    public function equipItem(Item $item): void
    {
        if (!$this->inventory->hasItem($item)) {
            throw PlayerInventoryException::itemNotFound($item);
        }

        if ($item->minLevel > $this->level) {
            throw PlayerException::lowLevelForThisEquipment($item);
        }

        $this->setupItem($item);
    }


    private function setupItem(Item $item): void 
    {
        // faça coisas iradas
    }
}
Enter fullscreen mode Exit fullscreen mode

6. Conclusão

Exceptions são de longe uma das coisas mais "chatas" de se lidar. Afinal ninguém quer erro estourando na tela do cliente, mas no geral elas só precisam ter uma boa escrita e adicionar um pouquinho de charme com chamadas estáticas e PLAU tu ganha um elogio e ponto positivo na entrevista de emprego.

Espero que vocês tenham curtido o conteúdo e não esqueça de me seguir nas redes sociais!

Referência: Formatting Exception Messages

Top comments (26)

Collapse
 
eusoumabel profile image
Lucca Mabel

Exception precisam contar minimamente uma história pro usuário ou pro desenvolvedor do quê tá acontecendo.

Acho que nunca havia olhado para as Exceptions dessa forma, e realmente isso muda tudo.

Excelente artigo, ótimo material para estudar!!!

Collapse
 
leandroseg profile image
LeandroSeg

Eu entendi a ideia, ela parece que deixa as coisas mais claras, porém isso aumenta o número de códigos que vão tratar as exceptions, isso pode gerar um montão de código que você vai precisar dar manutenção quando as regras mudarem.

Eu faço diferente, crio um repositório de mensagens exceptions (ou Resources) e depois uso as referência para dispará-las de dentro dos objetos de negócios, sem mais código envolvido, já que são os objetos de negócios que sabem o que deu errado e eles devem entregar as informações para as camadas superiores de uma forma padrão. A simplicidade às vezes é melhor do que mais código envolvido e mais acoplamentos entre classes ou mais funções.

Talvez sua ideia seja mais aplicável a aplicações onde determinar o que está ocorrendo dentro de uma regra de negócio é complexo e você queira isolar tais regras.

Mas ótimo artigo e parabéns.

Collapse
 
douglasffilho profile image
Douglas Fernandes

Excelente post amigo!! Se me permite um pitaco... Um tempo atrás li sobre o uso abusivo de try catch em desfavor ao uso de lógica com base em condicionais e o resultado é que blocos try catch tornam seu código lento (e caro) e, por este motivo, é desencorajado o uso excessivo salvo guardo ocasiões em que não se sabe qual tratativa realizar (casos desconhecidos pela sua lógica). Deixo aqui dois links úteis para levar em consideração quando estiver construindo alguma solução e estiver em dúvida sobre o uso de try catch ou if else:

Collapse
 
leandroseg profile image
LeandroSeg

uso excessivo de try catch é derivado de um mal projeto, a ideia base do try-catch é você reduzir o seu uso a lugares onde ele possa interceptar tudo que acontece embaixo do capô, evitando assim que aplicações buguem com CTD sem que nenhum rastreamento seja possível.
Evitar o uso de IFs é bom, adiciona complexidade ao código e pode gerar mais dor de cabeça do que resolver as coisas.

Collapse
 
fefas profile image
Felipe Martins

Parabéns pelo conteúdo de qualidade :)

Gostaria de apenas dar um feedback ou, talvez, mais um passo na refatoracao/evolucao das excecoes do exemplo: não é uma boa ideia colocar o código HTTP dentro das exceções de domínio/negócio.

Aqui vão motivos:

  • Teorico: código HTTP está relacionado ao protocolo de comuinicação. Portanto, este deve existir apenas na cada de apresentação ou interface de usário (aquele User Interface do DDD).
  • Prático: dependendo da origem da chamada do endpoint ou do cliente que irá chamar este método, o erro PlayerInventoryException::itemNotFound poderá ter significados diferentes e nem sempre o 403 é o mais apropriado. Portanto a tradução do erro de domínio/negócio para um erro de protocolo deve ser feita na camada mais externa.

Aqui uma ideia de código:

class IradoController
{
    public function equipItem(Request $request): Response
    {
        try {
            // chamada plau
        } catch (PlayerInventoryException) {
            return new Response(403, 'Ação não permitida');
        }

        // uau! Nao teve erro
    }

    public function showItemDetails(Request $request): Response
    {
        try {
            // chamada chata
        } catch (PlayerInventoryException) {
            return new Response(404, 'Item não encontrado');
        }

        // uau! Nao teve erro
    }
}
Enter fullscreen mode Exit fullscreen mode

Parabéns de nv e continue publicando conteúdo de qualidade!

Collapse
 
cherryramatis profile image
Cherry Ramatis

foda demais o conteudo, ate salvei pra ler com calma e adaptar o conhecimento pra ruby

Collapse
 
victorgabriel1998 profile image
Victor Farias

Que conteúdo bom! Valeu demais, espero usar de referência pra meus testes techs!

Collapse
 
m4rri4nne profile image
Alicia Marianne

Muito bom o artigo, claro e didático.

Collapse
 
canhassi profile image
Canhassi

Muito bom!

Collapse
 
guto profile image
guto

Simplesmente incrível! Muito bom artigo!

Collapse
 
nikolai1312 profile image
Nicolas Evangelista

Excelente artigo, parabéns pelo conteúdo! Obrigado por compartilhar!