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;
}
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
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).
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.
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.
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.