|
Boost Users : |
From: Bjorn Roald (bjorn_at_[hidden])
Date: 2006-09-12 19:15:27
Scott Meyers wrote:
> I have a question about library interface design in general with a
> strong interest in its application to C++, so I hope the moderators will
> view this at on-topic for a list devoted to the users' view of a set of
> widely used C++ libraries. In its most naked form, the question is
> this: how important is it that class constructors ensure that their
> objects are in a "truly" initialized state?
very important in my view
> I've always been a big fan of the "don't let users create objects they
> can't really use" philosophy. So, in general, I don't like the idea of
> "empty" objects or two-phase construction. It's too easy for people to
> make mistakes. So, for example, if I were to design an interface for an
> event log that required a user-specified ostream on which to log events,
> I'd declare the constructor taking an ostream parameter (possibly with a
> default, but that's a separate question, so don't worry about it):
>
> EventLog::EventLog(std::ostream& logstream); // an ostream must be
> // specified
>
> I've slept soundly with this philosophy for many years, but lately I've
> noticed that this idea runs headlong into one of the ideas of
> testability: that classes should be easy to test in isolation. The
> above constructor requires that an ostream be set up before an EventLog
> can be tested, and this might (1) be a pain and (2) be irrelevant for
> whatever test I want to perform. In such cases, offering a default
> constructor in addition to the above would make the class potentially
> easier to test. (In the general case, there might be many parameters,
> and they might themselves be hard to instantiate for a test due to
> required parameters that their constructors require....)
Testing in isolation make sense if you are the writer of the library,
not as the user if it. Internal (none-public) interfaces are
appropriate for such use-cases. As a library user I prefer to trust the
library, if I conclude it is no reason to trust it, I prefer to find
alternatives to using that particular library. Confusing interfaces are
to me traps waiting to take away my trust. It does not help that
intentions are good and that there may exist valid use-cases. Confusing
interfaces are still confusing.
Why mess up an otherwise good external interface of your component with
confusing junk for the sake of testability. If you really need such an
interface, make it a separate one. This "special needs" interface
should not be the first that pops up in the face of library users. Hide
it so only specialist looking for it will find it, and make sure they
are aware what territory they are entering. Preferably, in my view,
such interfaces should be internal to your component, hence supporting
testing and other needs for the library writer without messing up the API.
> Another thing I've noticed is that some users prefer a more exploratory
> style of development: they want to get something to compile ASAP and
> then play around with it.
Ok, sounds good and perfectly reasonable,
> In particular, they don't want to be bothered
> with having to look up a bunch of constructor parameters in some
> documentation and then find ways to instantiate the parameters, they
> just want to create the objects they want and then play around with
> them.
I can not see how adding confusing constructors or other confusing
methods in the interface would help anything. If I explore new
territory I certainly would like to be able to make valid assumptions
about the objects I use based on intuition. If I think I test-drive a
snow-mobile, and don't realize I have forgot to add a belt to it, then I
have no idea of what I am exploring. A reasonable default behavior must
be the better solution for exploring.
> My gut instinct is not to have much sympathy for this argument,
> but
agreed
> then I read in "Framework Design Guidelines" that it's typically
> better to let people create "uninitialized" objects and throw exceptions
> if the objects are then used.
I would much prefer build time diagnostics if possible. That said,
sometimes you need data holder objects which potentially have costly
default constructors. If in general the state of the object after
default initialization is legal, but not very useful, then there may be
a better idea to give the object a defined not_valid state into which
the default construction takes it. If that is done, throwing upon
access of the object may be an OK solution. Users could also by use of
policies decide if exceptions are thrown or explicit checking of an
is_valid() member should be used.
Note that there is a clear distinction here between uninitialized as in
undefined, and uninitialized as in a defined not_valid state. The
latter is in my view only a good solution for classes used to hold data.
Either as optimalization or more often as means to aid application
logic. The former is never a good solution.
> In fact, I took the EventLog example from
> page 27 of that book, where they make clear that this code will compile
> and then throw at runtime (I've translated from C# to C++, because the
> fact that the example is in C# is not relevant):
>
> EvengLog log;
> log.WriteEntry("Hello World"); // throws: no log stream was set
It does not make any sense to me why a reference to std::cout or
something similar could not be used as the default stream here. I fail
to see the benefit of the solution in the code example above.
> This book is by the designers of the .NET library, so regardless of your
> feelings about .NET, you have to admit that they have through about this
> kind of stuff a lot and also have a lot of experience with library users.
I am not convinced they have through so much about this, they may have -
but I am not convinced. It may not be wise to assume the solution is
good based on an assumption that some really smart people have thought
long and hard about these aspects of the API usability. Even if that
was the case and this in fact is the best solution for C#, the reasoning
behind it may not apply to C++. These types of interfaces are nothing
new, maybe the part that throws on uninitialized use is of newer date.
But except from that, this looks like patterns both library developers
and users has been used to since long before OO and C++ caught fire. I
am afraid we are so used to it that we miss the chance to see and call
out how bad it looks.
------
Bjørn Roald
Boost-users list run by williamkempf at hotmail.com, kalb at libertysoft.com, bjorn.karlsson at readsoft.com, gregod at cs.rpi.edu, wekempf at cox.net