Fog Creek Software
Discussion Board




DoSomething as InstallSoftware

The way to do that in C++ would be to write class objects to represent functionality. (Function objects ?)

For eg:
class CopyFiles {
        CopyFiles(vector<string> names, string from, string to);
        void operator()(); // throws exception on error
        void commit(); // set commited flag
        ~CopyFiles()
        {
            // remove copied files if copied and not committed
        }
};

class MakeRegistryEntries {
        MakeRegistryEntries(vector<RegistryEntries> entries);
          void operator()(); // throws exception on error
          void commit(); // set commited flag
        ~MakeRegistryEntries
          {
                  // remove registry entries if added but not committed
          }
};

void InstallSoftware(...)
{
        CopyFiles files(...);
        MakeRegistryEntries makeRegistryEntries(...);

        copyFiles();
        makeRegistryEntries();

        copyFiles.commit();
        makeRegistryEntries.commit();
}

Here the destructors (thrown on exception) will take care of  undoing their job if operator() has been called by not commited.

This works only in C++ due to guaranteed desctruction semantics. In Java / C# a little more work is involved with local flags and finally clause.

This is cleaner as the semantics of doing and un-doing an operation are captures in one logical unit (class). This is the basic Construction is resource acqusition principle (a little extended :-))

satya
Wednesday, October 15, 2003

I think the point Joel was trying to make was that the code isn't cleaner if you use exceptions.

Such as:

void InstallSoftware(...)
{

        try {

            CopyFiles files(...);
            copyFiles();
   
   
            MakeRegistryEntries makeRegistryEntries(...);
            makeRegistryEntries();

            copyFiles.commit();
            makeRegistryEntries.commit();
        } catch (CopyFileException &ex) {
            ...
        } catch (MakeRegException &ex) {
        } catch (...) {
            ...
        }
     

}


Wednesday, October 15, 2003

The whole point of the example is that you don't have to 'catch' the exceptions. So exceptions, with destructors can be used as a mechanism to 'automate' error handling without having to do explicit try/catch/rollback in C++ code.

satya
Wednesday, October 15, 2003

Joel's example was obviously talking about the need for rollback in some cases, and he is implicitly stating that this is easier with return codes.

I'll use a C# example just because that's what I'm working in at the moment:

STATUS InstallSoftware( )
{
    STATUS st;
    st = CopyFiles( );
    if (st != SGOOD) return st;
    st = MakeRegistryEntries( );
    if (st != SGOOD) {
        RollbackFiles( );
        return st;
    }
    return SGOOD;
}

The directly corresponding version with exceptions would be:

void InstallSoftware( )
{
    CopyFiles( );
    try
    {
        MakeRegistryEntries( );
    }
    catch
    {
        RollbackFiles( );
        throw;
    }
}

Ok, the direct transliteration sucks. What I'd do in C# is something like this instead:

void InstallSoftware( )
{
    InstallTransaction t = new InstallTransaction( );
    try
    {
        CopyFiles( t );
        MakeRegistryEntries( t );
    }
    catch
    {
        t.Rollback( );
    }
}

Ok, lots of effort for little gain, right? You need to write the extra transaction object that you don't need with return codes! Exceptions suck!

Ahh, but not so fast. Remember, software changes, and installers in particular change a LOT, in annoying ways. Let's add a few more steps to Joel's original example:

STATUS InstallSoftware( )
{
    STATUS st;
    st = CopyFiles( );
    if (st != SGOOD) return st;
   
    st = MakeRegistryEntries( );
    if (st != SGOOD) {
        RollbackFiles( );
        return st;
    }
    st = RegisterComObjects( );
    if( st != SGOOD ) {
        RollbackRegistryEntries( );
        RollbackFiles( );
        return st;
    }
    st = InstallPalmConduits( );
    if( st != SGOOD ) {
        RollbackRegisterComObjects( );
        RollbackFiles( );
        return st;
    }
    st = RegisterUninstaller( );
    if( st != SGOOD ) {
        RollbackPalmConduits( );
        RollbackRegisterComObjects( );
        RolbackRegistryEntries( );
        RollbackFiles( );
        return st;
    }   
    return SGOOD;
}

Now, NOBODY can argue that that kind of code duplication makes any kind of sense, or is anything but a maintenance nightmare. In fact, there's a bug in the example as it stands, and it'll be darn hard to find in testing, since you have to fail in exactly the right way for it to pop up. There are ways to make this a little more maintainable, but they're all ugly (massively nested ifs or the dreaded goto are two options).

