Originator: Patrick Schoenbach and Ralf Engels
This challenge is a small one, but it comes up fairly often in various forms, so I figure I will nip it now.
Patrick: "This raises another issue: Imagine you want to write an "external library" as you call it and you don't want to provide the customer with the source code. Since p/r [procedural] does not have subtyping, is it possible to extend the library with new or modified behavior without changing the source code? If so, how would you design it?"Here is one approach (but not the only):
sub processX(a,b,c) compiled_processX(a,b,c) end subRoutine processX is made available to the customer, but not the source for compiled_processX. Thus, the customer can:
Note that "process_X" should be the routine called in the application and not "compiled_processX". (Naming conventions shown are for illustration only.)
I sometimes do something like this to provide my own output routines. For example, in Microsoft Web programming tools, the output method call is "response.write". Rather than use that, I define my own:
sub hout(theText) response.write theText end sub ... hout "Hello World!" hout "Tables our your friend!" // instead of response.write "Hello World!" response.write "Tables our your friend!"This not only reduces the bloat caused by the OOP method syntax ("hout" is shorter than "response.write" and thus makes the code easier to read), but also allows me to do things like:
sub hout(theText) response.write theText LogIt theText, timeNow() // echo to a log end sub
In most cases creating stubs would be left to the package user rather than be supplied with the package. Further, overriding in OOP also requires the creation of at least one new child class and at least one new method for every method to be overridden.
Further, in most cases the differences (overrides) are not going to shared globally, but instead applied differently in different situations. Thus, there would be no ProcessX() routine because the behavior would be added to (or ignored) differently per situation, often in-line rather than a separate routine.
The difference is that OO would probably try to find a taxonomy to the differences, which gets us right back to the taxonomy issues and/or method granularity issues.
Another potential caveat is that the granularity (range) of what you want to change may be smaller than the routine you want to override. For example, you may want to override only half of the original behavior. However, OOP often suffers the same problem.
// Original "out-of-the-box" routine: sub overrides(subject, action, thing, target) doOrig = true // default // insert your overrides here return doOrig end sub // Example *after* some typical custom behavior changed/added sub overrides(subject, action, thing, target) doOrig = true // default // Sword grows larger if attacking a wizard if thing="sword" and action="attack" and target="wizard" scale(thing, 1.5) doOrig = true end if //---------- // replace sling rocks with magic dust if thing="slingRock" and action="obtain" thing = "magicDust" doOrig = true end if //---------- // Spupify both characters if wand is // used, but stupify target a bit longer if thing="wand" and action="attack" disable_character(subject, 4) disable_character(target, 8) doOrig = false end if //---------- ...etc... return doOrig end subThis is a simplified example, but illustrates the general idea. All standard actions would call this routine. The "doOrig" flag allows one to optionally include (continue) the original behavior.
Another variation for managing "parent" behavior is to optionally call the original action, similar to the prior ProcessX example. This may allow "before" and "after" overrides. The "before" and "after" could instead be specified in one of the parameters, a "stage" parameter similar to handling a "pre" and "post" stage in data validation customization routines. One may want to have a separate override routine for each stage. However, this makes certain logical groupings harder to keep together in my experience. Anyhow, the possibilities and choices for building such protocols is large.Note that this approach in some ways resembles rule-based Expert Systems (an artificial intelligence topic). See also Block Discrimination if you have complaints about lots of IF blocks. Why are lots of IF blocks more evil than lots of method blocks?
As a side note, role playing systems seem like they would be a great use of tables. It would be a lot easier to manage a huge cast of characters and actions via table(s) than in code. Often times the table can be used to manage "standard" features, and then a routine like the one above can be used in cases where the standard features don't provide enough specific control. I have used this approach for data dictionaries in form generation and validation systems and components.
The combination would also allow you to use sets to organize characters and actions rather than (or in addition to) hierarchies. For example, default wizards or warlocks could belong to both the "magic" group and to the "human" group. These features could be a checkbox (Boolean) in the character table, perhaps. Some overrides (or even standard operations) may do something to all characters with magical powers, and then later do something to all human characters. Later, a given warlock may be turned into a toad, but still have magical powers. OO sub-class hierarchies often have a tough time managing such orthogonal traits and action dispatching criteria. The above approach allows one to use AND's, OR's etc. to select a wider variety and combinations than hierarchies alone.
Just something extra to think about the next time you build a game.