Search code examples
perlabstract-classmoose

Prevent instantiation of a Moose abstract class


I am using Perl with Moose, and have to prevent instantiation of an abstract class.

The project is in a quite advanced stage - too late for Moose::Role or MooseX::*.

I am thinking about checking a package name against a class name in BUILDARGS, and calling die if there's a match.

Is there any problems with this approach?

package Foo::Abstract {

    use Moose;

    has 'test' => ( isa => 'Int', is => 'rw', default => '0' );

    around BUILDARGS => sub {
        die if $_[1] eq __PACKAGE__;
        $orig  = shift;
        $class = shift;
        $class->$orig( @_ );
    };

    no Moose;
}

package Foo::Concrete {

    use Moose;

    extends 'Foo::Abstract';

    no Moose;
}

use Test::More;
use Test::Exception;

dies_ok { Foo::Abstract->new() } "cannot instantiate. OK";

my $c;
lives_ok { $c = Foo::Concrete->new() } "instantiated Foo::Concrete. OK";

ok( 0 == $c->test );

done_testing();

Solution

  • As several people have pointed out in comments you probably should be using a Role and making the change in every "subclass" to do composition. However you make a compelling argument for laziness (one change in one place during a refactor).

    My suggestion would be "do both". Refactor the existing class you want to be abstract out into a role:

    mv lib/Foo/Abstract.pm lib/Foo/Role/Interface.pm; 
    perl -pie's/\bFoo::Abstract\b/Foo::Role::Interface/g' !$
    

    Then in a new Foo::Abstract simply do:

    package Foo::Abstract;
    use Moose;
    with qw(Foo::Role::Interface);
    around BUILDARGS => sub { 
        $_[1] ne __PACKAGE__ ? shift->(@_) : die __PACKAGE__ . 'is ABSTRACT';
    }
    1;
    

    This way you can slowly replace the extends qw(Foo::Abstract) over time with the more appropriate with qw(Foo::Role::Interface) but don't have to bite that cost all up front. You can even document that this is the plan in Foo::Abstract so that other developers who come along help with the conversion.