O problema:
Desde muito tempo, estou preocupado com o exceptions
mecanismo, porque sinto que ele realmente não resolve o que deveria.
REIVINDICAÇÃO: Há longos debates externos sobre esse tópico, e a maioria deles luta para comparar ou exceptions
retornar um código de erro. Definitivamente, este não é o tópico aqui.
Tentando definir um erro, eu concordo com o CppCoreGuidelines, da Bjarne Stroustrup & Herb Sutter
Um erro significa que a função não pode atingir seu objetivo anunciado
REIVINDICAÇÃO: O exception
mecanismo é uma linguagem semântica para o tratamento de erros.
REIVINDICAÇÃO: Para mim, "não há desculpa" para uma função por não realizar uma tarefa: definimos condições pré / pós incorretamente para que a função não possa garantir resultados ou algum caso excepcional específico não é considerado importante o suficiente para gastar tempo no desenvolvimento uma solução. Considerando que, na IMO, a diferença entre o código normal e o tratamento do código de erro é (antes da implementação) uma linha muito subjetiva.
REIVINDICAÇÃO: Usar exceções para indicar quando uma condição anterior ou posterior não é mantida é outro objetivo do exception
mecanismo, principalmente para fins de depuração. Não viso esse uso exceptions
aqui.
Em muitos livros, tutoriais e outras fontes, eles tendem a mostrar o tratamento de erros como uma ciência bastante objetiva, resolvida exceptions
e você só precisa catch
deles para ter um software robusto, capaz de se recuperar de qualquer situação. Mas meus vários anos como desenvolvedor me fizeram ver o problema de uma abordagem diferente:
- Os programadores tendem a simplificar sua tarefa lançando exceções quando o caso específico parece muito raro para ser implementado com cuidado. Os casos típicos são: problemas de falta de memória, problemas de disco cheio, problemas de arquivos corrompidos etc. Isso pode ser suficiente, mas nem sempre é decidido do nível da arquitetura.
- Os programadores tendem a não ler atentamente a documentação sobre exceções nas bibliotecas e geralmente não estão cientes de quais e quando uma função é executada. Além disso, mesmo quando eles sabem, eles realmente não os gerenciam.
- Os programadores tendem a não capturar exceções com antecedência suficiente e, quando o fazem, é principalmente para registrar e lançar ainda mais. (consulte o primeiro ponto).
Isso tem duas consequências:
- Erros que ocorrem com frequência são detectados no início do desenvolvimento e depurados (o que é bom).
- Exceções raras não são gerenciadas e fazem o sistema travar (com uma boa mensagem de log) na casa do usuário. Algumas vezes o erro é relatado, ou nem mesmo.
Considerando que, na IMO, o principal objetivo de um mecanismo de erro deve ser:
- Tornar visível no código onde algum caso específico não é gerenciado.
- Comunique o tempo de execução do problema ao código relacionado (pelo menos ao chamador) quando essa situação ocorrer.
- Fornece mecanismos de recuperação
A principal falha da exception
semântica como mecanismo de tratamento de erros é a IMO: é fácil ver onde a throw
está no código-fonte, mas não é absolutamente evidente se uma função específica pode ser lançada observando a declaração. Isso traz todo o problema que apresentei acima.
O idioma não aplica e verifica o código de erro tão estritamente quanto em outros aspectos do idioma (por exemplo, tipos fortes de variáveis)
Uma tentativa de solução
Com a intenção de melhorar isso, desenvolvi um sistema muito simples de tratamento de erros, que tenta colocar o tratamento no mesmo nível de importância que o código normal.
A ideia é:
- Cada função (relevante) recebe uma referência a um
success
objeto muito leve e pode configurá-lo para um status de erro no caso. O objeto fica muito claro até que um erro no texto seja salvo. - Uma função é incentivada a ignorar sua tarefa se o objeto fornecido já contiver um erro.
- Um erro nunca deve ser substituído.
O design completo obviamente leva em consideração cada aspecto (cerca de 10 páginas) e também como aplicá-lo ao OOP.
Exemplo da Success
classe:
class Success
{
public:
enum SuccessStatus
{
ok = 0, // All is fine
error = 1, // Any error has been reached
uninitialized = 2, // Initialization is required
finished = 3, // This object already performed its task and is not useful anymore
unimplemented = 4, // This feature is not implemented already
};
Success(){}
Success( const Success& v);
virtual ~Success() = default;
virtual Success& operator= (const Success& v);
// Comparators
virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}
// Retrieve if the status is not "ok"
virtual bool operator!() const { return status!=ok;}
// Retrieve if the status is "ok"
operator bool() const { return status==ok;}
// Set a new status
virtual Success& set( SuccessStatus status, std::string msg="");
virtual void reset();
virtual std::string toString() const{ return stateStr;}
virtual SuccessStatus getStatus() const { return status; }
virtual operator SuccessStatus() const { return status; }
private:
std::string stateStr;
SuccessStatus status = Success::ok;
};
Uso:
double mySqrt( Success& s, double v)
{
double result = 0.0;
if (!s) ; // do nothing
else if (v<0.0) s.set(Error, "Square root require non-negative input.");
else result = std::sqrt(v);
return result;
}
Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;
Eu usei isso em muitos códigos (próprios) e forçou o programador (eu) a pensar mais sobre possíveis casos excepcionais e como resolvê-los (bom). No entanto, ele possui uma curva de aprendizado e não se integra bem ao código que agora o utiliza.
A questão
Eu gostaria de entender melhor as implicações do uso desse paradigma em um projeto:
- A premissa do problema está correta? ou Perdi algo relevante?
- A solução é uma boa idéia arquitetônica? ou o preço é muito alto?
EDITAR:
Comparação entre métodos:
//Exceptions:
// Incorrect
File f = open("text.txt"); // Could throw but nothing tell it! Will crash
save(f);
// Correct
File f;
try
{
f = open("text.txt");
save(f);
}
catch( ... )
{
// do something
}
//Error code (mixed):
// Incorrect
File f = open("text.txt"); //Nothing tell you it may fail! Will crash
save(f);
// Correct
File f = open("text.txt");
if (f) save(f);
//Error code (pure);
// Incorrect
File f;
open(f, "text.txt"); //Easy to forget the return value! will crash
save(f);
//Correct
File f;
Error er = open(f, "text.txt");
if (!er) save(f);
//Success mechanism:
Success s;
File f;
open(s, "text.txt");
save(s, f); //s cannot be avoided, will never crash.
if (s) ... //optional. If you created s, you probably don't forget it.