Newer
Older
hb / 3-UnderTheHood.txt
@tundra tundra on 14 Jul 2001 8 KB Initial revision
$Id: 3-UnderTheHood.txt,v 1.1 2001/07/14 06:17:00 tundra Exp $

              UNDER THE HOOD: COMMENTARY ON HOW HB WORKS
              ==========================================

The hb.py source file is full of (hopefully) helpful comments.  You'll
probably get the most help from reading that source.  Just so you can
kind of see the overall picture, this document is intended to give you
more of a "Thousand Foot View" of the code.  At the end, I talk a bit
about some of the design decisions I made, and why.


Code Structure
--------------

The code is laid out like this:

    Variables Available For User Modification
    
    Imports

    Aliases & Redefinitions

    Constants & Literal Strings

    Prompts & Strings Used In The Application
 
    The Command Table
	This is where all available commands are described in
	tabular fashion.  This is the heart of how the Command 
	Interpreter knows what to do when the user presses a key.

    Global Variables & Data Structures

    Class Definitions

    Main Handler Function Definitions
        There is one of these for each feature the user can
	select.  So there is a Main Handler for Print, Transfer,
	Save, and so on.

    Support Function Definitions
	Functions we need to do the "housekeeping" inside HB.

    Startup Code
	(This is where the program actually begins execution.)
	This initializes some variables and objects, loads the
	default budget and generally gets things ready to go.

    Command Interpreter Loop
        Sits and waits for the user to key in a command and
        then does something about it.


Comments On The Command Interpreter
-----------------------------------

The Command Interpreter is the most complicated part of hb.py, but
it's pretty well commented in the code.  Here's the Big Picture:


Setting Up The Command Interpreter
----------------------------------

The Options table at the top of the program has a description
for each feature (like Save or Transfer) that the user can invoke.
This "description" consists of two parts:

     1) The feature *name*, and the *name* of the Main Handling Function
        for that feature.

     2) A list (a tuple, really) of all the functions that need to
        be run to gather arguments from the user in order for
        the feature's Main Handling Function to actually be run.
        For example, before we can run Transfer(), we have to discover
        the "From Account", the "To Account", and "How Much".

When you want to add a new feature you have to:

     1) Write a Main Handler Function for it
     2) Write any needed Support Functions
     3) Put an entry in Options for it.

These descriptions are all ***STRINGS***.  Every time HB loads a new
budget, it reads these strings to dynamically construct a so-called
"Jump Table" (sometimes these are called "Dispatch Tables").  This
Jump Table is what the Command Interpreter uses to figure out what to
do when the user asks for something.

You would think that you only need to build the Jump Table once and be
done with it, but that won't work.  Some features may only be available
if certain accounts are present in a given budget.  For example,
Balancing an account depends on the presence of another account
named by the BALACCT variable (in the user variable section at the
top of the code).  If this account is not present in a new budget,
then HB is smart enough to suppress showing that option on the toolbar.
That's why you have to build the JumpTable every time you Load a budget.


Running The Command Interpreter
-------------------------------

Here, roughly, is what the Command Intepreter is doing in pseudo-code:

If we're not done:

   Display the state of the budget to the user.

   Wait for the user to keyin a legitimate hotkey requesting a feature.
   A blank line just means to skip it and go back to the top of this
   whole Command Interpreter loop.

   Use the hotkey to index into the Jump Table (a Python dictionary)
   and retrieve the dispatch information for that feature.

   From this dispatch information we get:

	The name of the Main Handler Function that is responsible
	for that feature.

	The names of all the Support Functions (and their user
        prompts, if any) that we have to run to build a list of
        arguments to send to the Main Handler Function.

   Run each of the Support Functions we found and add their return
   values to args list.

   Create an Invocation Record - a list consisting of two things,
   a reference to the Main Handler Function, and the args we're sending
   it - and push it onto the UnDo stack.  We skip this step if the
   requested feature was UnDo - you cannot UnDo and Undo in HB.

   Now, call the Main Handler Function and pass it the args list we
   just built.


Why The Two Step Process From Options-> JumpTable ->Command Interpreter?
Why Not Just Code The Jump Table Directly?
------------------------------------------------------------------------

Actually that's how I started out.  I directly encoded a Jump Table
something like this:

JumpTable = {
             "B" : ("<B>alance", Balance, ((BalAmount, ""),))
	     ... And so on 
            }


There were several reasons I didn't like this approach:

1) Minor Issue: With a static Jump Table, I'd have to add a flag to
   determine whether the feature was enabled or not (To supress things
   like Balance when BALACCT is not present in the budget).  Not a big
   deal and, in retrospect, probably a little cleaner than the way
   I ended up doing it.

2) Major Issue: Notice that in this version of reality Balance and
   BalAmount are *object references* NOT strings. (A function in
   Python is an object - *everything* in Python is an object. So a
   function reference is really an object reference.) This means that
   the JumpTable would have to appear in the code *after* these
   functions were defined. (Python hates forward references - there's
   probably some way around this, but I've not bothered to figure it
   out.)  This would have meant having literal strings in two places
   in the code - at the top, where most of the literals are defined
   and right after all the function definitions.  I take this
   Destringing and Maintenance stuff pretty seriously - it is the way
   things are gonna be from here forward.  Putting stuff that needs to
   be changed in different places in the code it just begging for
   trouble later on.

3) I wanted to play around with the 'eval' feature of Python.
   It works very nicely, BTW ;))


Why Bother With All This Hocus-Pocus?
-------------------------------------

Yeah!  Why not just have a bunch of nested if-elif pairs to do the same thing.
Several reasons, Grasshopper:

1) This way keeps all the literal strings out of the code.  Destringing is
   pretty important as the globe gets smaller and smaller.  Not everyone
   speaks and reads English (which is one of the reasons Python has Unicode
   character support).  If you code is littered with strings, translating it
   to another language is a real pain and likely to be buggy.  This way, every
   string you may ever have to change sits happily at the top of the code
   before even the first class definition.  Repeat after me:

               SEPARATE & ISOLATE LITERALS FROM LOGIC
               SEPARATE & ISOLATE PRESENTATION FROM COMPUTATION
               SEPARATE & ISOLATE DATABASE FROM DATA STORAGE INFRASTRUCTURE
               SEPARATE & ISOLATE APPLICATIONS FROM SYSTEMS INFRASTRUCTURE
               SEPARATE & ISOLATE SYNCHRONOUS EXECUTION SEMANTICS

  The failure to do these things is why the Internet is such a buggy mess,
  why scale is so hard to achieve, why security is tough to do....
  (Ooops, I just regressed back into Systems Architect mode.  Sorry 'bout
   that, I'm all better now.)

2) Changing things and general maintenance is much simpler.  It's relatively
   easy to maintain a table and some functions.  It is VERY painful to maintain
   nested conditionals as the program logic evolves.

3) Adding new features is a snap.  Try it and see.  Just add a trivial feature
   that, say, displays how many accounts are in the current budget.  Once you
   understand what's going on here, it takes about 5 minutes.

4) More structure means less bugs, means less testing (hopefully).

5) This style of event-driven, table-lookup coding is well suited for
   GUIs.  Someday, I'm gonna break down and learn Tkinter and/or
   wxPython.  When I do, this code structure should change very little.
   I'll have to go to new routines for keyboard input and display, but
   the rest of it should pretty much stay the same way.