views:

230

answers:

5

I am writing a class that is linked to an external resource. One of the methods is a delete method that destroys the external resource. No further method calls should be made on that object. I was thinking of setting a flag and die'ing inside of all of the methods if the flag is set, but is there a better, easier way? Something involving DESTROY maybe?

So far, I am really liking Axeman's suggestion, but using AUTOLOAD because I am too lazy to recreate all of the methods:

#!/usr/bin/perl

use strict;
use warnings;

my $er = ExternalResource->new;

$er->meth1;
$er->meth2;

$er->delete;

$er->meth1;
$er->meth2;

$er->undelete;

$er->meth1;
$er->meth2;

$er->delete;

$er->meth1;
$er->meth2;
$er->meth3;

package ExternalResource;

use strict;
use warnings;

sub new {
    my $class = shift;
    return bless {}, $class;
}

sub meth1 {
    my $self = shift;
    print "in meth1\n";
}

sub meth2 {
    my $self = shift;
    print "in meth2\n";
}

sub delete {
    my $self = shift;
    $self->{orig_class} = ref $self;
    return bless $self, "ExternalResource::Dead";
}

package ExternalResource::Dead;

use strict;
use Carp;

our $AUTOLOAD;
BEGIN {
our %methods = map { $_ => 1 } qw/meth1 meth2 delete new/;
}
our %methods;

sub undelete {
    my $self = shift;
    #do whatever needs to be done to undelete resource
    return bless $self, $self->{orig_class};
}

sub AUTOLOAD {
    my $meth = (split /::/, $AUTOLOAD)[-1];
    croak "$meth is not a method for this object"
     unless $methods{$meth};
    carp "can't call $meth on object because it has been deleted";
    return 0;
}
+6  A: 

Is there a problem with simply considering the object in an invalid state. If the users hang on to it, isn't that their problem?

Here are some considerations:

  • Have you already decided whether it's worth dying over?

  • Chances are that if you have a function that is encapsulated enough, you really don't want to have the users parse through your code. For that purpose, you probably wouldn't like to use what I call the Go-ahead-and-let-it-fail pattern. 'Can't call method "do_your_stuff" on an undefined value' probably won't work as well for encapsulation purposes. Unless you tell them "Hey you deleted the object!

Here are some suggestions:

  • You could rebless the object into a class whose only job is to indicate an invalid state. It has the same basic form, but all symbols in the table point to a sub that just says "Sorry can't do it, I've been shut down (You shut me down, remember?)."

  • You could undef $_[0] in the delete. Then they get a nice 'Can't call method "read_from_thing" on an undefined value' from a line in their code--provided that they aren't going through an elaborate decorating or delegation process. But as pointed out by chaos, this doesn't clear up more than one reference (as I've adapted by example code below to show).


Some proof of concept stuff:

use feature 'say';

package A;

sub speak { say 'Meow!'; }

sub done { undef $_[0]; }

package B;

sub new { return bless {}, shift; }

sub speak { say 'Ruff!' }

sub done { bless shift, 'A'; }

package main;

my $a = B->new();
my $b = $a;

$a->speak(); # Ruff!
$b->speak(); # Ruff!
$a->done();
$a->speak(); # Meow!
$b->speak(); # Meow! <- $b made the switch
$a->done();
$b->speak(); # Meow!
$a->speak(); # Can't call method "speak" on an undefined value at - line 28
Axeman
I like option two, it keeps the normal methods clean, and I can throw a warning instead of dying. Something like package ClassName::Dead; use Carp; sub AUTOLOAD { carp "can't call $AUTOLOAD on object because it has been deleted" }
Chas. Owens
Because I've made an edit, what Chas refers to as "option two" is now "solution one".
Axeman
@Chas, Is there no way to "revivify" it?
Axeman
No, once the external resource has been deleted there is no undelete, but that would be easy to add one for cases where undelete was possible (an undelete method that rebless to the original class would work).
Chas. Owens
Interesting, but there's also no way to recreate a similar resource with roughly similar capabilities?
Axeman
I would rebless it, but make the rebless-ed class one that prints a nice warning for any method called on it instead of the Perl warning about undefined values. Effectively, the object becomes a null object.
brian d foy
I am writing an interface to the http://drop.io API. You can create a drop, add things to the drop, and then delete the drop. You could recreate the drop, but it would be empty. You could download all of the files in the drop before deleting, but that could be gigs of data.
Chas. Owens
@brian d foy - that is essentially what I have done (see example code in the question), but with AUTOLOAD because I am lazy.
Chas. Owens
+2  A: 

