home *** CD-ROM | disk | FTP | other *** search
/ Liren Large Software Subsidy 5 / 05.iso / a / a066 / 1.img / DBROWSE.DOC < prev    next >
Encoding:
Text File  |  1992-03-20  |  16.6 KB  |  348 lines

  1. This file contains a description of the TBrowse inheritance example
  2. provided, in the files DBROWSE.PRG and DBROWDEM.PRG.  This was adapted
  3. from an article in Reference(Clipper), August 1991.
  4.  
  5. Executing the batch file MAKEALL.BAT will create the demonstration program,
  6. DBROWDEM.EXE.  It will have debug information included, so is interesting to
  7. trace thorugh in the Clipper debugger.
  8.  
  9.                 ---------------------------------------
  10.  
  11.  
  12.                   dBrowse: Inheritance from TBrowse
  13.                   =================================
  14.  
  15.  
  16. The pulldown menu example in the manual is highly object-oriented,
  17. consisting of five interacting object classes.  This provides an
  18. interesting view of how such a system can be structured.  But few of us are
  19. in a position to begin writing completely object-oriented Clipper systems
  20. right now - although some people are successfully doing so.
  21.  
  22. This document discusses a more immediately practicable example of using
  23. user-defined classes in a Clipper program.  We are going to 'inherit' a new
  24. class from the built-in Clipper class, TBrowse, resulting in a class which
  25. is more complete, easier to use, and without sacrificing flexibility.
  26.  
  27. I have not attempted to create an "ultimate" browsing class here.  Instead,
  28. I have taken a piece of code which will be familiar to many readers -
  29. Nantucket's sample program TBDEMO.PRG - and reworked it as a new class,
  30. called dBrowse, inherited from TBrowse.
  31.  
  32. TBDEMO.PRG can be found in Clipper 5's SOURCE\SAMPLE subdirectory.  The
  33. version I am discussing is the Clipper 5.0 version, _not_ the 5.01 version.
  34.  
  35. It uses the TBrowse class to do a fairly simple database browse, with
  36. editing.  The majority of the code in TBDEMO is devoted to setting up and
  37. using a TBrowse object.  Essentially, TBDEMO 'specializes' the behaviour of
  38. TBrowse, by providing a specifically database oriented browsing capability.
  39.  
  40. It is not a coincidence that this exact kind of specialization is a major
  41. characteristic of any inherited class in a well designed object-oriented
  42. system.
  43.  
  44. What most of the code in TBDEMO is saying, in no uncertain terms to those
  45. willing to listen, is that it should be implemented as a subclass of
  46. TBrowse.  It makes little sense to revert to procedural programming, 'just
  47. when we could most use object orientation to manage the complexity of a
  48. general class such as TBrowse'.
  49.  
  50. The Code
  51. --------
  52.  
  53. To start with, we have declared the dBrowse class as follows:
  54.  
  55.     create class dBrowse from TBrowse
  56.         instvar appendMode
  57.  
  58.     export:
  59.         method  autoFields
  60.         method  exec
  61.  
  62.         method  goBottom
  63.         method  goTop
  64.         method  skipper
  65.     endclass
  66.  
  67. So the dBrowse class 'inherits' all of the instance variables and methods
  68. of the TBrowse class.  In addition, a new instance variable and five new or
  69. changed methods are added.
  70.  
  71. The new instance variable, 'appendMode',  makes quite a subtle, but
  72. important difference to the code.  In the original TBDEMO, a variable
  73. called 'lAppend' is declared local to the MyBrowse() function, and is
  74. continually passed to other functions such as DoGet() and Skipper().  If
  75. MyBrowse() were broken up into smaller procedures, as has been done in the
  76. dBrowse class, 'lAppend' would have to be passed as parameters to them too.
  77. By making it an instance variable, it becomes accessible to all the methods
  78. in the class, and it no longer has to be passed as a parameter.
  79.  
  80. Cargo is a kludge!
  81. ------------------
  82.  
  83. It is interesting to note that Nantucket recognized this little problem
  84. with the 'lAppend' variable, and in the Clipper 5.01 version of TBDEMO,
  85. used the TBrowse 'cargo' slot to hold the append mode flag instead.
  86. Unfortunately, to retain readability and maintainability, they have had to
  87. use the preprocessor to hide the underlying instance variable name, as
  88. follows (taken with comments from the latest version of TBDEMO):
  89.  
  90.     // These #defines use the browse's "cargo" slot to hold the
  91.     // "append mode" flag for the browse. The #defines make it
  92.     // easy to change this later (e.g. if you need to keep
  93.     // several items in the cargo slot).
  94.     #define TURN_ON_APPEND_MODE(b)      (b:cargo := .T.)
  95.     #define TURN_OFF_APPEND_MODE(b)     (b:cargo := .F.)
  96.     #define IS_APPEND_MODE(b)           (b:cargo)
  97.  
  98. This approach can only lead to trouble and complication, and only serves to
  99. highlight the fact that the entire concept of the 'cargo' slot, in all four
  100. of Clipper's predefined classes, is a workaround for the fact that Clipper
  101. itself does not support user-defined classes and inheritance.
  102.  
  103. Overall program structure
  104. -------------------------
  105.  
  106. The original TBDEMO code consists of four functions, TBDemo(), MyBrowse(),
  107. Skipper(), and DoGet().  I will briefly discuss each function and how it
  108. was modified to fit into the dBrowse class.
  109.  
  110. Before we do that, though, there is one change which was made repeatedly
  111. throughout the file which we should examine.  In the original TBDEMO, there
  112. are about 50 occurrences of a message send to 'b', which is the variable
  113. containing the TBrowse object.  But with the new structure, TBrowse is no
  114. longer a foreign entity - it is dBrowse's parent class.  As such, all
  115. TBrowse methods needed in dBrowse can be invoked by sending a message to
  116. 'self'.  So all occurrences of 'b:' in the dBrowse methods were replaced
  117. with the double colon, '::', which is Class(y) shorthand for sending a
  118. message to 'self'.
  119.  
  120. The very fact that so many messages were being sent to the TBrowse object
  121. in TBDEMO was another strong hint that those functions should have been
  122. methods in the class or a subclass, such as dBrowse.
  123.  
  124. TBDemo()
  125. --------
  126.  
  127. This function demonstrates the use of the MyBrowse() function and does not
  128. require much explanation.  A separate module has been created for this
  129. purpose, called DBROWDEM.PRG.  Extracting the relevant portions from this
  130. module gives us something like this:
  131.  
  132.     local dBrow := dBrowse():new(5, 5, 15, 70)
  133.     dBrow:autoFields()
  134.     dBrow:exec()
  135.  
  136. We have implemented the browse in three lines here.  What each of these
  137. lines do is explained more fully below.
  138.  
  139. MyBrowse()
  140. ----------
  141.  
  142. This is the main browsing function.  It can be broken into three parts:
  143.  
  144. i.   A TBrowse object is created and some of its instance variables are
  145.      initialized.  In a class, this is usually the job of the constructor,
  146.      so we have extracted this code to the 'new' method in our dBrowse
  147.      class.
  148.  
  149. ii.  A loop is executed in which a TBColumn object is created for each
  150.      field in the currently selected database table.  This object is then
  151.      added to the TBrowse object using the 'addColumn' method.  To avoid
  152.      making the dBrowse class so specific that it is only capable of
  153.      browsing all fields in the current DBF, this section has been made
  154.      into a separate method, called 'autoFields'.  It is then under the
  155.      caller's control whether the columns to be browsed are taken from the
  156.      current DBF or from some other source.
  157.  
  158. iii. Finally, an event loop is entered which 'stabilizes' the browse while
  159.      waiting for a keystroke.  When a key is pressed, a CASE statement is
  160.      used to execute the appropriate action.  For dBrowse, this section has
  161.      been made into a method called 'exec', since it is when this method is
  162.      called that the browse is actually activated.
  163.  
  164. Skipper()
  165. ---------
  166.  
  167. This function controls moving the record pointer through the data.  This
  168. has been made into a dBrowse method, also called 'skipper'.  As mentioned
  169. earlier, the original second parameter, 'lAppend', is not required.  It is
  170. replaced by the instance variable 'appendMode' which is automatically
  171. available to 'skipper'.
  172.  
  173. DoGet()
  174. -------
  175.  
  176. This function handles the editing of individual cells, or fields, in the
  177. browse.  This has been made into a method, also called 'doGet'.  Unlike the
  178. original function, this method does not require any parameters.  The
  179. original first parameter, 'b', is replaced by the implicit 'self'.  The
  180. second parameter, 'lAppend', is replaced by the instance variable
  181. 'appendMode'.
  182.  
  183. Where did the code blocks go?
  184. -----------------------------
  185.  
  186. Two methods in the dBrowse class have not yet been mentioned, since they do
  187. not have direct equivalents in TBDEMO.  These are 'goTop' and 'goBottom'.
  188. They are 'replacements' for methods of the same name in the TBrowse class.
  189. This has been done more as an interesting exercise than because it is
  190. necessary.  In the TBrowse class, these methods operate by evaluating the
  191. corresponding code blocks contained in the instance variables 'goTopBlock'
  192. and 'goBottomBlock'.  This technique was needed to enable TBrowse to browse
  193. data from different sources, where the code to move to the top or bottom of
  194. the data would vary with the data source.  Here again, as with the 'cargo'
  195. instance variable, a workaround has been used to compensate for Clipper's
  196. lack of an inheritance capability - in this case by using instance
  197. variables containing user-supplied code blocks.  To prove that these are
  198. unnecessary, these methods were reimplemented without using code blocks.
  199.  
  200. Unfortunately, the same approach could not be taken with TBrowse's
  201. 'skipBlock' instance variable, since there is no single corresponding
  202. method which evaluates it.  Rather, 'skipBlock' is evaluated directly in a
  203. number of places inside TBrowse.  This forces us to have a level of
  204. indirection, using the 'skipBlock' code block merely to invoke our
  205. 'skipper' method.
  206.  
  207. How to use the class
  208. --------------------
  209.  
  210. The demonstration program, DBROWDEM.PRG, shows how a browse can be created
  211. and activated in only three lines.  This could easily be done in one line,
  212. but the advantage of the former approach is that we can manipulate the
  213. dBrowse object in other ways, before starting the event loop using the
  214. 'exec' message.  After all, dBrowse is a subclass of TBrowse, and as such
  215. has all of TBrowse's methods available to it.  For example, instead of
  216. invoking the 'autoFields' method, we could easily write a loop using the
  217. 'addColumn' message to create calculated fields, or to obtain data from
  218. multiple related DBFs.
  219.  
  220. What next?
  221. ----------
  222.  
  223. What has been done so far provides little more than a start towards a
  224. powerful yet easy to use browsing class.  The changes made to TBDEMO were
  225. deliberately kept to a minimum to illustrate how easy it can be to convert
  226. to a class-based design.  As a result, our dBrowse class is not really much
  227. more useful than the original TBDEMO.  The crucial difference is that it
  228. should be easier to expand and enhance - as we will now examine.
  229.  
  230. First of all, dBRowse actually goes too far, too quickly.  In one step from
  231. TBrowse, we have a class that implements an event loop, does field editing,
  232. and is tied to a database source.  However, the event loop is quite a
  233. general piece of code, which we could use in, say, an array browsing class.
  234. The obvious solution here is to have an intermediate class, which we will
  235. call GenBrowse, as illustrated in the following class hierarchy diagram:
  236.  
  237.                    ┌─────────────┐
  238.                    │   TBrowse   │
  239.                    └──────┬──────┘
  240.                           │
  241.              ┌────────────┴────────────┐
  242.              │        GenBrowse        │
  243.              │                         │
  244.              │ General purpose browsing│
  245.              │ class which implements  │
  246.              │ method exec - a default │
  247.              │ event handling loop.    │
  248.              └──────┬──────────────┬───┘
  249.               ┌─────┘              └─────────┐
  250.   ┌───────────┴──────────────┐    ┌──────────┴────────────┐
  251.   │        dBrowse           │    │      aBrowse          │
  252.   │                          │    │                       │
  253.   │ Database-oriented browse │    │ Array-oriented browse │
  254.   │ which implements methods │    │                       │
  255.   │ such as autoFields.      │    │                       │
  256.   └──────────────────────────┘    └───────────────────────┘
  257.  
  258.  
  259. With such a class structure, we can achieve maximum expandability with
  260. minimum rewriting of code.  An important point to remember when dealing
  261. with inheritance is that you should only have to program the exceptions and
  262. additions in an inherited class - if you end up duplicating code in
  263. different classes, their is probably something wrong with the design.  By
  264. moving the 'exec' method up into the GenBrowse class, we avoid such
  265. duplication.
  266.  
  267. To take this one step further, it can easily be seen that at the bottom of
  268. the above tree we will need to inherit a new class from GenBrowse for each
  269. different data source we wish to browse.  This is not totally sensible;
  270. after all, we are trying to develop a browsing class, not a data providing
  271. class - but the latter would in fact provide an excellent solution.  If we
  272. had a general purpose data providing class, with standard methods such as
  273. 'goTop', 'goBottom', and 'skip', we could have a subclass of this class for
  274. any type of data we might wish to handle - even, for example, SQL or
  275. Paradox data.  OOPS! (sic) - I think we've just invented RDDs - replacable
  276. database drivers!  Seriously, what this would buy us is the capability to
  277. implement a single general purpose browsing class which accepted a
  278. data-providing object as a parameter.  We could then browse any type of
  279. data using the same browse class.  In addition, we would not have to
  280. repeatedly implement data-providing methods in in every class which
  281. requires a data source - we would just pass data-providing objects around.
  282.  
  283. Event Handling
  284. --------------
  285.  
  286. Another area which could stand improvement is the key handling in the
  287. 'exec' method's event loop.  At present, the key mappings - which keys are
  288. handled and what they do - cannot be changed without overriding the method
  289. in a subclass.  To allow more flexibility, we could add an instance
  290. variable to the class to contain an array of paired values - a key value
  291. and a code block.  By setting this instance variable to a suitable array,
  292. the keyboard mapping can be changed at will by the call.  This subject will
  293. be discussed in more detail in a future article.
  294.  
  295. Field Editing
  296. -------------
  297.  
  298. There is one last slightly more mundane, but useful, enhancement which
  299. could easily be made to our browsing class system, relating to editing of
  300. fields in the browse.  The code for editing a field in TBDEMO and dBrowse
  301. is fairly complex - but what if we want a read-only browse?  We could
  302. override or otherwise disable the editing behaviour, but the unused code
  303. would remain in the class.  Thinking about it, a browsing class is not the
  304. right place to put field editing code, anyway.
  305.  
  306. A more logical place to implement an 'edit' method is in the TBColumn
  307. class (or a subclass), since that is where the contents of individual cells
  308. is decided.
  309.  
  310. So let's inherit two new classes from TBColumn - call them ROColumn and
  311. RWColumn - and implement an 'edit' method in each.  ROColumn (read-only
  312. column) would have a dummy, or null, 'edit' method which would do nothing,
  313. or perhaps just generate a beep.  RWColumn would have a fully functional
  314. 'edit' method, similar to the 'doGet' method currently in the dBrowse class
  315. (which would now become redundant).  In the generic event loop in the
  316. GenBrowse class, we could specify that hitting the ENTER key, say, would
  317. cause an 'edit' message to be sent to the currently selected column object,
  318. which could be either an ROColumn or an RWColumn object.  This would look
  319. something like the following:
  320.  
  321.         case nKey == K_ENTER
  322.             columnObj:edit
  323.  
  324. Editing would then take place, or not, depending on what type of column
  325. object had been inserted when the browse object was set up.  No IF
  326. statement is necessary here.  During setup, read-only and editable columns
  327. could easily be mixed in the same browse.
  328.  
  329. Summary
  330. -------
  331.  
  332. The system proposed here would provide a set of classes which would meet
  333. the requirement for a powerful, easy to use, and flexible browsing system.
  334. It would be easy to use because it would have default behaviours, requiring
  335. very little code to implement in a calling program.  It would be flexible
  336. because other behaviour could be implemented by inheriting new classes from
  337. the existing ones.  No functionality has been lost, because methods all the
  338. way up the tree to TBrowse can still be used as required.  Having an
  339. object-based solution also makes it easy to implemement multiple
  340. simultaneously active browses - something much harder to do with a
  341. procedural approach (without simulating objects).
  342.  
  343. The combination of all of the above capabilities is undeniably very
  344. powerful, and is a good example of the kind of benefits which can be
  345. achieved with object oriented programming.
  346.  
  347.  
  348.