Skip to content

hoytech/Object-Sub

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

package Object::Sub;

use strict;

our $VERSION = '0.103';

our $AUTOLOAD;

use overload fallback => 1,
             '&{}' => sub {
                        my $self_ref = \$_[0];

                        return sub {
                          return $$self_ref->{__object_sub_internal_cb}->($$self_ref, undef, @_);
                        };
                      };

sub AUTOLOAD {
  die "$_[0] is not an object" if !ref $_[0];

  my $name = $AUTOLOAD;
  $name =~ s/.*://;

  return $_[0]->{__object_sub_internal_cb}->($_[0], $name, @_[1 .. $#_]);
}

sub new {
  my ($class, $cb) = @_;

  if (ref $cb eq 'HASH') {
    my $orig_cb = $cb;
    $cb = sub {
      ($orig_cb->{$_[1]} || die "unable to find method $_[1]")->($_[0], @_[2 .. $#_]);
    };
  }

  die "need a callback" if ref $cb ne 'CODE';

  my $self = { __object_sub_internal_cb => $cb, };
  bless $self, $class;

  return $self;
}

## Prevent DESTROY method from being handled by AUTOLOAD

sub DESTROY {
}


1;



__END__

=encoding utf-8

=head1 NAME

Object::Sub - Create objects without those pesky classes

=head1 SYNOPSIS

    use Object::Sub;

    my $obj = Object::Sub->new(sub {
                  my ($self, $method, @args) = @_;

                  print "self: $self, method name: $method, first arg: $args[0]\n";
              });

    $obj->whatever(123);
    ## self: Object::Sub=HASH(0xc78eb0), method name: whatever, first arg: 123

    $obj->(123);
    ## self: Object::Sub=HASH(0xc78eb0), method name: , first arg: 123
    ##   ($method is undef)

Alternatively, you can use a hash of subs:

    my $obj = Object::Sub->new({
        add => sub {
            my ($self, $num1, $num2) = @_;
            return $num1 + $num2;
        },
        mul => sub {
            my ($self, $num1, $num2) = @_;
            return $num1 * $num2;
        },
    });

    $obj->add(2, 3);
    ## => 5

=head1 DESCRIPTION

Sometimes you want something that acts like an object but you don't want to go to all the trouble of creating a new package, with constructor and methods and so on. This module is a trivial wrapper around perl's L<AUTOLOAD> functionality which intercepts method calls and lets you handle them in a single C<sub>. It also uses L<overload> so that you can additionally treat the object as a C<sub> if you desire.

=head1 USE-CASES

=head2 AUTOLOAD SYNTACTIC SUGAR

L<AUTOLOAD> allows you to dispatch on method names at run-time which can sometimes be useful, for example in RPC protocols where you transmit method call messages to another process for them to be executed remotely. Unfortunately, using L<AUTOLOAD> is a bit annoying since the interface is somewhat arcane. L<Object::Sub> is a nicer interface to the most commonly used AUTOLOAD functionality:

    my $obj = Object::Sub->new(sub {
                my ($self, $method, @args) = @_;

                my $rpc_input = encode_json({ method => $method, args => [ @args ] });

                my $rpc_output = do_rpc_call($rpc_input);

                return decode_json($rpc_output);
              });

Because C<Object::Sub> objects can also be treated as subs, your RPC interface can support sub-routine calls on the objects as well as method calls, even on the same object.

=head2 PLACE-HOLDER OBJECTS

Some APIs require you to pass in or provide an object but then don't actually end up using it. Instead of passing in undef and getting a weird C<Can't call method "XYZ" on an undefined value> error, you can pass in an L<Object::Sub> which will throw a "helpful" exception instead:

    my $obj = Some::API->new(
                logger => Object::Sub->new(sub { die "FIXME: add logger" }),
              );

Alternatively, you may choose to minimally implement the API "inline" in your program:

    my $obj = Some::API->new(
                logger => Object::Sub->new(sub {
                            my ($self, $method, @args) = @_;

                            return if $method eq 'debug';

                            say STDERR "Some::API $method: " . join(' ', @args);
                          })
              );

=head2 LAZY OBJECT CREATION

Again, some APIs may never end up using an object so you may wish to "lazily" defer the creation of that object until a method is actually called on it. This module can help you make the cases where it doesn't use it more efficient.

For example, suppose you have a large L<CGI> script which always opens a L<DBI> connection but only actually accesses this connection for a small portion of runs. You can prevent the script from accessing the database on the majority of runs with L<Object::Sub>:

    my $dbh = Object::Sub->new(sub {
                require DBI;
                $_[0] = DBI->connect($dsn, $user, $pass, { RaiseError => 1 })
                    || die "Unable to connect to database: $DBI::errstr";

                my ($self, $method, @args) = @_;
                return $self->$method(@args);
              });

Note how we don't even load or compile the module until the first method is called. After you call a method on C<$dbh> it changes from a C<Object::Sub> object into a C<DBI> object (assuming the C<< DBI->connect >> constructor succeeds). This works because the C<$_[0]> argument is actually an alias to C<$dbh> and can be modified.

To demonstrate this, here is an example with L<Session::Token>:

    my $o = Object::Sub->new(sub {
              require Session::Token;
              $_[0] = Session::Token->new;

              my ($self, $method, @args) = @_;
              return $self->$method(@args);
            });

    say ref $o;
    ## Object::Sub

    say $o->get;
    ## mhDPtfLlFMGl5kyNcJgFt7

    say ref $o;
    ## Session::Token

    say $o->get;
    ## 4JYkGgwWbYWGleU7Qk912P

With L<Object::Sub> you can lazily "create" and pass around objects before their constructor code has even been loaded.


=head1 BUGS

Although not really a bug in this module, common perl code tends to copy references of objects. Any code that overwrites the caller object (for example in the L<LAZY OBJECT CREATION> section) will only update one of the copies.


=head1 SEE ALSO

L<Object-Sub github repo|https://github.com/hoytech/Object-Sub>

=head1 AUTHOR

Doug Hoyte, C<< <doug@hcsw.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2015-2016 Doug Hoyte.

This module is licensed under the same terms as perl itself.

About

Create objects without those pesky classes

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages