mstone has asked for the wisdom of the Perl Monks concerning the following question:
Hey all -
I've been kicking around some OO design ideas, and have come up with a data storage class well suited to being the Model in a Model-View-Control architecture. It uses an embedded data hash and a table of functions to reduce the whole access interface to the functions set() and get().
Thing is, I'm not completely happy with the lookup tables.
The sample class looks like so:
package Data_holder; =head CONSTRUCTOR Nothing terribly exciting here. All we do is bless a hashref, embed a data hash, and store two hashes that map attribute names to the names of the methods that will handle them. =cut sub new { my $O = bless {}, shift; $O->{'data'} = {}; $O->{'get-funcs'} = { 'key-1' => 'get_normal', 'key-2' => 'get_synthetic', }; $O->{'set-funcs'} = { 'key-1' => 'set_normal', 'key-2' => 'set_synthetic', }; return ($O); } =head1 DISPATCH FUNCTIONS =over 4 =item get (list) : hash This is the inspection routine. It takes a list of attribute names as input, and returns a hash of named values as output. It does its job by calling functions identified in {'get-funcs'}. =cut sub get { my ($O, @keys) = @_; my $result = {}; for my $k (@keys) { my $func = $O->{'get-funcs'}->{ $k } || 'get_error'; $result->{ $k } = $O->$func ( $k ); } return ($result); } =item set (hash) : hash This is the storage function. It takes a hash of named values as input, and returns a hash of result codes as output. It does its job by calling functions identified in {'set-funcs'}. =cut sub set { my ($O, $args) = @_; my $result = {}; for my $k (keys %$args) { my $func = $O->{'set-funcs'}->{ $k } || 'set_error'; $result->{ $k } = $O->$func ( $k, $args->{ $k } ); } return $result; } =back =head1 INSPECTION METHODS These are the functions that do the actual work of getting values. They're called by get() when it finds their names in {'get-funcs'}. =over 4 =item get_normal (key) : value This routine simply pulls the named value from the emedded hash. =cut sub get_normal { my ($O, $key) = @_; return ($O->{'data'}->{ $key }); } =item get_synthetic (key) : value This routine is a little more interesting. It calculates the return value from some other value or combination of values in the embedded data hash. In this case, we'll calculate the farenheit value of a stored centigrade temperature. This is also just an example. In a real version of this class, you'd have a different function for every value you wanted to calculate. =cut sub get_synthetic { my ($O, $key) = @_; my $result = 32 + (1.8 * $O->{'data'}->{'temperature'}); return ($result); } =back =head1 STORAGE METHODS These are the functions that do the actual work of setting values. They're called by set() when it finds their names in {'set-funcs'}. =over 4 =item set_normal (key, value) : result This is the basic storage routine. It drops the value into the embedded data hash under the assigned key. It also returns a success code since there's no way that operation can fail. =cut sub set_normal { my ($O, $key, $val) = @_; $O->{'data'}->{ $key } = $val; return ('SUCCESS'); } =item set_synthetic (key, value) : result Like get_synthetic(), this is a sample routine. Here we store the centigrade value of a temperature entered in farenheit. =cut sub set_synthetic { my ($O, $key, $value) = @_; $O->{'data'}->{'temperature'} = ($value / 1.8) - 32; return ('SUCCESS'); } =back =head1 ERROR ROUTINES These are the catch-all routines. They handle keys that don't have set() or get() functions defined in the lookup tables. In theory, you could use these routines to replace the get_normal() and set_normal() operations, but I really don't like that idea. It makes typos *way* too powerful for my taste. I prefer to define my interfaces explicitly. =over 4 =cut sub get_error { my ($O, $key) = @_; return ( "FAILURE - " . "I don't know how to locate the value for '$key'." ); } sub set_error { my ($O, $key, $val) = @_; return ( "FAILURE - " . "I don't know how to set '$key' to '$value'." ); }
You can also throw in a table of data validation functions, and write set_normal() like so:
sub set_normal { my ($O, $key, $val) = @_; my $func = $O->{'validate-funcs'}->{ $key }; my $result = $O->$func ($val); if ($result eq 'SUCCESS') { $O->{'data'}->{ $key } = $val; } return ($result); }
You can use a synonym table to make the same values accessible by multiple names:
sub new { {...} $O->{'synonyms'} = { 'rm' => 'REQUEST_METHOD', 'q' => 'QUERY_STRING', {...} }; {...} } sub get { my ($O, @keys) = @_; my $result = {}; for my $k (@keys) { my $real_key = $O->{'synonyms'}->{ $k } || $k; my $func = $O->{'get-funcs'}->{ $real_key } || 'get_error' ; $result->{ $k } = $O->$func ($real_key); } return ($result); }
And you can trick out the dispatch functions with argument-list validation code to enforce rules about data access.. setting things up so you can only set 'v1' if you also set 'v2', for instance.
Overall, I'm happy with the design.
Thing is, I have this semi-irrational prejudice against using runtime evaluation to identify functions.. i.e.: doing this:
$O->$func ($key);
I know that it works. I know that I'm doing it in a reasonably disciplined way. I'm cool with the idea of treating functions as first-order items of data. And I really like the simplicity of the notation. I just have this deeply-ingrained attitude that says program structure should be defined at compile time.
There is a way to get the same result without doing the runtime evaluation: build a family of lightweight Command classes and use those in the lookup tables rather than strings:
package Command; sub new { return (bless {}, shift); } sub execute { } package Get_normal; @ISA = qw( Command ); sub execute { my ($O, $target, $key) = @_; $target->get_normal ($key); return; } package Data_holder; sub new { {...} my $g = new Get_normal; $O->{'get-funcs'} = { 'key-1' => $g, {...} } {...} } sub get { {...} my $cmd = $O->{'get-funcs'}->{ $k } || $O->{'get-funcs'}->{'get_error'} ; $result->{ $k } = $cmd->execute ( $k ); {...} }
The effect is exactly the same, but this time the method invocations are wired directly into the source. The branching ends up getting built into the syntax tree, rather than being determined at runtime. This version could be ported to C++ where the previous version couldn't.
But for cripe's sake, look at all the extra code! And the coupling between the main class and all the command classes is just plain ugly.
I'm trying to convince myself that the first version is okay.. roughly on the order of using macros in Lisp. I know that any language worth using offers certain high-voltage techniques that should be avoided when it's at all convenient to do so, but used with respect when the benefit outweighs the risk.
I think that covers the situation I'm dealing with here. The fact that I do know how to write the package with a C++ accent means it's probably safe to call the first version an idiom. All I have to do is remind myself that runtime evaluation is the short way of saying something I'd have to spell out more thoroughly in C++, and offer a quick prayer of thanks to Larry for saving me all that extra typing whenever I use the technique in the future.
But I'm still not completely convinced. We're talking about symbolic references here, and the short course on that subject is:
Which brings us to the point of this query. Does anyone else have a tidier way of storing method invocation in a lookup table, or is this particular way of using symbolic references basically okay?
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: Need help with a conceptual speed bump
by chromatic (Archbishop) on May 03, 2005 at 22:00 UTC | |
by mstone (Deacon) on May 03, 2005 at 23:22 UTC | |
by chromatic (Archbishop) on May 04, 2005 at 01:02 UTC | |
|
Re: Need help with a conceptual speed bump
by tilly (Archbishop) on May 03, 2005 at 23:04 UTC | |
by mstone (Deacon) on May 03, 2005 at 23:33 UTC | |
by tilly (Archbishop) on May 04, 2005 at 00:01 UTC | |
by mstone (Deacon) on May 04, 2005 at 00:41 UTC |