So, what does the exception version of this look like?

void InstallSoftware( )
{
    InstallTransaction t = new InstallTransaction( );
    try
    {
        CopyFiles( );
        MakeRegistryEntries( );
        RegisterComObjects( );
        InstallPalmConduits( );
        RegisterUninstaller( );
    }
    catch
    {
        t.Rollback( );
        throw;
    }
}

Simple, short, easy to maintain, and (more importantly) HARD TO SCREW UP.

I guess my point is that if you code with exceptions the same as you did with return codes, of course exceptions are going to suck. Learn new idioms, on the other hand, and you get much more robust code.

       

Chris Tavares
Wednesday, October 15, 2003

The point is you don't have to catch the exception in Satya's code.  This is a HUGE benefit of C++ exceptions.  I really this discussion can not continue with out considering the work that has been be done by Sutter, Abrahams, et al on the topic.  I feel like we are trying to derive the concept of the Strong Guarantee from first principles, when the equation has already been determined by Sutter -- Exceptional C++ and More Exceptional C++, read them, then return to the subject.

Deterministic destruction buys a lot more than cleaning up memory.  Satya's example illustrates this perfectly.

http://www.summitsage.com.

christopher baus (tahoe, nv)
Wednesday, October 15, 2003

Here's how Satya's code would be used.

try{
  InstallSoftware();
}
catch (SomeInstallerException e)
{
  //
  // transaction already canceled, do nothing, but notify the
  // the user.
  //
  //
  std::cout<<"couldn't install software"<<std::end;
}

The magic of the C++ stack unwiding does everything else.  Deterministic destruction is probably one of leading reasons why C++ is still my language of choice.

christopher baus (tahoe, nv)
Wednesday, October 15, 2003

Sorry, just curious.

But what's the code in the t.Rollback function?

Mike Aldred
Thursday, October 16, 2003

Probably something like:

Rollback()
{
  while (more actions in action stack)
  {
    last_action.Rollback()
    pop last_action off stack
  }
}

(see the GoF patterns book - they suggest using the Command pattern for this kind of thing)


Thursday, October 16, 2003

void operator()(); // throws exception on error


Why doesn't this exception have to be caught?

If you don't want your program blowing up, that is.

Bob Ng
Thursday, October 16, 2003

"The point is you don't have to catch the exception in Satya's code"


I'm with Bob.  If you don't want your program to fault, then you're going to need to catch those exceptions _somewhere_ in the program. 

Please explain how you can not catch those exceptions yet still have a program that behaves in a user friendly manner.

Johnny Simmson
Thursday, October 16, 2003

The point is you don't need to catch the exception RIGHT THERE. You're free to handle the error somewhere further up the stack, without having to cook up an ad-hoc means of getting the information there.

This really becomes important when building reusable libraries. I recently wrote a library to implement communication with a local network server. If I had to handle, say, a connection failure within the library, what would I do? Suppose I throw up a messagebox.

Ok, that's great - until somebody tries to use my library inside a Windows Service, which the throws that messagebox up onto a background desktop. Their service hangs, and nobody can figure out why.

Instead, I throw an exception. It's the caller that knows how to report the error, not me, so I pass the responsibility off to the caller.

Chris Tavares
Thursday, October 16, 2003

"If I had to handle, say, a connection failure within the library, what would I do?"

I really don't give a sh*t one way or the other about exceptions, BUT there's too many successful 'C' libraries out there for you not to know the answer to this question:

You return an error value.

risk taker
Thursday, October 16, 2003

As has been said so many times before, returning an error value has three problems.

One, if your method/subroutine already returns a value, you have the -99 Problem: you have to pick some magic number that will never be a normal return value. In an OO paradigm, it may be practically or even mathematically impossible to do this in some cases.

Two, if you ever have to modify that code, you have to be careful to honor that error code convention, and the compiler can't help you, since it sees no difference between a returned error code and anything else.

Three, whether your subroutine returns a value or not, an exception would be much more explicit, much less error-prone.  In other words, it's based on better principle.  It's also more natural.  If I ask you what the hexadecimal number 0B4K is in decimal, you don't say "-1", you say "it's not valid".

Those successful C libraries are successful because they've been around so long that everyone's gotten used to the irregular usage.

Paul Brinkley
Friday, October 17, 2003

*  Recent Topics

*  Fog Creek Home