Class::BuildMethods

Ovid on 2005-11-23T20:41:28

Though I was not happy with the thought of adding another module to the CPAN Class:: hierarchy, I did. It's essentially a poor man's MethodMaker. Here are the features I needed in the order I needed them.

  1. It's implementation agnostic. You can used a blessed typeglob for all I care (most modules which build your methods assume you're using a specific type of blessed reference).
  2. Super flexible and optional data validation.
  3. Easy to specify default values.
  4. Very lightweight.

Note that the first item on my list was the most important yet I couldn't find any modules in this namespace which respected that requirement. I'm sure they're there, but where?

NAME
    Class::BuildMethods - Lightweight implementation-agnostic generic
    methods.

VERSION
    Version 0.10

SYNOPSIS
        use Class::BuildMethods 
            'name',
            rank => { default  => 'private' },
            date => { validate => \&valid_date };

DESCRIPTION
    This class allows you to quickly add simple getter/setter methods to
    your classes with optional default values and validation. We assume no
    implementation for your class, so you may use a standard blessed
    hashref, blessed arrayref, inside-out objects, etc. This module does not
    alter anything about your class aside from installing requested methods
    and optionally adding a "DESTROY" method. See "CLEANING UP" for more
    information, particularly the "destroy" method.

BASIC METHODS
     package Foo;
     use Class::BuildMethods qw/name rank/;
 
     sub new {
       ... whatever implementation you need
     }

     # later

     my $foo = Foo->new;
     $foo->name('bob');
     print $foo->name;   # prints 'bob'

    Using a simple list with "Class::BuildMethods" adds those methods as
    getters/setters to your class.

    Note that when using a method as a setter, you may only pass in a single
    value. Arrays and hashes should be passed by reference.

DEFAULT VALUES
     package Foo;
     use Class::BuildMethods
       'name',
       rank => { default => 'private' };

     # later

     my $foo = Foo->new;
     print $foo->rank;   # prints 'private'
     $foo->rank('corporal');
     print $foo->rank;   # prints 'corporal'

    After any method name passed to "Class::BuildMethods", you may pass it a
    hash reference of constraints. If a key of "default" is found, the value
    for that key will be assigned as the default value for the method.

VALIDATION
     package Drinking::Buddy;
     use Class::BuildMethods;
       'name',
       age => {
         validate => sub {
            my ($self, $age) = @_;
            die "Too young" if $age < 21;
         }
       };

     # later

     my $bubba = Drinking::Buddy->new;
     $bubba->age(18);   # fatal error
     $bubba->age(21);   # Works
     print $bubba->age; # prints '21'

    If a key of "validate" is found, a subroutine is expected as the next
    argument. When setting a value, the subroutine will be called with the
    invocant as the first argument and the new value as the second argument.
    You may supply any code you wish to enforce validation.

ADDING METHODS AT RUNTIME
  build
      Class::BuildMethods->build(
        'name',
        rank => { default => 'private' }
      );

    This allows you to add the methods at runtime. Takes the same arguments
    as the import list to the class.

CLEANING UP
  destroy
      Class::BuildMethods->destroy($instance);

    This method destroys instance data for the instance supplied.

    Ordinarily you should never have to call this as a "DESTROY" method is
    installed in your namespace which does this for you. However, if you
    need a custom destroy method, provide the special "[NO_DESTROY]" token
    to "Class::BuildMethods" when you're creating it.

     use Class::BuildMethods qw(
        name
        rank
        serial
        [NO_DESTROY]
     );

     sub DESTROY {
       my $self shift;
       # whatever cleanup code you need
       Class::BuildMethods->destroy($self);
     }

  reset
      Class::BuildMethods->reset;   # assumes current package
      Class::BuildMethods->reset($package);

    This methods deletes all of the values for the methods added by
    "Class::BuildMethods". Any methods with default values will now have
    their default values restored. It does not remove the methods. Returns
    the number of methods reset.

  reclaim
      Class::BuildMethods->reclaim;   # assumes current package
      Class::BuildMethods->reclaim($package);
 
    Like "reset" but more final. Removes any values set for methods, any
    default values and pretty much any trace of a given module from this
    package. It does not remove the methods. Any attempt to use the the
    autogenerated methods after this method is called is not guaranteed.

  packages
      my @packages = Class::BuildMethods->packages;

    Returns a sorted list of packages for which methods have been built. If
    "reclaim" has been called for a package, this method will not return
    that package. This is generally useful if you need to do a global code
    cleanup from a remote package:

     foreach my $package (Class::BuildMethods->packages) {
        Class::BuildMethods->reclaim($package);
     }
     # then whatever teardown you need

    In reality, you probably will never need this method.

