Re: Exception handling and encapsulation
"benben" <benhonghatgmaildotcom@nospam> wrote in message
news:47357b27$0$19803$afc38c87@news.optusnet.com.au...
Hello C++ experts,
This is rather a design question, I must say. My question is, how do you
write exception safe code while ensuring encapsulation?
In C++, if a function by itself does not know how to handle an exception
raised from a lower level system, we usually implement the function in
such a way that it is exception safe, yet allow the exception to be
propagated to a higher level system.
However, since each system level defines a certain abstraction, allowing
exceptions to propagate through multiple system levels may breach
encapsulation, because it exposes low level implementation details. The
exception hence can be less useful than it should be, as the higher level
system will have little knowledge how such low level exceptions are to be
handled accurately.
Furthermore, should the higher level system choose to handle exceptions
from multiple layers down, it risks to be turned invalid as soon as
changes are made to the lower level systems. This is very similar to
accessing a class member that is ought to be private.
To give an example, consider the following program, which consists of
three distinct system levels: at the lowest, a database class; up one
level, an enrolment form; and at the highest level, the main function that
uses the enrolment forms.
In general, you want to handle the exception at the lowest level that can do
something about it, either fix it (attempt to relog into the database, try
to reopen the file, etc...). A lot of times you will have to handle the
exception in a number of places. The function that actually caused the
exception may need to catch it and if it can't fix it itself, perhaps set
some state before propogating it upward (in a database where the exception
was connection closed, if it can't reestablish the connection, but the
connection status in a bad state somehow) and then the higher level would
catch the exception and do what it can about it (the caller sees that the
connection was closed, it can try to re-establish connection, if it can't
send a UI message that the connection was closed to the user), and so on up
until the error is fixed, or the program is terminated.
#include <iostream>
#include <sstream>
// Incomplete program, will not link.
///////////////////////////////////////////////////
// Low level system
class connection_error{};
class IO_error{};
class repetition_error{};
class database
{
public:
void connect(std::string location) throw (connection_error);
void create_record(std::string rc) throw (IO_error,
repetition_error);
// ...
};
///////////////////////////////////////////////////
// Mid level system
class submission_error{};
class invalid_name{};
class invalid_age{};
class enrolment_form
{
std::string name;
unsigned int age;
void check() const
{
if (name.size() == 0)
throw invalid_name();
if (age > 80 || age < 18)
throw invalid_age();
}
public:
enrolment_form(std::string _name,
unsigned int _age);
// submission routine version 1
// throw everything has it
void submit1() const
{
check(); // may throw invalid_name
// or invalid age
database db;
db.connect("enrolment"); // may throw connection_error
std::ostringstream oss;
oss << name << ":" << age;
db.create_record(oss.str()); // may throw IO_error
}
// submission routine version 2
// translate lower level exceptions
void submit2() const
try
{
submit1();
}
catch (connection_error)
{
throw submission_error();
}
catch (IO_error)
{
throw submission_error();
}
};
///////////////////////////////////////////////////
// High level
int main()
{
enrolment_form form1("Ben", 22);
form1.submit1();
enrolment_form form2("Bob", 22);
form2.submit2();
}
Here is the dilemma:
1) If main() is to handle the exceptions from submit1() member function
call, it will have to deal with exceptions from both enrolment_form
(mid-level) and database (low-level.) Since the use of a database is to be
encapsulated from main(), the writer of main() obviously knows little how
to handle such exceptions.
1. Where did submit() get the information of "enrolment"? If it is actually
hard coded and the connection can not be established, then there is nothing
that the program can do about the connection error at all. The user will
have to find out why it is not establishing connection. In this case it has
to propogate up to the UI with a message that there is a connection error
and the user will have to fix it (ooop, disconnected the network cable).
If submit() got the information of "enrolment" from somewhere else, perhaps
it's a variable, where did it come from? Was it passed into the constructor?
If so, then whatever constructed the class needs to handle the error, and
with this simple 2 class system it had to of been main, so again, main has
to handle the exception.
Same with the user name and age.
Again, the lowest level that can handle the exception and do something about
it should handle it. If it can't it needs to propogate it up into the user
interface and state there is a problem and give the user a chance to fix it
(type in the correct name and age or check the connection or type in the
correct database to connect to, etc...).
With data errors (which these both are) the user is the only one that can
fix it, and so it has to propogate up to the user interface, main or
whatever handles the UI.
With other types of errors (out of disk space, out of memory) then a lower
level system can sometimes handle it (try a smaller file size, delete a temp
file it created, try another disk, try allocating more memory, etc...) and
so it doesn't have to propogate up. But if it does again it goes to the
user interface "out of disk space" "out of memory" etc...
2) If the mid-level enrolment_form class is to catch every lowe-level
exceptions and translate it to one of its own (but higher level in
abstraction) exception types, such as submission_error, encapsulation is
enforced but:
a. The writer of main() will be equally clueless on how exceptions, such
as submission_error shall be handled, since the nature of the exception is
abstracted away (that is, the type information of the original exception,
at least)
b. The implementation of mid-level system, enrolment_form, is complicated
as it is responsible to translate all lower-level exceptions. For the very
least, functions are littered with try-catch blocks, which is ugly IMO.
It can be done though, through snowballing fashion, in which the lower
level exception information is rolled into the translated exception every
time the exception propagates up one abstraction level. The down side of
this solution is it is complicated. It requires exceptions to have more
complex data structure which itself may raise a few terminal exceptions.
Exception types with different conventions can also make this strategy
very difficult, if not at all impossible.
Most of you must have been involved in some large C++ projects dealing
with such problems. I would like to hear what you think, how you deal with
it in your designs, what tools you use to levitate such problem, or just
generally what other options I still have. I would love to find out a
graceful solution to problems of this kind!