|
Boost Users : |
From: David Abrahams (dave_at_[hidden])
Date: 2006-09-13 10:07:32
Scott Meyers <usenet_at_[hidden]> writes:
> 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?
Speaking as a library developer ;-) it's extremely beneficial and
almost always the right thing to do.
> 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
Just about any amount of pain in setting up a test (as long as it's
possible) is justified in order to to get the interface right for
users.
> and (2) be irrelevant for whatever test I want to perform.
How so?
> In such cases, offering a default constructor in addition to the
> above would make the class potentially easier to test.
How so?
> (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....)
Sorry, that's too recursive for me to understand. A concrete example
would help.
> 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.
Often true.
> 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.
Uh, yeah, but if you don't supply enough information to meaninfully
construct the object, you don't have something you can play with. And
encouraging users to "play" with dangerous, half-constructed objects
isn't exactly friendly.
> My gut instinct is not to have much sympathy for this argument,
Good gut ;-)
> but 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.
That's terrible advice. It leads to a style of development where
you're always checking to see if your program state is good. It's a
horrible burden on maintainability; the "not good" path is almost
never covered in any tests and so is probably broken anyway.
> 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
>
> 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 do? Why?
> But then on the third hand I get mail like this:
>
> > The .NET libraries have many objects with many constructors that
> > leave the constructed object in a not ready-to-use state.
> >
> > An example: System.Data.SqlClient.SqlParameter is a class that
> > describes a bound parameter used in a database statement. Bound
> > parameters are essential to prevent SQL injection attacks. They
> > should be exceedingly easy to use since the "competition" (string
> > concatenation of parameters into the SQL statement) is easy, well
> > understood, and dangerous.
> >
> > However, the SqlParameter class has six constructors. Only two
> > constructors create a sqlParameter object that can be immediately
> > used.
Omigosh.
> > The others all require that you set additional properties (of course,
> > which additional properties is unclear).
Failure to write documentation that makes a component's requirements
clear is not the mark of a library designer who has "thought about
this kind of stuff a lot!"
> > Failure to prepare the
> > SqlParameter object correctly typically generates an un-helpful database
> > error when the SQL statement is executed. To add to the confusion, the
> > first ctor shown by intellisense has 10 parameters (which, if set
> > correctly, will instantiate a usable object). The last ctor shown by
> > intellisense has only 2 parameters and is the most intuitive choice. The
> > four in between are all half-baked. It's confusing, and even though I
> > use it all the time, I still have to look at code snippets to remember how.
>
> So I'm confused. Constructors that "really" initialize objects detect
> some kind of errors during compilation,
Yes.
> but they make testing harder,
I've not heard a convincing argument that they do that.
> are arguably contrary to exploratory programming,
Nor that.
> and seem to contradict the advice of the designers of the .NET API.
Well, that should tell you something about the designers of the .NET
API. If they find it necessary to sacrifice sound interfaces in order
to get testability and explore-ability, they have weak design muscles
and not nearly the experience you give them credit for.
> Constructors that "sort
> of" initialize objects are more test-friendly
I'll believe it when I see it.
> (also more loosely coupled, BTW)
How so?
> and facilitate an exploratory programming style,
Exploratory programming with capriciously-broken parts is _not_ fun!
> but defer some kinds of error detection to runtime (as well as incur
> the runtime time/space costs of such detection and ensuing actions).
>
> So, library users, what do you prefer, and why?
As a library user, I prefer documentation that describes what I need
to do to use a component correctly and designs that try not to let me
use broken parts. Naturally there are design choices that can be made
*within those constraints* that make testing and exploration easier,
but there is absolutely no need to sacrifice static checking to reach
these ends!
-- Dave Abrahams Boost Consulting www.boost-consulting.com
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