Anotações Capítulo 7: Error Handling
Jonilson Sousa
Posted on June 28, 2021
- As coisas podem dar errado, e quando isso ocorre, nós somos responsáveis por certificar que nosso código faça o que seja preciso fazer.
- É quase impossível ver o que o código faz devido a tantos tratamentos de erros.
- Esse recurso é importante, “mas se obscurecer a lógica, está errado”.
Use exceções em vez de retornar códigos
- Antigamente muitas linguagens não suportavam exceções.
- Assim a solução era criar uma flag de erro ou retornar um código de erro que o chamador pudesse verificar. Exemplo:
public class DeviceController {
…
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
// Check the state of the device
if (handle != DeviceHandle.INVALID) {
// Save the device status to the record field
retrieveDeviceRecord(handle);
// If not suspended, shut down
if (record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
} else {
logger.log("Device suspended. Unable to shut down");
}
} else {
logger.log("Invalid handle for: " + DEV1.toString());
}
}
...
}
- “O problema era que essas técnicas entupiam o chamador, que devia verificar erros imediatamente após a chamada”. Porém facilmente se esqueciam de fazer essa verificação. Por esse motivo, é melhor lançar uma exceção quando um erro for encontrado.
- Assim o código de chamada fica mais limpo e sua lógica não fica ofuscada pelo tratamento de erro.
- Exemplo anterior lançando exceções:
public class DeviceController {
…
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id) {
…
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
…
}
...
}
- O código fica mais claro. O código fica melhor porque as duas coisas estão separadas: O algoritmo para o desligamento do dispositivo e o tratamento de erro.
Crie primeiro sua estrutura try-catch-finally
- As exceções definem um escopo dentro de seu programa.
Sempre que executar o
try
, você declara que aquela execução pode ser cancelada a qualquer momento e então continuar no catch. - De certa forma, os blocos
try
são como transações. Seucatch
tem que deixar seu programa num estado consistente, não importa o que aconteça notry
. - Assim, uma boa prática é começar com uma estrutura
try...catch...finally
quando for escrever um código que talvez lance exceções. Isso ajuda a definir o que o usuário do código deve esperar, independente do que ocorra de errado no código que é executado notry
. - Exemplo:
- Um código que acesse um arquivo e consulte alguns objetos em série:
- Começamos com um teste de unidade que mostra como capturar uma exceção se o arquivo não existir:
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
sectionStore.retrieveSection("invalid - file");
}
- O teste nos leva a criar esse stub:
public List<RecordedGrip> retrieveSection(String sectionName) {
// dummy return until we have a real implementation
return new ArrayList<RecordedGrip>();
}
- Nosso teste falha porque ele não lança uma exceção. Agora, mudamos nossa implementação de modo a tentar acessar um arquivo inválido, lançando uma exceção:
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
} catch (Exception e) {
throw new StorageException("retrieval error", e);
}
return new ArrayList<RecordedGrip>();
}
- Agora o teste funciona porque captura a exceção.
- Refatorando para a exceção que realmente é lançada pelo
FileInputStream
:
public List<RecordedGrip> retrieveSection(String sectionName) {
try {
FileInputStream stream = new FileInputStream(sectionName);
stream.close();
} catch (FileNotFoundException e) {
throw new StorageException("retrieval error”, e);
}
return new ArrayList<RecordedGrip>();
}
Use exceções não verificadas
- Exceções verificadas violão o princípio do Aberto-Fechado.
- Se lançar uma exceção a ser verificada a partir de um método em seu código e o catch estiver três níveis acima, será preciso declará-la na assinatura de cada método entre você e o catch.
- Isso significa que uma modificação em um nível mais baixo do software pode forçar a alteração de assinaturas em muitos níveis mais altos.
- Os módulos precisaram ser alterados mesmo que nada tenha realmente mudado.
- O propósito de exceções é que elas lhe permitem tratar erros distantes, porém as exceções verificadas quebram o encapsulamento.
- Exceções verificadas podem às vezes ser úteis se estiver criando uma biblioteca crítica. Mas no desenvolvimento geral, os custos da dependência superam as vantagens.
Forneça exceções com contexto
- Cada exceção lançada deve fornecer contexto suficiente para determinar a fonte e a localização de um erro.
- No Java temos a
stack trace
de qualquer exceção. Porém ela não consegue dizer o motivo da falha da operação. - Crie mensagens de erro informativas e as passe juntamente com as exceções.
- Mencione a operação que falhou e o tipo de falha.
Defina as classes de exceções segundo as necessidades do chamador
- Exemplo que cobre todas as possíveis exceções:
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
…
}
- A estrutura acima possui muita duplicação! E na maioria dos casos de tratamento de exceções, o que fazemos é relativamente padrão, independente da situação no momento.
- Temos que registar um erro e nos certificar que podemos prosseguir.
- Nesse caso podemos simplificar o código pegando um tipo comum de exceção:
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
…
}
- Assim temos a classe
LocalPort
é um simples wrapper (empacotador) que captura e traduz as exceções:
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
…
}
- Esses empacotadores podem ser muito úteis.
- Essa abordagem acima é a melhor prática que existe.
- Com isso, minimizamos as dependências nelas.
- Assim é fácil migrar para outra biblioteca no futuro sem grandes problemas.
Defina o fluxo normal
- Um código que soma as despesas em um aplicativo de finanças:
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
- Nesse código, se as refeições (meals) forem um custo, elas se tornam parte do total. Caso contrário, o funcionário recebe uma quantia para ajuda de custos (PerDiem) pela refeição daquele dia. Assim, a exceção confunde a lógica.
- Nesse caso seria melhor não ter que lidar com o caso especial, da seguinte forma:
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
- E para deixar o código ainda mais simples, alterando o
ExpenseReportDAO
para que ele sempre retorne um objetoMealExpenses
. Se não houver gastos com refeições, ele retorna um objetoPerDiemMealExpenses
que retorna a diária como seu total:
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// retorna a ajuda de custo padrão
}
}
Isso se chama padrão Special Case (Padrão de caso especial). Você cria uma classe ou configure um objeto de modo que ele trate de um caso especial para você. Com isso o código do cliente não precisa lidar com um comportamento diferente. Este fica encapsulado num objeto de caso especial.
Não retorne null
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if (registry != null) {
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
- Quando retornamos
null
, estamos criando mais trabalho para nós mesmos e jogando problemas em cima de nossos chamadores. Só basta esquecer uma verificaçãonull
para que o aplicativo fique fora de controle. - Percebeu que não havia uma verificação de
null
na segunda linha doif
aninhado? E seperistentStore
fossenull
? - O problema do código acima é a quantidade de coisas que podem ser
null
. -
Em vez de retornar
null
de um método, lance uma exceção ou um objeto SPECIAL CASE! - Se um terceiro método retornar
null
use o empacotamento ou o objeto de caso especial. - Outro exemplo de código que retorna null:
List<Employee> employees = getEmployees();
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
- Agora usando uma lista varia em vez de
null
:
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
E no método getEmployees
:
public List<Employee> getEmployees() {
if( .. there are no employees .. )
return Collections.emptyList();
}
- Com isso minimizamos a chande
NullPointerException
e o código será mais limpo.
Não passe null
- Retornar
null
dos métodos é ruim, mas passarnull
para eles é pior ainda. - A menos que seja uma API que espere receber
null
, devemos evitar passá-lo. - Exemplo do porquê:
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
return (p2.x – p1.x) * 1.5;
}
…
}
-
Se alguém passar
null
para o método acima receberemos umaNullPointerException
! - Poderíamos melhor isso lançando uma exceção:
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
if (p1 == null || p2 == null) {
throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection");
}
return (p2.x – p1.x) * 1.5;
}
}
- Melhorou, porém passar
null
é sinal de problema e pode gerar mais erros por descuido.
Conclusão
- Um código limpo é legível, mas também robusto.
- Podemos fazer esse tipo de código se enxergarmos o tratamento de erro como uma preocupação à parte, algo visível independentemente de nossa lógica principal.
- Com isso damos um grande passo na capacidade de manutenção do código.
💖 💪 🙅 🚩
Jonilson Sousa
Posted on June 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.