Boost logo

Boost Users :

From: Johan Nilsson (r.johan.nilsson_at_[hidden])
Date: 2006-09-13 03:44:09


[I think I'm mostly repeating what other people have said here, but decided
to post anyway]

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?

Essential, but there's no rule without an exception.

>
> 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.

Actually, I think the above is an example of a class that's easy to test in
isolation. In fact, close to perfect ;-)

You're requiring a reference to a polymorphic object in the ctor, which is
great from a testing point of view. It's possible to provide a ostringstream
if you want to test the formatting, or some variety of "nullstream" if you
want to test other stuff. Dependency injection via ctors are my preferred
way of making (C++) objects testable.

Also, in this very case the ostream isn't provided just for testability -
it's an essential part of the interface. For your user requirement (~"needs
an ostream for which to log events") there's really no good default either,
as e.g. std::cout requires a console application.

Perhaps a different example would be better to avoid this distraction?

>
> 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....)

For the general case, that's true. For this specific case, I don't agree.

I'm a bit curious though, about what constitutes a test from your point of
view. From time to time in your posting, I'm not getting the grip of whether
you are talking about unit testing from the authors point of view, or
exploratory testing.

>
> 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. 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.

If the constructor arguments (or rather, their contributions to the
functional state of the object) are essential for the functionality of the
object, they shouldn't have unusable defaults - i.e. cause violations of
later method call preconditions.

As a developer I occasionally find myself adding extra arguments to ctors,
or ctors overloads, just to make the dang thing testable without having to
access an external resource, such as the file system, or the underlying OS
API. For those cases, where the extra argument or overload are there just
for the sake of testability, there always exists a reasonable default (or
perhaps even only one real implementation). I just try to make sure that
those extra arguments won't have to be provided by the casual user.

I can agree that this last thing is a kind of interface pollution, but IMHO
it is essential to be able to test as much as possible in isolation.

> My gut instinct is not to have much sympathy for this argument,
> 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.

I think your gut instinct is correct. Also, taking design guidelines for
.NET (which are perhaps absolutely appropriate there) and attempting to
apply them to C++ programming might not be the right way to go.

> 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

Yuk. If there was really a need for this lazy init, I think a null outputter
would be better in this case. But it would depend on the application in
question.

[snip lots of .NET discussion]

>
> So I'm confused. Constructors that "really" initialize objects detect
> some kind of errors during compilation, but they make testing harder,

Again, if the ctor arguments are essential for the operation of the object
and have no reasonable defaults, what's the alternative, really? If the
arguments are non-essential, don't require them, but perhaps provide
additional overloads to allow customized construction.

An example for latter would be the inclusion of a filter for the EventLog
class, e.g. based on message contents or priority (as I believe someone else
said also).

> are arguably contrary to exploratory programming,

I might be saying the same thing over and over again, but how can you
explore something unusable? As a side not though, I often prefer using unit
testing as an exploratory tool.

> and seem to
> contradict the advice of the designers of the .NET API.

Different platform and philosophy, unless you're talking about C++/CLI.

> Constructors
> that "sort
> of" initialize objects are more test-friendly (also more loosely
> coupled, BTW)

I don't understand how loose coupling and "sort-of-initialized" objects
connect?

> and facilitate an exploratory programming style, 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 above.

Regards,

Johan Nilsson


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