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
- 2. O que gostariamos de EVITAR
- 3. Refatoração 1: Criando Exceptions
- 4. Design Patterns: Factory Pattern
- 5. Refatoração 2: Refinando as Exceptions
- 6. Conclusão
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
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
}
}
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
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
{}
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
}
}
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');
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
);
}
}
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);
}
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
);
}
}
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);
}
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
);
}
}
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
}
}
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)
Acho que nunca havia olhado para as Exceptions dessa forma, e realmente isso muda tudo.
Excelente artigo, ótimo material para estudar!!!
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.
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:
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.
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:
PlayerInventoryException::itemNotFound
poderá ter significados diferentes e nem sempre o403
é 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:
Parabéns de nv e continue publicando conteúdo de qualidade!
foda demais o conteudo, ate salvei pra ler com calma e adaptar o conhecimento pra ruby
Que conteúdo bom! Valeu demais, espero usar de referência pra meus testes techs!
Muito bom o artigo, claro e didático.
Muito bom!
Simplesmente incrível! Muito bom artigo!
Excelente artigo, parabéns pelo conteúdo! Obrigado por compartilhar!