CAVEATS
    Some people will not be happy that if they need to store an array or a
    hash they must pass them by reference as each generated method expects a
    single value to be passed in when used as a "setter". This is because
    this module is designd to be *simple*. It's very lightweight and very
    fast.

AUTHOR
    Curtis "Ovid" Poe, ""

ACKNOWLEDGEMENTS
    Thanks to Kineticode, Inc. for supporting development of this package.

BUGS
    Please report any bugs or feature requests to
    "bug-class-buildmethods@rt.cpan.org", or through the web interface at
    . I
    will be notified, and then you'll automatically be notified of progress
    on your bug as I make changes.

COPYRIGHT & LICENSE
    Copyright 2005 Curtis "Ovid" Poe, all rights reserved.

    This program is free software; you can redistribute it and/or modify it
    under the same terms as Perl itself.


Great Class - was about to do something similar.

warden on 2005-11-24T03:23:04

Yo Ovid,

I have been working with one of my coworkers at Yahoo! on the design of a class very much like the one you just wrote, having many of the same issues with the existing stuff on CPAN.  I really like what you've done, and we may just end up using your classes, but we have a few things we'd like to add.  I'll list them here so you can tell me if they make sense.  If not, I'll be happy to explain my reasoning, or let you try to change my mind.

My Wishlist:

- A 'description' attribute for meaningful error messages and auto-generated documentation
- Reflection: ability to get hash of { member_name => definition } at runtime (for auto-generated docs, auto-commandline-parsing, etc).  This could possibly be accomplished just by the convention of creating a package constant containing this hash, and then passing this to ->build.  This would require ->build to take a hashref of (member => definition) instead a loose set of key=>definition pairs.  But I think it would be better to have ->build take a hashref anyway, since if ever build needs additional parameters, we don't want to 'use up all of @_'.
- A simpler syntax for cases where you just have name => validation: e.g. { foo => qx/.../}
- Multiple validation conditions (as arrayref).
- Don't pass member defintions to 'use'.  Instead, always explicitly call ->build.  Maybe not important, but I think it's more 'safe' and clean, at the cost of syntactic convenience.

The following sample code demonstrates each of these features.

    package MyClass;

    use Class::BuildMethods; # don't pass method definitions here.

    # explicitly build methods
    Class::BuildMethods->build(
        # methods defined with a hash that can be read at runtime
        {
            foo => {
                validate => qr/x/,
                description => q{a string containing the letter 'x'}
            },
            bar => [
                {
                    validate => $RE{num}{real},
                    description => 'a real number',
                },
                ...some other condition...

            ],
            baz => qr/simple regex/,
        }
    );

Re:Great Class - was about to do something similar

Ovid on 2005-11-24T04:12:18

I'll consider this, but I have some questions about your needs. First, I had considered allowing this:

foo => $RE{num}{real},

That raises the obvious question of "how will you throw the exception? My class makes no assumptions. Instead, it lets you do this:

sub validate_re (
  my ($re, $message) = shift;
  return sub {
    my ($class, $value) = @_;
    throw_custom_exception $message unless $value =~ $re;
  }
}

Class::BuildMethods->build(
  foo => {
    validate => validate_re(
      $RE{num}{real},
      "Argument to foo() must be a real number"
    )
  }
);

Of course, autogenerating plenty of those for custom needs is simple. That's what I like about just providing a subref for validation. It gives you virtually unlimited flexibility without making the code of Class::BuildMethods more complex than it needs to be. I could add a default exception handler method which allows this, but it would be more difficult to get custom error messages and would likely violate my "keep it simple" rule.

As for always passing a hashref, it kills the common case for most people:

use Class::Build qw/foo bar baz/;

Instead, they'd be forced to do this:

use Class::Build {
  foo => '',
  bar => '',
  baz => '',
};

Naturally, optimizing for the common case is my primary intent. Of course, I could provide an alternative to the build method to allow for the extended syntax, but I'd probably shove the docs for it into an "advanced usage" section so that most folks see a nice simple page of documentation. That way, I could simply have it properly munge its arguments and pass them to build.

