This is a discussion on Best Practices for Checking Prerequisites within the Software Patterns forums, part of the Testing category; Greetings, I've been reviewing the code for software that I've been working on for a couple years. I'...
|
|||||||
| Register | FAQ | Members List | Calendar | Search | Today's Posts | Mark Forums Read |
|
|||
|
Best Practices for Checking Prerequisites
Greetings,
I've been reviewing the code for software that I've been working on for a couple years. I'm looking at some of the situations that I've come across and deciding what I can learn from them and how I can best handle similar situations in the future. Since I am sure that my situation is a familiar one to many of you, I am wondering what practices many of you have formed over the years. Perhaps there is even a published best pattern/practice for my scenerio. Here it is: I have certain methods that users direct my software to call in order to perform certain business-specific tasks. To avoid speaking in generalities, I'll provide a specific example. My software allows tellers (like bank tellers) to transact with customers (as one would do at a bank or a supermarket). At the beginning of the day when a teller reports to work he must assign himself a cash drawer over which cash contents he will be held entirely accountable. My software has methods that correspond to the actions a teller may take over the course of a day's business. Two that come immediately to mind are 1) the assigning of a drawer and 2) the unassigning of a drawer. Let's focus on the latter for now: Unassigning a drawer. My .NET C# code has a method: UnassignDrawer(DrawerData.DrawerRow drawer) Imagine also that my application has a button entitled "Unassign Drawer" (it could have just as well been a menu option in addition to or instead of). When the user clicks this button, the application calls the UnassignDrawer method. Now, understand that the drawer unassignment process has a number of prerequisites. Before a drawer may be unassigned the teller must have: 1. Deposited all his checks (using another method that flags his checks as deposited) 2. Audited (counted out) the cash in his drawer (by denomination so that database may track the exact count of each cash/coin denomination) 3. Closed out any two-sided transactions (as a banker would first do a withdrawal transaction and then a second deposit transaction to transfer funds between accounts as part of one larger overall transaction) .... .... .... (etc.) The number of procedural steps in unimportant. What is important is that there may be any number of business-related prerequisites that the teller must meet before unassigning the drawer. As part of my method UnassignDrawer method I have another method entitled AllowUnassignDrawer(DrawerData.DrawerRow drawer). This method is called early within the UnassignDrawer method. If the prerequisites have not been met, the teller is alerted (with a list) as to the actions he must take in order for the unassignment to be allowed. An interesting question I've asked myself is this: Do I disable the "Unassign Drawer" button until the prerequisites have been met? If I do, then I will have to call the AllowUnassignDrawer method as the state of the application (the requisite conditions) changes in order to enable/disable the button. Whenever the prerequisites are checked there is overhead associated with querying the database. Even if this overhead takes only 3 seconds to check the multiple conditions, it seems wise to query the database as infrequently as possible. Futhermore, since I have to call the AllowUnassignDrawer method in order to disable/enable the button and then call it again from within the UnassignDrawer method itself (as a safegaurd against allowing the drawer to be unassigned if the button is ever inadvertently enabled when it should not have been), it seems that it may be necessary to call the AllowUnassignDrawer method twice. Presently, I leave the "Unassign Drawer" button enabled (so long as the teller has a drawer assigned). Then when the teller clicks the button, I report any failed prerequisites in a panel at the side of the screen. The teller then performs the tasks to meet the prerequisites before clicking the "Unassign Drawer" button again. My thinking was: If I go ahead and disable the button while the prerequisites are not met then I must be responsible for calling the prerequiste checking function (AllowUnassignDrawer) more frequently in order to maintain the enable/disabled status (even if the user does not wish to unassign the drawer). Conversely, when I call the prerequisite checking function only when the user wishes to take the action, I alleviate the overhead of maintaining the enabled/disabled status of the button. Furthermore, by perpetually maintaining the button status I may confuse the user who will wonder why he is unable to unassign his drawer (because he cannot click the disabled "Unassign Drawer" button in order to review the prerequisite checklist). If I choose to provide some other option for reviewing the checklist, where would it best be located? In my opinion, it would best be located as close to the "Unassign Drawer" button as possible so that the user can easily determine why the button is disabled in the first place. The other issue with my AllowUnassignDrawer method is that it returns a boolean value indicating whether or not the drawer may be unassigned. Since the scenerio at hand deals with potentially numerous prerequisites the one fact (the true/false response provided by the AllowUnassignDrawer method) is insufficient. I need some other output to communicate the various reasons (the failed prerequistes) to the user. In my case, I invoke an event to display a filled out checklist. Since the AllowUnassignDrawer method doesn't return the checklist (it invokes the event), the event listener (which is the application's primary form itself) must then go ahead and build the checklist thereby calling the database again for each of the prerequisite conditions. Obviously, I could have built more elaborate event argument/handler classes to communicate the checklist conditions directly to the listener. I'm not concerned at all with this specific example. I provided it only as framework on which to more clearly communicate the issue at hand. In my case I have a method that facilitates a business task (UnassignDrawer), a method that tests for permission to perform the particular business task (AllowUnassignDrawer), and an event that is subscribed to by the primary form in order to display the numerous prerequisite conditions to the user (the user interface). This is one design that evolved as the application evolved. It wasn't one that I fully thought out and planned in advance. I make this post to faciliate a discussion on the best practices/patterns surrounding the handling of prerequisite conditions associated with specific business tasks. Your ideas and practical examples are appreciated. Mario T. Lanza Clarity Information Architecture, Inc. |
|
|||
|
Re: Best Practices for Checking Prerequisites
Mario T. Lanza wrote: .... snip ... > In my case I have a method that facilitates a business task > (UnassignDrawer), a method that tests for permission to perform the > particular business task (AllowUnassignDrawer), and an event that is > subscribed to by the primary form in order to display the numerous > prerequisite conditions to the user (the user interface). This is one > design that evolved as the application evolved. It wasn't one that I > fully thought out and planned in advance. I make this post to > faciliate a discussion on the best practices/patterns surrounding the > handling of prerequisite conditions associated with specific business > tasks. > > Your ideas and practical examples are appreciated. Patterns: Observer (esp. MVC), Adapters, Checks Practice: Separation of concerns a) Build a complete and consistant model of the system. By "complete" is meant "has interfaces which allow all needed manipulations and/or which produce any and all data which is of interest to the outside world." By "consistant" is meant "every manipulation available through such an interface either succeeds (prereqs are met, invariants are maintained, change is applied, and any data provided to outside world from now on reflects the change), or fails (prereqs not met, changes not applied, any data provided to outside world remains as it was before the attempted manipulation)". b) When finished, attach Views and Controllers to it. If you have first accomplished (a), then (b) is nearly trivial. As an example, the notion of a button which is enabled only when all prereqs are met, becomes: Attach a controller which sends "releaseDrawer" whenever it is both enabled and pressed. Attach a view which observes the "drawerIsReleaseable" boolean, and which enables/disables the aforementioned controller (button) accordingly. Note that "drawerIsReleaseable" is a boolean maintained by the model. If you keep this in mind, you needn't "ask permission", you can instead "observe availability". This boolean *must* be changed (recalculated) whenever anything it depends on is changed -- it *can* be recalculated more often than this (such as every time a caller needs the current value) -- but it *must* be recalculated whenever it could actually change. You are free (as designer) to pick any frequency of update between these two extremes. Such choices affect "performance" and "usability", but not correctness. MVC allows one to optimize update frequency. Another question you can ask yourself is whether the "releaseDrawer" function is distinct, or just one of the subtasks the user must complete. Consider keeping a list of unfinished subtasks (in the model), each of which is enabled/disabled according to the current state of the model. Consider keeping the list sorted in "a proposed order", such that the "releaseDrawer" subtask is always last in the list. Attach controllers/views as before, observing this list. Regards, -cstb |