$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" : ("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.