Thoughts? My main goal is to keep the core of this module as dead simple as possible. If that goal doesn't meet your needs, I wouldn't be offended if you forked it for your personal needs (and, of course, I couldn't stop you :)

In any event, my overriding consideration here is to keep this simple and easy to use. Anything which violates that has to be rejected (which is why I've rejected plenty of my own ideas so far).

Re:Great Class - was about to do something similar

warden on 2005-11-24T07:05:20

Great point: a coderef will probably support most use cases.

However, I don't see much need to allow the user to customize exceptions.  Why would you need anything but croak "$value is not a $description"?

Even if you think this is crucial...consider an alternative interface where validate subroutines just return true or false, and the 'description' parameter defines the exception message by default.  The disadvantage is that you now have to define your accessor with both a 'validate' property and a 'description' property.  But the advantages are:
    - the coderef can still throw an exception if you do want funny exceptions
    - validation subroutines that just return t/f can be reused in other contexts
    - the 'description' attribute is now meta-information about your accessor that can be used in other useful.  This is a feature I have grand plans for
    - you'll have greater consistancy: all exception messages will look the same
    - if exceptions are being thrown out of the validation subroutine, it might be hackish to get the exceptions to come from the point of view of the caller
    - you won't have to type more, even though you have more attributes
        foo => {
            validate => sub { /regex/ },
            message => "a bar"
        }
    is about the same as than as:
        foo => {
            validate => sub { /regex/ or croak "foo is not a bar"},
        }

As for ->build taking a hash, vs. passing parameters to 'use': if you expose a public ->build that takes a hash (I imagine the 'import' method would call this more general ->build, so you need it anyway), then that keeps the class simple but solves most of my requirements for introspection/reflection.

Another thing to think about: I often run into situations where the 'default' for an accessor needs to itself be a subroutine...

Also, I think that, realistically, you'll find that people will sometimes want different "types" of accessors: readonly, writeonly, coersion (forcing arrayref or hashref, or forcing utf8, for example), accessors that override an existing sub but use that sub as the default.  It would be nice if this class was architected in such a way that other common types of accessors can added on eventually (perhaps as plug-in modules).  So you can define different types of accessors, but still have them all in one place.  So maybe:

use Class::BuildMethods::SomeTypes 'FORCE_ARRAY';
use Class::BuildMethods foo => {
    validate => sub { ... },
    type => FORCE_ARRAY
}

I know you are wary of feature creep, and of course you have no obligation to meet my requirements.  But I have thought alot about this, and something with all these features would once and for all solve my accessor problems.

Thoughts?

Re:Great Class - was about to do something similar

malte on 2005-11-24T12:09:12

However, I don't see much need to allow the user to customize exceptions. Why would you need anything but croak "$value is not a $description"?

Well, in my team we use the Error module for all Exceptions. The exception above should probably be something like Project::Exceptions::Validation::Accessor::MyField, which would be a sub class of a more general exception.

Re:Great Class - was about to do something similar

warden on 2005-11-25T02:51:11

But you surely don't require that all modules used in your project throw a special type of Exception. It's great if you write your own modules to use more sophisticated exceptions, but you should be able to use modules that thows standard Perl text exceptions. There's no reason that we need to treat this class any differently than any other class that throws exceptions.

Re:Great Class - was about to do something similar

Ovid on 2005-11-25T03:12:32

For Bricolage, we throw custom exceptions for localization reasons: we present the exception in the user's native language. However, there's no requirement to use custom exceptions with Class::BuildMethods. Just croak, die, or whatever you'd like to do.

Implementation-agnostic Method Generators

simonm on 2005-11-24T16:22:45

It's implementation agnostic. You can used a blessed typeglob for all I care (most modules which build your methods assume you're using a specific type of blessed reference). ... Note that the first item on my list was the most important yet I couldn't find any modules in this namespace which respected that requirement. I'm sure they're there, but where?

Several of the method-generators included with my Class::MakeMethods have this capability. (Specifically, those that end in ::InsideOut or ::Inheritable.)

I'm sorry those aren't easier to find; I know that the documentation leaves a lot to be desired. (Patches and suggestions welcome!)

It's not a light-weight distribition, and there aren't the same easy validation hooks, but it's worth looking at if you need a million and one different kinds of methods. use Class::MakeMethods::Standard::Inheritable 'scalar' => [ 'foo', 'bar' ];