Thursday, April 30, 2009

Modifying core method behavior without touching core code

The other day I was writing some patches for a custom product (see Developers: patch it yourself). One of the requirements is that the product code needs to be supported at 4, 4.1 and 4.1.1. The change is to add a new dynamic variable to the beginning of map_view.int!do_render().

Originally, I thought I would have to make three different versions of this patch file (one for each CST version that the product supports). That would seem to make sense because at each of the versions, the contents of the method could have changed slightly. As it turns out, the contents had changed between two of the versions.

Having three version-specific patches for a single method ends up being an administrative nightmare because each time a new TSB is released, I need to review all the changes in the TSB for conflicts with my patch code. In some cases this might be necessary because the patch makes significant changes in the middle of the method. But in many cases, the changes we want to put in are at the top or the bottom of the method. In those cases, I discovered that I could use method :define_method_synonym() to wrap my new code around the core code without touching any of the functionality in the core code...


# use define_method_synonym() to wrap some of our behavior
# around the core method without redefining the core behavior.
_if map_view.method(:original!int!do_render|()|) _is _unset
_then
map_view.define_method_synonym(:original!int!do_render|()|,:int!do_render|()|)
_endif
$
_pragma(classify_level=restricted)
_method map_view.int!do_render( _optional post_render_set )
##
## Draws the current visible set in the render thread.
##

_dynamic !ve_connector_current_map_view! << _self

_return _self.original!int!do_render(post_render_set)
_endmethod
$


And if you use the _gather/_scatter keywords, your wrapper does not even need to worry about whether the arguments for a method change from version to version.

This technique is also useful for putting debug code into unshipped source code. Let's say I want to get a traceback every time I get a user_error dialog with the word "coordinate" in the dialog message. The dialog is actually activated somewhere in basic_window.show_alert(). This method code, however, is unshipped so your test code might look like...



_if basic_window.method(:original!show_alert|()|) _is _unset
_then
basic_window.define_method_synonym(:original!show_alert|()|,:show_alert|()|)
_endif
$
_pragma(classify_level=restricted, usage={redefinable})
_method basic_window.show_alert(message, _optional yes_message, no_message, default, mode)

_if message.canonical.index_of_seq("coordinate") _isnt _unset
_then
!traceback!()
_endif

_return _self.original!show_alert(message,yes_message,no_message,default,mode)
_endmethod
$


NOTE: the method comments on :define_method_synonym() say that it is "severely deprecated." But until the time that this method is removed from the core code, I find it a very useful tool.

NOTE NOTE: While this technique does minimize the touch points of your code with the core code, you still need to be aware of the classifications (basic,advanced,restricted) of a method to understand how/if you should modify the method.

4 comments:

John Fowler said...

Alfred, I am looking at using this, it will work perfectly. Interesting note though: if you look at the comments for define_method_synonym, they say:
"Use of this method is severely deprecated. It defines a
second method which does the same thing as the first."
Not sure what a SEVERELY deprecated method means, I guess it's way more deprecated than a normally deprecated method.

Alfred Sawatzky said...

Hi John,

Reinhard Hahn suggested a way around this potential problem with define_method_synonym() being "severely deprecated"...


If the method :define_method_synonym() ever disappears, you could use the technique, I used up to now:

_method receiver_class.method_name!old(arg1, arg2, _optional arg3, _gather more)
## copy of _self.method_name()

# no code at all at this place!
_endmethod
$
receiver_class.method(:|method_name!old()|).value << receiver_class.method(:|method_name()|).value
$

which should be exactly equivalent to

receiver_class.define_method_synonym(:|method_name!old()|,:|method_name()|)

, if you have choosen exactly the same signature of your method.

Reinhard Hahn said...

Hi Alfred,

I just found out an a bit more sophisiticated application of the define_method_synonym() method:

For debugging purposes I wanted to know, on which way a certain attribute is set. Therefore I wanted to place a !traceback!(!output!) within the chevronned method of the field. The problem is, that I didn't find a possibility to define a method on the table's record_exemplar directly. (With 'record_exemplar' I mean that automatically generated class with that arbitrary number at the end of the class name.)

What I did is the following:


t << gis_program_manager.cached_dataset(:my_view_name).collection(:my_class_name)
$
t.descriptor.record_class.define_method_synonym(:|orig!my_field<<|,:|my_field<<|)
$
_method my_class_name.changed!my_field<< new_val
!traceback!(!output!)
_return _self.orig!my_field<< new_val
_endmethod
$
t.descriptor.record_class.method(:|my_field<<|).value <<
my_class_name.method(:|changed!my_field<<|).value
$


The trick is, that the method :|changed!my_value<<| isn't originally defined on that record_exemplar. But the last line of the code, assigns the value of that method (which is a procedure) to the value of the method on the record_exemplar.

Cheers
Reinhard

Reinhard Hahn said...

With Smallworld 5.0 (SWV) the method define_method_synonym() has been removed, indeed, but my variant still works. This holds even for methods on a record exemplar. In this case instead of writing:

t.descriptor.record_class.define_method_synonym(:|orig!my_field<<|,:|my_field<<|)

you now have to write:

_method my_class_name.orig!my_field << new_val
_endmethod

my_class_name.method(:|orig!my_field<<|).value << t.descriptor.record_class.method(:|my_field<<|).value