Unexceptional examples

| 4 Comments

We're going to run out of amusing titles for these exception related blog entries sooner or later...

Joel wants Ned to rename the functions in his 'exceptions are better' example. He wants InstallSoftware(), CopyFiles() and MakeRegistryEntries(). Jesse Ezell leapt in with a C# version which includes the rollback functionality that Joel was undoubtedly hinting at. The thing is, although Jesse is defending the use of exceptions, I think his example is making the case for the other side...

I spend most of my time in C++ land. We have the best exceptions. ;) They fit well with our constructors and deterministic destructors. We can use the Resource Acquisition Is Initialisation idiom (RAII) to write classes that manage the lifetime of resources and make them safe in a world of exceptional exits. We don't need many try {} catch blocks because, if we write it right, our code just does the right thing when an exception passes through. Our exceptions are unchecked by the compiler and this is a Good Thing. This is the land where Joel is dead wrong in his belief that exceptions are like gotos and that error returns are better.

If Joel's error returns are a hammer then our exceptions are a power screwdriver. When used correctly, with the correct parts of the rest of our toolbox our power screwdriver is considerably more powerful and elegant than a hammer. The problem is that most of the people using it seem to just hold it by the end and swing it at their nails. I've also seen people who advocate error returns saying that we're not comparing 'like with like' if we try and prove that exceptions, used in the right way, are better than error returns. I'm sorry guys, you can't compare 'like with like' because if you just replace your error returns with exceptions and try and structure the code in pretty much the same way then you're just missing the point entirely.

So how would I show that exceptions are superior to error returns? Something like this I guess:

void DoInstall()
{
   CFileInstaller fileInstaller;
   CRegistyInstaller regInstaller;

   fileInstaller.Install();
   regInstaller.Install();

   fileInstaller.Commit();
   regInstaller.Commit();
}

The 'installer' classes would follow an 'undoable command' pattern. They'd have an Install() and a Commit() and a Rollback() method and they'd operate much like database transactions do. You call Install() which does the work and as long as you call Commit() they don't call Rollback() when they are destroyed. Install() is allowed to throw, Commit() isn't.

Those of you at the back that just stood up and started shouting about how my little rules about which functions can throw and which can't? Sit down, shut up, live with it. Effective use of exceptions sometimes requires these little guarentees. All Commit() should do is set a boolean flag to true. In C++ we could choose to enforce our 'can't throw' rule with a throw specification on the function - note: throw specs are a pretty broken feature of C++ so don't use them for anything except specifying a no throw situation...

For those of you unfamiliar with C++'s destructors there's a 'hidden' call to ~CRegistryInstaller() and ~CFileInstaller() just before the closing brace. The destructors can check to see if the action was committed and if not call Rollback(). The destructors are guaranteed to be called just before the closing brace and they're called in the opposite order to the order you constructed the objects.

One of the callers can handle any exceptional situations because they'll know what to do; all we know is that either both installations work or both fail.

My example scales; though if you get many more installation components you'd probably want to add them to some form of 'Installation Stack' object so that you can do something like this:

{
   CInstallerStack installer;

   CFileInstaller fileInstaller(installer);
   CRegistyInstaller regInstaller(installer);
   COtherInstaller reg2Installer(installer);
   COtherInstaller reg3Installer(installer);
   COtherInstaller reg4Installer(installer);
   COtherInstaller reg5Installer(installer);
   COtherInstaller reg6Installer(installer);

   installer.Install();
   installer.Commit();
}

Each installer adds itself to the supplied stack in it's constructor, you call Install() on the stack and it walks its list of installers in order and calls Install() on them. It then walks its list and calls Commit(). Object destruction takes care of any rollbacks required as above.

In C# I guess you'd use IDisposable and Using with the stack based solution to achieve the same goal. With Java you'd need a try {} finally but that's just because they don't trust you.

I can provide source code to the above in C++, C# and Java if you really want.

So, in summary; Exceptions when used correctly with the idioms and parts of the language that support them are clearly superior ;) to error returns. They can be misused, or used as a replacement for error returns and that's less effective.

Thanks Joel, I've enjoyed the discussions that broke out all over the place. I've agreed with lots of people, I've disagreed with lots of people; I've questioned my own code and my reasons and I've been forced to think about things. Now, can we move on?

4 Comments

Actually, I prefer explict calls to rollback. The problem is that you can't control the rollback order like this. You probably want the rollback to occur in reverse order if the method fails (although it could be a custom order for some reason). Explicitly defining that rollback order is a good thing. Yes, technically you could just do this and destroy them in the order you want them uninstalled, but that makes your code hard for someone else to work with, because things are happening implicitly that they might not expect.

Jesse,

I agree that if you want to explicitly control the rollback order then you cant use this approach as it stands. In that case I'd probably adjust the 'command stack' object to allow you to specify the order of the rollbacks so that I could still rely on the destruction or disposal of the command stack to do the rollback itself.

Anyway, I think this has probably been done to death now ;) Everytime someone 'solves' the problem someone else changes the requirements. It's becoming too much like normal work ;)

Actually rollback would occur in opposite order in the above code because destruction of automatic objects occurs in opposite order to creation.
When necessary customising the rollback order is a simple matter of creating the object to be destroyed last before any other objects, even if it isn’t going to be used. In a few cases this will require a “half-constructed” object, but these cases are rare - as are the cases where rollback order other than opposite to creation is important.

Jon

Yup, I assumed that Jesse wanted the rollback in the opposite order to the reverse order that it would occur in ;) But perhaps I was just misunderstanding him.

Leave a comment

About this Entry

Tests and TDD in anger was the previous entry in this blog.

Tangled testing? is the next entry in this blog.

I usually write about C++ development on Windows platforms, but I often ramble on about other less technical stuff...

Find recent content on the main index or look in the archives to find all content.

I have other blogs...

Subscribe to feed The Server Framework - high performance server development
Subscribe to feed Lock Explorer - deadlock detection and multi-threaded performance tools
Subscribe to feed l'Hexapod - embedded electronics and robotics
Subscribe to feed MegèveSki - skiing