Ideally, it should fall out of scope. If, for some reason, a proper scope can't be deliniated, and you're worried about references accidentally keeping the resource active, might consider weak references (Scalar::Util is core in at least 5.10).

Anonymous
Since you have 5.10 you can run a new utility called corelist that will tell you if/when a module was added to Core Perl. In this case Scalar::Util was added in 5.007003 (aka 5.7.3), which means we got it in 5.8.0.
Chas. Owens
Thanks for reminding me about corelist. I'm always checking my version to make sure people know what I'm familiar with; I should check that instead.
Anonymous
+2  A: 

You could make the users only get weakrefs to the object, with the single strong reference kept inside your module. Then when the resource is destroyed, delete the strong reference and poof, no more objects.

chaos
Wouldn't undef $_[0] accomplish the same thing? It did in my test code.
Axeman
undef $_[0] just destroys that reference to the object. The rest are untouched.
chaos
Good point. Your way would just abduct every single copy the users had made.
Axeman
This is a good solution, but I think the error message that can be returned from Axeman's solution is more helpful. Also, I don't want them calling methods (since the external resource won't answer), but the members are all still valid (and useful).
Chas. Owens
Yeah, Axeman's rebless is definitely very slick. And somehow very Perlish. :)
chaos
A: 

With Moose you can alter the class using its MOP underpinnings:

package ExternalResource;
use Moose;
use Carp;

sub meth1 {
    my $self = shift;
    print "in meth1\n";
}

sub meth2 {
    my $self = shift;
    print "in meth2\n";
}

sub delete {
    my $self = shift;
    my %copy;   # keeps copy of original subref
    my @methods = grep { $_ ne 'meta' } $self->meta->get_method_list;

    for my $meth (@methods) {
        $copy{ $meth } = \&$meth;
        $self->meta->remove_method( $meth );
        $self->meta->add_method( $meth => sub {
            carp "can't call $meth on object because it has been deleted";
            return 0;
        });
    }

    $self->meta->add_method( undelete => sub {
        my $self = shift;
        for my $meth (@methods) {
            $self->meta->remove_method( $meth );
            $self->meta->add_method( $meth => $copy{ $meth } );
        }
        $self->meta->remove_method( 'undelete' );
    });
}

Now all current and new instances of ExternalResource will reflect whatever the current state is.

/I3az/

draegtun
That would be bad, deleting one resource shouldn't break all of them.
Chas. Owens
Yes I empathised this because wasn't sure if this was helpful to you or not. The code maybe useful for other requirements so I'll post another answer which shows how to amends the object.
draegtun
+1  A: 

Following on from the comments in my first answer here is "one way" to amend an object behaviour with Moose.

{
    package ExternalResource;
    use Moose;
    with 'DefaultState';
    no Moose;
}

{
    package DefaultState;
    use Moose::Role;

    sub meth1 {
        my $self = shift;
        print "in meth1\n";
    }

    sub meth2 {
        my $self = shift;
        print "in meth2\n";
    }

    no Moose::Role;
}

{
    package DeletedState;
    use Moose::Role;

    sub meth1 { print "meth1 no longer available!\n" }
    sub meth2 { print "meth2 no longer available!\n" }

    no Moose::Role;
}

my $er = ExternalResource->new;
$er->meth1;     # => "in meth1"
$er->meth2;     # => "in meth2"

DeletedState->meta->apply( $er );
my $er2 = ExternalResource->new;
$er2->meth1;    # => "in meth1"  (role not applied to $er2 object)
$er->meth1;     # => "meth1 no longer available!"
$er->meth2;     # => "meth2 no longer available!"

DefaultState->meta->apply( $er );
$er2->meth1;    # => "in meth1"
$er->meth1;     # => "in meth1"
$er->meth2;     # => "in meth2"

There are other ways to probably achieve what you after in Moose. However I do like this roles approach.

Certainly food for thought.

/I3az/

draegtun