Beefy Boxes and Bandwidth Generously Provided by pair Networks
more useful options
 
PerlMonks  

RFC - Parameter Objects

by Ovid (Cardinal)
on May 14, 2003 at 16:57 UTC ( [id://258164]=perlmeditation: print w/replies, xml ) Need Help??

I've been reading through the book Refactoring: Improving the Design of Existing Code by Martin Fowler and noticed that one of his refactorings, Parameter Objects, was fairly easy to implement as a generic module. Whether or not people would use this is another question. I've seen several modules that do similar things, but not quite this thing. If this is duplicated effort, please let me know.

You can download the module from my site.

POD follows.


NAME

Sub::ParamObject - Perl extension for creating parameter objects


SYNOPSIS

  use Sub::ParamObject;
  my %params = (
    type  => qr/Order|Return/,
    month => sub { my $month = shift; return grep { /$month/ } (1 .. 12) }
  );
  my $param = Sub::ParamObject->new(\%params);
  $param
    ->set_type('Return')
    ->set_month(3);
  print $param->type;  # prints 'Return'
  print $param->month; # prints 3
  # the following two method calls will fail (returning false)
  $param->set_type('Ovid');
  $param->set_month(13);


DESCRIPTION

When working with large programs, we often find that we're passing the same parameters to multiple subroutines. We also have to remember to validate those parameters in every subroutine. Rather than do this, we can create a ``parameter object'' (see Martin Fowler's book 'Refactoring') that allows us to pass those parameters as a unit. Further, this parameter object can take the data and apply standard validations, thus ensuring that you do not have to repeat the validation every time.

The benefit results in smaller parameter lists and less repetition of validation code. Further, when parameter groups are created and used, it may provide insight into behaviors that should logically be moved to other classes.

Sub::ParamObject is a generic mechanism for creating parameter objects.


CONSTRUCTOR

The constructor expects a hash reference as an argument. It will croak if one is not supplied. The keys are the parameter names and the values must either be a regular expression to validate arguments against, or an anonymous subroutine that takes returns true or false for argument validation.

  my %params = (
    type  => qr/Order|Return/,
    month => sub { my $month = shift; return grep { /$month/ } (1 .. 12) }
  );
  my $param = Sub::ParamObject->new(\%params);

The constructor croaks if the hash values are not a regex or code reference. Returns an object upon success.


DYNAMIC METHODS

Mutators

Each key in the argument hash will be transformed into a mutator by prepending the name with 'set_'. Any value supplied to the mutator must successfully validate against the value supplied to the key. Upon success, the mutator will return the object. Upon failure, it will simply return.

Because the object is returned upon success, mutators may be chained:

  $param
    ->set_foo(7)
    ->set_bar('Higher')
    ->set_baz('whuzzup?');

This is equivalent to:


  $param->set_foo(7);
  $param->set_bar('Higher');
  $param->set_baz('whuzzup?');

The only difference between the two is that with the first method, you don't need to explicitly test every mutator for success. Passing a bad argument will return false, thus ensuring that the subsequent method call in the chain fails.

Note that because the argument to the mutator must be a single value. Thus, if you need to supply something like a array or a hash, you will need to pass a reference to it and you'll have to use subroutine validation instead of regex validation. Maybe I'll change this in the future?

Accessors

Each key in the argument hash will be transformed into an accessor with the same name as the key.

  sub something {
    my $param_object = shift;
    my $foo = $param_object->foo;
    my $bar = $param_object->bar;
    ...
  }

Note that once the parameter object is passed, validation is not necessary because that happened when the values were set.

EXPORT

Nothing.


AUTHOR

Curtis ``Ovid'' Poe, <poec@yahoo.com>


BUGS

Probably.


SEE ALSO

perl.

``Refactoring'', by Martin Fowler. Published by Addison Wesley.

Cheers,
Ovid

New address of my CGI Course.
Silence is Evil (feel free to copy and distribute widely - note copyright text)

Replies are listed 'Best First'.
Re: RFC - Parameter Objects
by fruiture (Curate) on May 14, 2003 at 18:08 UTC

    Being into OO in general, i'd say a certain collection of data that is used more than once should be made an object anyway, so i'd create a class supporting only accessors and mutators and no further methods. That's already close to your idea, only that each object is it's own class. That's where Class::Object and Class::Classless cross my thoughts, but these are too general for Paramtereobjects. So I'd suppose not Sub::ParamObject but Class::(Object|Classless)::ParamObject as name, although it might make people think it's built on top of one of the two. It could be, somehow.

    --
    http://fruiture.de

      Each in its own class? Aack! Why didn't I see that bug coming? If more than one parameter object is used, identically named parameters would conflict. To get around this, the simplest thing that could possibly work could be for me to just auto-increment the class name of the parameter object. However, building this on top of Class::Object does look like a nice way to go. However, I might have a problem with the resulting object already having new() and sub() methods.

      Cheers,
      Ovid

      New address of my CGI Course.
      Silence is Evil (feel free to copy and distribute widely - note copyright text)

        Yep, a counter is also Class::Object's method to create unique classnames. As for the new() and sub() methods: I don't think they'd conflict with the purpose of the parameter object, even more they are very usefull for introducing hossman's validation methods (why limit this to one isValid, why not isValidForPurposeA and isValidForPurposeB ...) and to derive objects from objects. To me, it's just fulfillment of concept ;)

        --
        http://fruiture.de
Re: RFC - Parameter Objects
by hossman (Prior) on May 14, 2003 at 18:16 UTC
    three minor comments...
    • Why make the mutators "set_foo" ... why not just overload "foo" (ie: foo with args does a set, and returns true if-and-only-if the args are valid, without args returns whatever the parameter value(s) should be.
    • Frequently when I've needed to use Parameter Objects (or "Keys" as their sometimes called) there are acctually two issues of validity:
      1. Are the individual parameters valid?
      2. is the combination of parameters valid?
      ... you've addressed the first, but not the second. There are lots of examples of when this becomes crucial, but the the most basic is: what if none of the set methods are called?" Typically the easiest solution is to have a unified "isValid" method on all of your Parameter Objects that returns true if-and-only-if all of the required parameters have been set, and there is "internal consistency" among the parameters, then any method that wants to take in a Parameter object doesn't need to do it's own validation, it can just call $param->isValid() However ... in cases where i've seen this done, it works becaue the methods expect a particular sub class of the "Parameter Object" class, and each sub class defines it's own isValid method. Based on your design, I'm not really sure what the best way to do this would be (because whoever calls "new Sub::ParamObject" can pass whatever rules they want.
    • Your example validation for month has a slight bug in it...
      my %params = ( type => qr/Order|Return/, month => sub { my $month = shift; return grep { /$month/ } (1 .. 1 +2) } );
      ..that would allow me to pass '' as a valid month.

      I have to admit that I've never cared for overloaded methods that function as both accessors and mutators, but in this case, it might make sense.

      The combination of parameters can be dealt with if I pass $self to the subroutines used for validation. Then, each item can check the other values, but this could make things order dependant (untested).

      my $param = Sub::ParamObject->new({ foo => qr/\d+/, bar => sub { $_[0]->{foo} > 3 && $_[1] =~ /^this|that$/ } }); $param ->foo(7) ->bar('this'); # succeeds $param ->foo(2) ->bar('this'); # fails

      And thanks for the bug catch!

      Cheers,
      Ovid

      New address of my CGI Course.
      Silence is Evil (feel free to copy and distribute widely - note copyright text)

        While this is nice and convenient for those cases where you have independently valid parameters, it's impossible (or dangerous, you choose) to have pairs (or more) of parameters that are valid together :

        my $ratio = Sub::ParamObject->new({ nominator => qr/^[-+]?\d+$/, denominator => sub { $_[1] =~ qr/^\d+$/ and $_[1] != 0 }, }); # works and is convenient $ratio->nominator(-1); $ratio->denominator(1); my $source = Sub::ParamObject->new({ selector => qr/^filename|url$/, filename => sub {$_->[0]->{selector} eq 'filename' and $_[1] =~ +qr/^\w+$/ }, url => $_->[0]->{selector} eq 'filename' and $_[1] =~ qr!^h +ttps?://!i }, }); # and now ??? # I want to have selector set to one of the values # and (only) the corresponding field set. $source->selector('filename')->filename('foo.txt'); # works $source->filename('foo.txt')->selector('filename'); # cannot in my i +mplementation

        Either you make the approach dependent on the order of parameters (plausible but ugly IMO), or you find another approach to validation (which I would rather welcome, since I did stuff like this years ago and didn't find a good solution to this).

        perl -MHTTP::Daemon -MHTTP::Response -MLWP::Simple -e ' ; # The $d = new HTTP::Daemon and fork and getprint $d->url and exit;#spider ($c = $d->accept())->get_request(); $c->send_response( new #in the HTTP::Response(200,$_,$_,qq(Just another Perl hacker\n))); ' # web
        That approach wouldn't solve the main case I pointed out where it becomes important to have validation...
        what if none of the set methods are called?
        ...ie: what if someone constructs a Params object, but never sets any values in it? Then there is no validation of any kind.

      hossman wrote:

      Why make the mutators "set_foo" ... why not just overload "foo" (ie: foo with args does a set, and returns true if-and-only-if the args are valid, without args returns whatever the parameter value(s) should be.

      Sorry to come into this late, but I didn't feel the question was answered. There are a couple related reasons not to overload mutators or accessors.

      Most of the time, coders are aware whether they intend to call a mutator in "set" or "get" mode. This awareness happens at coding time.

      Having separate mutators for "set" and "get" allows code to be explicit about its intentions. Mutators may be prototyped such that accidentally supplying or omitting a parameter is a compile-time error rather than a runtime misbehavior. If prototypes aren't desirable, separate mutators still makes mismatches between design and code more visible.

      To illustrate that last point, compare misused overloaded mutators with misused explicit ones.

      $thing->foo(); $foo = $thing->foo($bar);

      vs.

      $thing->get_foo(); $foo = $thing->set_foo($bar);

      The latter form of each example is legal code, and only the surrounding context will determine whether they are errors.

      An obvious solution is to check the number of parameters given against the mutator's runtime context. The resulting code might look like this.

      sub foo { my $self = shift; if (@_) { if (defined wantarray) { croak "can't expect a return value when setting foo"; } # store foo = shift; } else { unless (defined wantarray) { croak "must expect a return value when fetching foo"; } # return foo member here } }

      That has its own drawbacks.

      "set" mutators cannot be chained. While foo() might return $self in "set" mode, the wantarray() check would make it an error to actually use it.

      "set" mutators cannot be allowed to return their members' new values. Even if they did, the wantarray() checks would prohibit code like print $thing->foo($new_value);

      It's still a runtime error. Ideally every line of a program should be instrumented before it goes into production. Pragmatically speaking, compile time errors are better at preventing bogus code from being released.

      Even if these problems are not an issue for a particular application, every mutator has gained an enormous amount of avoidable runtime overhead.

      -- Rocco Caputo - troc@pobox.com - poe.perl.org

Re: RFC - Parameter Objects
by chromatic (Archbishop) on May 14, 2003 at 18:29 UTC

    Interesting. I've often thought that one failing of type checking parameter lists is that they can only tell you if a parameter fits in a type hierarchy somehow, not if the value of the parameter is actually valid.

      I actually got that idea from my CSV Database Validation program. For that, you can specify a regular expression as a "data type" for a database field. In thinking about this more, I realized that by arbitrarily passing in subroutines to validation routines, rather than try to hard-code methods that cover every possible case, I can let the programmer specify those in advance via sub references. It seems like a much more powerful system, the code is shorter, and you only build what you need.

      I do agree with you about the failings of parameter lists. Design by Contract seems to be one attempt to get around this weakness.

      Cheers,
      Ovid

      New address of my CGI Course.
      Silence is Evil (feel free to copy and distribute widely - note copyright text)

Re: RFC - Parameter Objects
by Juerd (Abbot) on May 15, 2003 at 06:50 UTC

    I've used the approach where every set of parameters has its own class.

    use Attribute::Property; { package Param::function; sub new : New; # XXX - Both need anchors? sub type : Property { /Order|Return/ } sub month : Property { grep /$_[1]/, 1 .. 12 } } sub function { my $self = shift; ... }
    After which you can do
    my %params = ( type => qr/Order|Return/, month => sub { my $month = shift; return grep { /$month/ } (1 .. 1 +2) } ); my $param = Param::function->new(\%params); $param->type = 'Return'; $param->month = 3; print $param->type; # prints 'Return' print $param->month; # prints 3 # the following two method calls will fail (croaking) $param->type = 'Ovid'; $param->month = 13;
    But I dislike param objects. The idea is great, but it's too much work in practice. Not because I created many classes, but because it's just too much work to create objects every time you want to pass parameters. I ended up using
    my $param = @_ == 1 ? shift : Param::function->new({ @_ });
    everywhere and not actually using the parameter classes.

    Juerd # { site => 'juerd.nl', plp_site => 'plp.juerd.nl', do_not_use => 'spamtrap' }

      Juerd wrote:

      But I dislike param objects. The idea is great, but it's too much work in practice. Not because I created many classes, but because it's just too much work to create objects every time you want to pass parameters.

      That just means I need to update the POD to make it more clear what these objects are for. Thanks!

      Parameter objects aren't for every subroutine or method. Instead, imagine the following code:

      sub foo { my ($quantity, $mangled, $item, $color, $puppies) = @_; ... } sub bar { my ($thing, $quantity, $color, $item) = @_; ... } sub baz { my ($quantity, $mushroom, $color, $item, $limit) = @_; ... } sub quux { my ($quantity, $color, $item, $kittens) = @_; ... }

      Now imagine that those are four subroutines out of about 30. If the identically named variables are truly identical, but we start to get large parameter lists (this is frequent when we're passing parameters through a chain of functions and wind up with tramp data) then we have a "data clump". This clump can be grouped in one parameter object with a standard set of validations applied. This reduces the number of arguments to the various subroutines and makes it less likely that we'll forget to validate the variables.

      In the above example, we may simply have one parameter object encapsulating the quantity, color, and item. We won't have thirty parameter objects. These objects are merely a refactoring tool to lower the amount of code duplication (see Martin Fowler's book on refactoring for more information on parameter objects).

      Another benefit of parameter objects is that it might make it clear that another class is required. In the above example, it might be the case that quantity, color and item can be grouped into a "Product" class or something similar. However, when these parameters are constantly separated, such redesign of code may not be apparent.

      Cheers,
      Ovid

      New address of my CGI Course.
      Silence is Evil (feel free to copy and distribute widely - note copyright text)

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://258164]
Approved by particle
Front-paged by jeffa
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having an uproarious good time at the Monastery: (5)
As of 2024-03-28 23:14 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found